diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..352204d8f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +*.html eol=lf +*.css eol=lf +*.js eol=lf +*.md eol=lf +*.json eol=lf +*.yml eol=lf diff --git a/.travis.yml b/.travis.yml index f4f24e0e28..bcad7a389a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ - language: node_js - node_js: - - "0.8" - - before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start - - npm install --quiet -g grunt-cli karma - - npm install - + language: node_js + node_js: + - "0.8" + + before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - npm install --quiet -g grunt-cli karma + - npm install + script: grunt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b445dcd1dd..72e7a82d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,93 +1,93 @@ -# 0.3.0 (2013-04-30) - -## Features - -- **progressbar:** - - add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072)) -- **rating:** - - add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369)) -- **typeahead:** - - support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe)) - - support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88)) -- **tooltip:** - - added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8)) - - added appendToBody to $tooltip ([1ee467f8](https://github.com/angular-ui/bootstrap/commit/1ee467f8)) - - added tooltip-html-unsafe directive ([45ed2805](https://github.com/angular-ui/bootstrap/commit/45ed2805)) - - support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b)) - -## Bug Fixes - -- **alert:** - - don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a)) -- **carousel:** - - Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565)) -- **collapse:** - - remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16)) -- **dialog:** - - set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31)) - - close dialog on location change ([474ce52e](https://github.com/angular-ui/bootstrap/commit/474ce52e)) - - IE8 fix to not set data() against text nodes ([a6c540e5](https://github.com/angular-ui/bootstrap/commit/a6c540e5)) - - fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9)) -- **tabs:** - - remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd)) -- **tooltip:** - - fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487)) -- **typeahead:** - - close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd)) - - stop keydown event propagation when ESC pressed to discard matches ([22a00cd0](https://github.com/angular-ui/bootstrap/commit/22a00cd0)) - - correctly render initial model value ([929a46fa](https://github.com/angular-ui/bootstrap/commit/929a46fa)) - - correctly higlight matches if query contains regexp-special chars ([467afcd6](https://github.com/angular-ui/bootstrap/commit/467afcd6)) - - fix matches pop-up positioning issues ([74beecdb](https://github.com/angular-ui/bootstrap/commit/74beecdb)) - -# 0.2.0 (2013-03-03) - -## Features - -- **dialog:** - - Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f)) -- **modal:** - - allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b)) -- **buttons:** - - add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4)) -- **carousel:** - - add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee)) -- **typeahead:** - - add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2)) -- **accordion:** - - enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4)) -- **pagination:** - - add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454)) - -## Bug fixes - -- **dialog:** - - update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486)) - - $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c)) -- **tooltip:** - - don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e)) - - fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d)) -- **collapse:** - - Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119)) -- **accordion:** - - fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6)) -- **typeahead:** - - update inputs value on mapping where label is not derived from the model ([a5f64de](https://github.com/angular-ui/bootstrap/commit/a5f64de)) - -# 0.1.0 (2013-02-02) - -_Very first, initial release_. - -## Features - -Version `0.1.0` was released with the following directives: - -* accordion -* alert -* carousel -* dialog -* dropdownToggle -* modal -* pagination -* popover -* tabs -* tooltip +# 0.3.0 (2013-04-30) + +## Features + +- **progressbar:** + - add progressbar directive ([261f2072](https://github.com/angular-ui/bootstrap/commit/261f2072)) +- **rating:** + - add rating directive ([6b5e6369](https://github.com/angular-ui/bootstrap/commit/6b5e6369)) +- **typeahead:** + - support the editable property ([a40c3fbe](https://github.com/angular-ui/bootstrap/commit/a40c3fbe)) + - support typeahead-loading bindable expression ([b58c9c88](https://github.com/angular-ui/bootstrap/commit/b58c9c88)) +- **tooltip:** + - added popup-delay option ([a79a2ba8](https://github.com/angular-ui/bootstrap/commit/a79a2ba8)) + - added appendToBody to $tooltip ([1ee467f8](https://github.com/angular-ui/bootstrap/commit/1ee467f8)) + - added tooltip-html-unsafe directive ([45ed2805](https://github.com/angular-ui/bootstrap/commit/45ed2805)) + - support for custom triggers ([b1ba821b](https://github.com/angular-ui/bootstrap/commit/b1ba821b)) + +## Bug Fixes + +- **alert:** + - don't show close button if no close callback specified ([c2645f4a](https://github.com/angular-ui/bootstrap/commit/c2645f4a)) +- **carousel:** + - Hide navigation indicators if only one slide ([aedc0565](https://github.com/angular-ui/bootstrap/commit/aedc0565)) +- **collapse:** + - remove reference to msTransition for IE10 ([55437b16](https://github.com/angular-ui/bootstrap/commit/55437b16)) +- **dialog:** + - set _open to false on init ([dcc9ef31](https://github.com/angular-ui/bootstrap/commit/dcc9ef31)) + - close dialog on location change ([474ce52e](https://github.com/angular-ui/bootstrap/commit/474ce52e)) + - IE8 fix to not set data() against text nodes ([a6c540e5](https://github.com/angular-ui/bootstrap/commit/a6c540e5)) + - fix $apply in progres on $location change ([77e6acb9](https://github.com/angular-ui/bootstrap/commit/77e6acb9)) +- **tabs:** + - remove superfluous href from tabs template ([38c1badd](https://github.com/angular-ui/bootstrap/commit/38c1badd)) +- **tooltip:** + - fix positioning issues in tooltips and popovers ([6458f487](https://github.com/angular-ui/bootstrap/commit/6458f487)) +- **typeahead:** + - close matches popup on click outside typeahead ([acca7dcd](https://github.com/angular-ui/bootstrap/commit/acca7dcd)) + - stop keydown event propagation when ESC pressed to discard matches ([22a00cd0](https://github.com/angular-ui/bootstrap/commit/22a00cd0)) + - correctly render initial model value ([929a46fa](https://github.com/angular-ui/bootstrap/commit/929a46fa)) + - correctly higlight matches if query contains regexp-special chars ([467afcd6](https://github.com/angular-ui/bootstrap/commit/467afcd6)) + - fix matches pop-up positioning issues ([74beecdb](https://github.com/angular-ui/bootstrap/commit/74beecdb)) + +# 0.2.0 (2013-03-03) + +## Features + +- **dialog:** + - Make $dialog 'resolve' property to work the same way of $routeProvider.when ([739f86f](https://github.com/angular-ui/bootstrap/commit/739f86f)) +- **modal:** + - allow global override of modal options ([acaf72b](https://github.com/angular-ui/bootstrap/commit/acaf72b)) +- **buttons:** + - add checkbox and radio buttons ([571ccf4](https://github.com/angular-ui/bootstrap/commit/571ccf4)) +- **carousel:** + - add slide indicators ([3b677ee](https://github.com/angular-ui/bootstrap/commit/3b677ee)) +- **typeahead:** + - add typeahead directive ([6a97da2](https://github.com/angular-ui/bootstrap/commit/6a97da2)) +- **accordion:** + - enable HTML in accordion headings ([3afcaa4](https://github.com/angular-ui/bootstrap/commit/3afcaa4)) +- **pagination:** + - add first/last link & constant congif options ([0ff0454](https://github.com/angular-ui/bootstrap/commit/0ff0454)) + +## Bug fixes + +- **dialog:** + - update resolve section to new syntax ([1f87486](https://github.com/angular-ui/bootstrap/commit/1f87486)) + - $compile entire modal ([7575b3c](https://github.com/angular-ui/bootstrap/commit/7575b3c)) +- **tooltip:** + - don't show tooltips if there is no content to show ([030901e](https://github.com/angular-ui/bootstrap/commit/030901e)) + - fix placement issues ([a2bbf4d](https://github.com/angular-ui/bootstrap/commit/a2bbf4d)) +- **collapse:** + - Avoids fixed height on collapse ([ff5d119](https://github.com/angular-ui/bootstrap/commit/ff5d119)) +- **accordion:** + - fix minification issues ([f4da4d6](https://github.com/angular-ui/bootstrap/commit/f4da4d6)) +- **typeahead:** + - update inputs value on mapping where label is not derived from the model ([a5f64de](https://github.com/angular-ui/bootstrap/commit/a5f64de)) + +# 0.1.0 (2013-02-02) + +_Very first, initial release_. + +## Features + +Version `0.1.0` was released with the following directives: + +* accordion +* alert +* carousel +* dialog +* dropdownToggle +* modal +* pagination +* popover +* tabs +* tooltip diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bab1ffded6..68367292ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,12 @@ -We are always looking for the quality contributions and will be happy to accept your Pull Requests as long as those adhere to some basic rules: - -* Please make sure that your contribution fits well in the project's context: - * we are aiming at rebuilding boostrap directives in pure AngularJS, without any dependencies on any external JavaScript library; - * the only dependency should be boostrap CSS and its markup structure; - * directives should be html-agnostic as much as possible which in practice means: - * templates should be referred to using the `templateUrl` property - * it should be easy to change a default template to a custom one - * directives shouldn't manipulate DOM structure directly (when possible) -* Please assure that you are submitting quality code, specifically make sure that: - * your directive has accompanying tests and all the tests are passing; don't hesitate to contact us (angular-ui@googlegroups.com) if you ned any help with unit testing - * your PR doesn't break the build; check the Travis-CI build status after opening a PR and push corrective commits if anything goes wrong +We are always looking for the quality contributions and will be happy to accept your Pull Requests as long as those adhere to some basic rules: + +* Please make sure that your contribution fits well in the project's context: + * we are aiming at rebuilding boostrap directives in pure AngularJS, without any dependencies on any external JavaScript library; + * the only dependency should be boostrap CSS and its markup structure; + * directives should be html-agnostic as much as possible which in practice means: + * templates should be referred to using the `templateUrl` property + * it should be easy to change a default template to a custom one + * directives shouldn't manipulate DOM structure directly (when possible) +* Please assure that you are submitting quality code, specifically make sure that: + * your directive has accompanying tests and all the tests are passing; don't hesitate to contact us (angular-ui@googlegroups.com) if you ned any help with unit testing + * your PR doesn't break the build; check the Travis-CI build status after opening a PR and push corrective commits if anything goes wrong diff --git a/Gruntfile.js b/Gruntfile.js index fd0386ec30..4317b037e8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,6 +11,8 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-karma'); // Project configuration. + grunt.util.linefeed = '\n'; + grunt.initConfig({ ngversion: '1.0.5', bsversion: '2.3.1', diff --git a/misc/changelog.tpl.md b/misc/changelog.tpl.md index 21eec97e84..f311f47c54 100644 --- a/misc/changelog.tpl.md +++ b/misc/changelog.tpl.md @@ -1,28 +1,28 @@ -# <%= version%> (<%= today%>) - -## Features - -<% _(changelog.feat).forEach(function(changes, component) { %> -- **<%= component%>:** - <% changes.forEach(function(change) { %> - - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) - <% }) %> -<% }) %> - -## Bug Fixes - -<% _(changelog.fix).forEach(function(changes, component) { %> -- **<%= component%>:** - <% changes.forEach(function(change) { %> - - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) - <% }) %> -<% }) %> - -## Breaking Changes - -<% _(changelog.breaking).forEach(function(changes, component) { %> -- **<%= component%>:** - <% changes.forEach(function(change) { %> - <%= change.msg%> - <% }) %> -<% }) %> +# <%= version%> (<%= today%>) + +## Features + +<% _(changelog.feat).forEach(function(changes, component) { %> +- **<%= component%>:** + <% changes.forEach(function(change) { %> + - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) + <% }) %> +<% }) %> + +## Bug Fixes + +<% _(changelog.fix).forEach(function(changes, component) { %> +- **<%= component%>:** + <% changes.forEach(function(change) { %> + - <%= change.msg%> ([<%= change.sha1%>](https://github.com/angular-ui/bootstrap/commit/<%= change.sha1%>)) + <% }) %> +<% }) %> + +## Breaking Changes + +<% _(changelog.breaking).forEach(function(changes, component) { %> +- **<%= component%>:** + <% changes.forEach(function(change) { %> + <%= change.msg%> + <% }) %> +<% }) %> diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css index 2f77fbe6fe..7c58d86d10 100644 --- a/misc/demo/assets/demo.css +++ b/misc/demo/assets/demo.css @@ -1,92 +1,92 @@ -body { - opacity: 1; - -webkit-transition: opacity 1s ease; - -moz-transition: opacity 1s ease; - transition: opacity 1s; -} - -.ng-cloak { - opacity: 0; -} - -section { - padding-top: 30px; -} - -.page-header h1 > small > a { - color: #999; -} -.page-header h1 > small > a:hover { - text-decoration: none; -} - -.footer { - text-align: center; - padding: 30px 0; - margin-top: 70px; - border-top: 1px solid #e5e5e5; - background-color: #f5f5f5; -} - -.hero-unit { - position: relative; - padding: 40px 0; - color: #fff; - text-align: center; - text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); - background: #020031; - background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); - background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); - background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); - background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%); - background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%); - background: linear-gradient(45deg, #020031 0%,#6d3353 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); - -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); - -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); - box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); - border-radius: 0; - -moz-border-radius: 0; - -webkit-border-radius: 0; - -o-border-radius: 0; -} -.hero-unit .btn, .pagination-centered .btn { - float: none; - font-weight: normal; -} -.hero-unit p { - margin: 1em 0; -} -.bs-docs-social { - margin-top: 1em; - padding: 15px 0; - text-align: center; - background-color: rgba(245,245,245,0.3); - border-top: 1px solid rgba(255,255,255,0.3); - border-bottom: 1px solid rgba(221,221,221,0.3); -} -.bs-docs-social-buttons { - margin-left: 0; - margin-bottom: 0; - padding-left: 0; - list-style: none; -} -.bs-docs-social-buttons li { - display: inline-block; - padding: 5px 8px; - line-height: 1; -} - -.icon-github { - background: no-repeat url('github-16px.png'); - width: 16px; - height: 16px; -} - -/* Not enough room on mobile for markup tab, js tab, and plunk btn. - And no one cares about plunk button on a phone anyway */ -@media only screen and (max-device-width: 480px) { - #plunk-btn { - display: none; - } +body { + opacity: 1; + -webkit-transition: opacity 1s ease; + -moz-transition: opacity 1s ease; + transition: opacity 1s; +} + +.ng-cloak { + opacity: 0; +} + +section { + padding-top: 30px; +} + +.page-header h1 > small > a { + color: #999; +} +.page-header h1 > small > a:hover { + text-decoration: none; +} + +.footer { + text-align: center; + padding: 30px 0; + margin-top: 70px; + border-top: 1px solid #e5e5e5; + background-color: #f5f5f5; +} + +.hero-unit { + position: relative; + padding: 40px 0; + color: #fff; + text-align: center; + text-shadow: 0 1px 3px rgba(0,0,0,.4), 0 0 30px rgba(0,0,0,.075); + background: #020031; + background: -moz-linear-gradient(45deg, #020031 0%, #6d3353 100%); + background: -webkit-gradient(linear, left bottom, right top, color-stop(0%,#020031), color-stop(100%,#6d3353)); + background: -webkit-linear-gradient(45deg, #020031 0%,#6d3353 100%); + background: -o-linear-gradient(45deg, #020031 0%,#6d3353 100%); + background: -ms-linear-gradient(45deg, #020031 0%,#6d3353 100%); + background: linear-gradient(45deg, #020031 0%,#6d3353 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#020031', endColorstr='#6d3353',GradientType=1 ); + -webkit-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); + box-shadow: inset 0 3px 7px rgba(0,0,0,.2), inset 0 -3px 7px rgba(0,0,0,.2); + border-radius: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; + -o-border-radius: 0; +} +.hero-unit .btn, .pagination-centered .btn { + float: none; + font-weight: normal; +} +.hero-unit p { + margin: 1em 0; +} +.bs-docs-social { + margin-top: 1em; + padding: 15px 0; + text-align: center; + background-color: rgba(245,245,245,0.3); + border-top: 1px solid rgba(255,255,255,0.3); + border-bottom: 1px solid rgba(221,221,221,0.3); +} +.bs-docs-social-buttons { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; +} +.bs-docs-social-buttons li { + display: inline-block; + padding: 5px 8px; + line-height: 1; +} + +.icon-github { + background: no-repeat url('github-16px.png'); + width: 16px; + height: 16px; +} + +/* Not enough room on mobile for markup tab, js tab, and plunk btn. + And no one cares about plunk button on a phone anyway */ +@media only screen and (max-device-width: 480px) { + #plunk-btn { + display: none; + } } \ No newline at end of file diff --git a/misc/demo/assets/plunker.js b/misc/demo/assets/plunker.js index 32f1cff222..ffd809ebec 100644 --- a/misc/demo/assets/plunker.js +++ b/misc/demo/assets/plunker.js @@ -1,58 +1,58 @@ -angular.module('plunker', []) - - .factory('plunkGenerator', function ($document) { - - return function (ngVersion, bsVersion, version, module, content) { - - var form = angular.element('
'); - var addField = function (name, value) { - var input = angular.element(''); - input.attr('value', value); - form.append(input); - }; - - var indexContent = function (content, version) { - return '\n' + - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n\n' + - content + '\n' + - ' \n' + - '\n'; - }; - - var scriptContent = function(content) { - return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content; - }; - - addField('description', 'http://angular-ui.github.io/bootstrap/'); - addField('files[index.html]', indexContent(content.markup, version)); - addField('files[example.js]', scriptContent(content.javascript)); - - $document.find('body').append(form); - form[0].submit(); - form.remove(); - }; - }) - - .controller('PlunkerCtrl', function ($scope, plunkGenerator) { - - $scope.content = {}; - - $scope.edit = function (ngVersion, bsVersion, version, module) { - plunkGenerator(ngVersion, bsVersion, version, module, $scope.content); - }; - }) - - .directive('plunkerContent', function () { - return { - link:function (scope, element, attrs) { - scope.$parent.content[attrs.plunkerContent] = element.text(); - } - } - }); +angular.module('plunker', []) + + .factory('plunkGenerator', function ($document) { + + return function (ngVersion, bsVersion, version, module, content) { + + var form = angular.element('
'); + var addField = function (name, value) { + var input = angular.element(''); + input.attr('value', value); + form.append(input); + }; + + var indexContent = function (content, version) { + return '\n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n\n' + + content + '\n' + + ' \n' + + '\n'; + }; + + var scriptContent = function(content) { + return "angular.module('plunker', ['ui.bootstrap']);" + "\n" + content; + }; + + addField('description', 'http://angular-ui.github.io/bootstrap/'); + addField('files[index.html]', indexContent(content.markup, version)); + addField('files[example.js]', scriptContent(content.javascript)); + + $document.find('body').append(form); + form[0].submit(); + form.remove(); + }; + }) + + .controller('PlunkerCtrl', function ($scope, plunkGenerator) { + + $scope.content = {}; + + $scope.edit = function (ngVersion, bsVersion, version, module) { + plunkGenerator(ngVersion, bsVersion, version, module, $scope.content); + }; + }) + + .directive('plunkerContent', function () { + return { + link:function (scope, element, attrs) { + scope.$parent.content[attrs.plunkerContent] = element.text(); + } + } + }); diff --git a/src/accordion/accordion.js b/src/accordion/accordion.js index 54f33452c8..9179e6e81d 100644 --- a/src/accordion/accordion.js +++ b/src/accordion/accordion.js @@ -1,141 +1,141 @@ -angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) - -.constant('accordionConfig', { - closeOthers: true -}) - -.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { - - // This array keeps track of the accordion groups - this.groups = []; - - // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to - this.closeOthers = function(openGroup) { - var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; - if ( closeOthers ) { - angular.forEach(this.groups, function (group) { - if ( group !== openGroup ) { - group.isOpen = false; - } - }); - } - }; - - // This is called from the accordion-group directive to add itself to the accordion - this.addGroup = function(groupScope) { - var that = this; - this.groups.push(groupScope); - - groupScope.$on('$destroy', function (event) { - that.removeGroup(groupScope); - }); - }; - - // This is called from the accordion-group directive when to remove itself - this.removeGroup = function(group) { - var index = this.groups.indexOf(group); - if ( index !== -1 ) { - this.groups.splice(this.groups.indexOf(group), 1); - } - }; - -}]) - -// The accordion directive simply sets up the directive controller -// and adds an accordion CSS class to itself element. -.directive('accordion', function () { - return { - restrict:'EA', - controller:'AccordionController', - transclude: true, - replace: false, - templateUrl: 'template/accordion/accordion.html' - }; -}) - -// The accordion-group directive indicates a block of html that will expand and collapse in an accordion -.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) { - return { - require:'^accordion', // We need this directive to be inside an accordion - restrict:'EA', - transclude:true, // It transcludes the contents of the directive into the template - replace: true, // The element containing the directive will be replaced with the template - templateUrl:'template/accordion/accordion-group.html', - scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope - controller: ['$scope', function($scope) { - this.setHeading = function(element) { - this.heading = element; - }; - }], - link: function(scope, element, attrs, accordionCtrl) { - var getIsOpen, setIsOpen; - - accordionCtrl.addGroup(scope); - - scope.isOpen = false; - - if ( attrs.isOpen ) { - getIsOpen = $parse(attrs.isOpen); - setIsOpen = getIsOpen.assign; - - scope.$watch( - function watchIsOpen() { return getIsOpen(scope.$parent); }, - function updateOpen(value) { scope.isOpen = value; } - ); - - scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; - } - - scope.$watch('isOpen', function(value) { - if ( value ) { - accordionCtrl.closeOthers(scope); - } - if ( setIsOpen ) { - setIsOpen(scope.$parent, value); - } - }); - } - }; -}]) - -// Use accordion-heading below an accordion-group to provide a heading containing HTML -// -// Heading containing HTML - -// -.directive('accordionHeading', function() { - return { - restrict: 'E', - transclude: true, // Grab the contents to be used as the heading - template: '', // In effect remove this element! - replace: true, - require: '^accordionGroup', - compile: function(element, attr, transclude) { - return function link(scope, element, attr, accordionGroupCtrl) { - // Pass the heading to the accordion-group controller - // so that it can be transcluded into the right place in the template - // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] - accordionGroupCtrl.setHeading(transclude(scope, function() {})); - }; - } - }; -}) - -// Use in the accordion-group template to indicate where you want the heading to be transcluded -// You must provide the property on the accordion-group controller that will hold the transcluded element -//
-//
...
-// ... -//
-.directive('accordionTransclude', function() { - return { - require: '^accordionGroup', - link: function(scope, element, attr, controller) { - scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { - if ( heading ) { - element.html(''); - element.append(heading); - } - }); - } - }; -}); +angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) + +.constant('accordionConfig', { + closeOthers: true +}) + +.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) { + + // This array keeps track of the accordion groups + this.groups = []; + + // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to + this.closeOthers = function(openGroup) { + var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; + if ( closeOthers ) { + angular.forEach(this.groups, function (group) { + if ( group !== openGroup ) { + group.isOpen = false; + } + }); + } + }; + + // This is called from the accordion-group directive to add itself to the accordion + this.addGroup = function(groupScope) { + var that = this; + this.groups.push(groupScope); + + groupScope.$on('$destroy', function (event) { + that.removeGroup(groupScope); + }); + }; + + // This is called from the accordion-group directive when to remove itself + this.removeGroup = function(group) { + var index = this.groups.indexOf(group); + if ( index !== -1 ) { + this.groups.splice(this.groups.indexOf(group), 1); + } + }; + +}]) + +// The accordion directive simply sets up the directive controller +// and adds an accordion CSS class to itself element. +.directive('accordion', function () { + return { + restrict:'EA', + controller:'AccordionController', + transclude: true, + replace: false, + templateUrl: 'template/accordion/accordion.html' + }; +}) + +// The accordion-group directive indicates a block of html that will expand and collapse in an accordion +.directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) { + return { + require:'^accordion', // We need this directive to be inside an accordion + restrict:'EA', + transclude:true, // It transcludes the contents of the directive into the template + replace: true, // The element containing the directive will be replaced with the template + templateUrl:'template/accordion/accordion-group.html', + scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope + controller: ['$scope', function($scope) { + this.setHeading = function(element) { + this.heading = element; + }; + }], + link: function(scope, element, attrs, accordionCtrl) { + var getIsOpen, setIsOpen; + + accordionCtrl.addGroup(scope); + + scope.isOpen = false; + + if ( attrs.isOpen ) { + getIsOpen = $parse(attrs.isOpen); + setIsOpen = getIsOpen.assign; + + scope.$watch( + function watchIsOpen() { return getIsOpen(scope.$parent); }, + function updateOpen(value) { scope.isOpen = value; } + ); + + scope.isOpen = getIsOpen ? getIsOpen(scope.$parent) : false; + } + + scope.$watch('isOpen', function(value) { + if ( value ) { + accordionCtrl.closeOthers(scope); + } + if ( setIsOpen ) { + setIsOpen(scope.$parent, value); + } + }); + } + }; +}]) + +// Use accordion-heading below an accordion-group to provide a heading containing HTML +// +// Heading containing HTML - +// +.directive('accordionHeading', function() { + return { + restrict: 'E', + transclude: true, // Grab the contents to be used as the heading + template: '', // In effect remove this element! + replace: true, + require: '^accordionGroup', + compile: function(element, attr, transclude) { + return function link(scope, element, attr, accordionGroupCtrl) { + // Pass the heading to the accordion-group controller + // so that it can be transcluded into the right place in the template + // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] + accordionGroupCtrl.setHeading(transclude(scope, function() {})); + }; + } + }; +}) + +// Use in the accordion-group template to indicate where you want the heading to be transcluded +// You must provide the property on the accordion-group controller that will hold the transcluded element +//
+//
...
+// ... +//
+.directive('accordionTransclude', function() { + return { + require: '^accordionGroup', + link: function(scope, element, attr, controller) { + scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { + if ( heading ) { + element.html(''); + element.append(heading); + } + }); + } + }; +}); diff --git a/src/accordion/test/accordionSpec.js b/src/accordion/test/accordionSpec.js index acad565a2f..2288863b62 100644 --- a/src/accordion/test/accordionSpec.js +++ b/src/accordion/test/accordionSpec.js @@ -1,299 +1,299 @@ -describe('accordion', function () { - var $scope; - - beforeEach(module('ui.bootstrap.accordion')); - beforeEach(module('template/accordion/accordion.html')); - beforeEach(module('template/accordion/accordion-group.html')); - - beforeEach(inject(function ($rootScope) { - $scope = $rootScope; - })); - - describe('controller', function () { - - var ctrl, $element, $attrs; - beforeEach(inject(function($controller) { - $attrs = {}; $element = {}; - ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs }); - })); - - describe('addGroup', function() { - it('adds a the specified group to the collection', function() { - var group1, group2; - ctrl.addGroup(group1 = $scope.$new()); - ctrl.addGroup(group2 = $scope.$new()); - expect(ctrl.groups.length).toBe(2); - expect(ctrl.groups[0]).toBe(group1); - expect(ctrl.groups[1]).toBe(group2); - }); - }); - - describe('closeOthers', function() { - var group1, group2, group3; - beforeEach(function() { - ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop }); - ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop }); - ctrl.addGroup(group3 = { isOpen: true, $on : angular.noop }); - }); - it('should close other groups if close-others attribute is not defined', function() { - delete $attrs.closeOthers; - ctrl.closeOthers(group2); - expect(group1.isOpen).toBe(false); - expect(group2.isOpen).toBe(true); - expect(group3.isOpen).toBe(false); - }); - - it('should close other groups if close-others attribute is true', function() { - $attrs.closeOthers = 'true'; - ctrl.closeOthers(group3); - expect(group1.isOpen).toBe(false); - expect(group2.isOpen).toBe(false); - expect(group3.isOpen).toBe(true); - }); - - it('should not close other groups if close-others attribute is false', function() { - $attrs.closeOthers = 'false'; - ctrl.closeOthers(group2); - expect(group1.isOpen).toBe(true); - expect(group2.isOpen).toBe(true); - expect(group3.isOpen).toBe(true); - }); - - describe('setting accordionConfig', function() { - var originalCloseOthers; - beforeEach(inject(function(accordionConfig) { - originalCloseOthers = accordionConfig.closeOthers; - accordionConfig.closeOthers = false; - })); - afterEach(inject(function(accordionConfig) { - // return it to the original value - accordionConfig.closeOthers = originalCloseOthers; - })); - - it('should not close other groups if accordionConfig.closeOthers is false', function() { - ctrl.closeOthers(group2); - expect(group1.isOpen).toBe(true); - expect(group2.isOpen).toBe(true); - expect(group3.isOpen).toBe(true); - }); - }); - }); - - describe('removeGroup', function() { - it('should remove the specified group', function () { - var group1, group2, group3; - ctrl.addGroup(group1 = $scope.$new()); - ctrl.addGroup(group2 = $scope.$new()); - ctrl.addGroup(group3 = $scope.$new()); - ctrl.removeGroup(group2); - expect(ctrl.groups.length).toBe(2); - expect(ctrl.groups[0]).toBe(group1); - expect(ctrl.groups[1]).toBe(group3); - }); - it('should ignore remove of non-existing group', function () { - var group1, group2; - ctrl.addGroup(group1 = $scope.$new()); - ctrl.addGroup(group2 = $scope.$new()); - expect(ctrl.groups.length).toBe(2); - ctrl.removeGroup({}); - expect(ctrl.groups.length).toBe(2); - }); - }); - }); - - describe('accordion-group', function () { - - var scope, $compile; - var element, groups; - var findGroupLink = function (index) { - return groups.eq(index).find('a').eq(0); - }; - var findGroupBody = function (index) { - return groups.eq(index).find('.accordion-body').eq(0); - }; - - - beforeEach(inject(function(_$rootScope_, _$compile_) { - scope = _$rootScope_; - $compile = _$compile_; - })); - - afterEach(function () { - element = groups = scope = $compile = undefined; - }); - - describe('with static groups', function () { - beforeEach(function () { - var tpl = - "" + - "Content 1" + - "Content 2" + - ""; - element = angular.element(tpl); - angular.element(document.body).append(element); - $compile(element)(scope); - scope.$digest(); - groups = element.find('.accordion-group'); - }); - afterEach(function() { - element.remove(); - }); - - it('should create accordion groups with content', function () { - expect(groups.length).toEqual(2); - expect(findGroupLink(0).text()).toEqual('title 1'); - expect(findGroupBody(0).text().trim()).toEqual('Content 1'); - expect(findGroupLink(1).text()).toEqual('title 2'); - expect(findGroupBody(1).text().trim()).toEqual('Content 2'); - }); - - it('should change selected element on click', function () { - findGroupLink(0).click(); - scope.$digest(); - expect(findGroupBody(0).scope().isOpen).toBe(true); - - findGroupLink(1).click(); - scope.$digest(); - expect(findGroupBody(0).scope().isOpen).toBe(false); - expect(findGroupBody(1).scope().isOpen).toBe(true); - }); - - it('should toggle element on click', function() { - findGroupLink(0).click(); - scope.$digest(); - expect(findGroupBody(0).scope().isOpen).toBe(true); - findGroupLink(0).click(); - scope.$digest(); - expect(findGroupBody(0).scope().isOpen).toBe(false); - }); - }); - - describe('with dynamic groups', function () { - var model; - beforeEach(function () { - var tpl = - "" + - "{{group.content}}" + - ""; - element = angular.element(tpl); - model = [ - {name: 'title 1', content: 'Content 1'}, - {name: 'title 2', content: 'Content 2'} - ]; - - $compile(element)(scope); - scope.$digest(); - }); - - it('should have no groups initially', function () { - groups = element.find('.accordion-group'); - expect(groups.length).toEqual(0); - }); - - it('should have a group for each model item', function() { - scope.groups = model; - scope.$digest(); - groups = element.find('.accordion-group'); - expect(groups.length).toEqual(2); - expect(findGroupLink(0).text()).toEqual('title 1'); - expect(findGroupBody(0).text().trim()).toEqual('Content 1'); - expect(findGroupLink(1).text()).toEqual('title 2'); - expect(findGroupBody(1).text().trim()).toEqual('Content 2'); - }); - - it('should react properly on removing items from the model', function () { - scope.groups = model; - scope.$digest(); - groups = element.find('.accordion-group'); - expect(groups.length).toEqual(2); - - scope.groups.splice(0,1); - scope.$digest(); - groups = element.find('.accordion-group'); - expect(groups.length).toEqual(1); - }); - }); - - describe('is-open attribute', function() { - beforeEach(function () { - var tpl = - "" + - "Content 1" + - "Content 2" + - ""; - element = angular.element(tpl); - scope.open1 = false; - scope.open2 = true; - $compile(element)(scope); - scope.$digest(); - groups = element.find('.accordion-group'); - }); - - it('should open the group with isOpen set to true', function () { - expect(findGroupBody(0).scope().isOpen).toBe(false); - expect(findGroupBody(1).scope().isOpen).toBe(true); - }); - }); - - describe('is-open attribute with dynamic content', function() { - beforeEach(function () { - var tpl = - "" + - "
{{item}}
" + - "Static content" + - "
"; - element = angular.element(tpl); - scope.items = ['Item 1', 'Item 2', 'Item 3']; - scope.open1 = true; - scope.open2 = false; - angular.element(document.body).append(element); - $compile(element)(scope); - scope.$digest(); - groups = element.find('.accordion-group'); - }); - - afterEach(function() { - element.remove(); - }); - - it('should have visible group body when the group with isOpen set to true', function () { - expect(findGroupBody(0)[0].clientHeight).not.toBe(0); - expect(findGroupBody(1)[0].clientHeight).toBe(0); - }); - }); - - describe('accordion-heading element', function() { - beforeEach(function() { - var tpl = - '' + - '' + - 'Heading Element {{x}} ' + - 'Body' + - '' + - ''; - element = $compile(tpl)(scope); - scope.$digest(); - groups = element.find('.accordion-group'); - }); - it('transcludes the content into the heading link', function() { - expect(findGroupLink(0).text()).toBe('Heading Element 123 '); - }); - it('attaches the same scope to the transcluded heading and body', function() { - expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); - }); - - }); - - describe('accordion-heading, with repeating accordion-groups', function() { - it('should clone the accordion-heading for each group', function() { - element = $compile('{{x}}')(scope); - scope.$digest(); - groups = element.find('.accordion-group'); - expect(groups.length).toBe(3); - expect(findGroupLink(0).text()).toBe('1'); - expect(findGroupLink(1).text()).toBe('2'); - expect(findGroupLink(2).text()).toBe('3'); - }); - }); - }); -}); +describe('accordion', function () { + var $scope; + + beforeEach(module('ui.bootstrap.accordion')); + beforeEach(module('template/accordion/accordion.html')); + beforeEach(module('template/accordion/accordion-group.html')); + + beforeEach(inject(function ($rootScope) { + $scope = $rootScope; + })); + + describe('controller', function () { + + var ctrl, $element, $attrs; + beforeEach(inject(function($controller) { + $attrs = {}; $element = {}; + ctrl = $controller('AccordionController', { $scope: $scope, $element: $element, $attrs: $attrs }); + })); + + describe('addGroup', function() { + it('adds a the specified group to the collection', function() { + var group1, group2; + ctrl.addGroup(group1 = $scope.$new()); + ctrl.addGroup(group2 = $scope.$new()); + expect(ctrl.groups.length).toBe(2); + expect(ctrl.groups[0]).toBe(group1); + expect(ctrl.groups[1]).toBe(group2); + }); + }); + + describe('closeOthers', function() { + var group1, group2, group3; + beforeEach(function() { + ctrl.addGroup(group1 = { isOpen: true, $on : angular.noop }); + ctrl.addGroup(group2 = { isOpen: true, $on : angular.noop }); + ctrl.addGroup(group3 = { isOpen: true, $on : angular.noop }); + }); + it('should close other groups if close-others attribute is not defined', function() { + delete $attrs.closeOthers; + ctrl.closeOthers(group2); + expect(group1.isOpen).toBe(false); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(false); + }); + + it('should close other groups if close-others attribute is true', function() { + $attrs.closeOthers = 'true'; + ctrl.closeOthers(group3); + expect(group1.isOpen).toBe(false); + expect(group2.isOpen).toBe(false); + expect(group3.isOpen).toBe(true); + }); + + it('should not close other groups if close-others attribute is false', function() { + $attrs.closeOthers = 'false'; + ctrl.closeOthers(group2); + expect(group1.isOpen).toBe(true); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(true); + }); + + describe('setting accordionConfig', function() { + var originalCloseOthers; + beforeEach(inject(function(accordionConfig) { + originalCloseOthers = accordionConfig.closeOthers; + accordionConfig.closeOthers = false; + })); + afterEach(inject(function(accordionConfig) { + // return it to the original value + accordionConfig.closeOthers = originalCloseOthers; + })); + + it('should not close other groups if accordionConfig.closeOthers is false', function() { + ctrl.closeOthers(group2); + expect(group1.isOpen).toBe(true); + expect(group2.isOpen).toBe(true); + expect(group3.isOpen).toBe(true); + }); + }); + }); + + describe('removeGroup', function() { + it('should remove the specified group', function () { + var group1, group2, group3; + ctrl.addGroup(group1 = $scope.$new()); + ctrl.addGroup(group2 = $scope.$new()); + ctrl.addGroup(group3 = $scope.$new()); + ctrl.removeGroup(group2); + expect(ctrl.groups.length).toBe(2); + expect(ctrl.groups[0]).toBe(group1); + expect(ctrl.groups[1]).toBe(group3); + }); + it('should ignore remove of non-existing group', function () { + var group1, group2; + ctrl.addGroup(group1 = $scope.$new()); + ctrl.addGroup(group2 = $scope.$new()); + expect(ctrl.groups.length).toBe(2); + ctrl.removeGroup({}); + expect(ctrl.groups.length).toBe(2); + }); + }); + }); + + describe('accordion-group', function () { + + var scope, $compile; + var element, groups; + var findGroupLink = function (index) { + return groups.eq(index).find('a').eq(0); + }; + var findGroupBody = function (index) { + return groups.eq(index).find('.accordion-body').eq(0); + }; + + + beforeEach(inject(function(_$rootScope_, _$compile_) { + scope = _$rootScope_; + $compile = _$compile_; + })); + + afterEach(function () { + element = groups = scope = $compile = undefined; + }); + + describe('with static groups', function () { + beforeEach(function () { + var tpl = + "" + + "Content 1" + + "Content 2" + + ""; + element = angular.element(tpl); + angular.element(document.body).append(element); + $compile(element)(scope); + scope.$digest(); + groups = element.find('.accordion-group'); + }); + afterEach(function() { + element.remove(); + }); + + it('should create accordion groups with content', function () { + expect(groups.length).toEqual(2); + expect(findGroupLink(0).text()).toEqual('title 1'); + expect(findGroupBody(0).text().trim()).toEqual('Content 1'); + expect(findGroupLink(1).text()).toEqual('title 2'); + expect(findGroupBody(1).text().trim()).toEqual('Content 2'); + }); + + it('should change selected element on click', function () { + findGroupLink(0).click(); + scope.$digest(); + expect(findGroupBody(0).scope().isOpen).toBe(true); + + findGroupLink(1).click(); + scope.$digest(); + expect(findGroupBody(0).scope().isOpen).toBe(false); + expect(findGroupBody(1).scope().isOpen).toBe(true); + }); + + it('should toggle element on click', function() { + findGroupLink(0).click(); + scope.$digest(); + expect(findGroupBody(0).scope().isOpen).toBe(true); + findGroupLink(0).click(); + scope.$digest(); + expect(findGroupBody(0).scope().isOpen).toBe(false); + }); + }); + + describe('with dynamic groups', function () { + var model; + beforeEach(function () { + var tpl = + "" + + "{{group.content}}" + + ""; + element = angular.element(tpl); + model = [ + {name: 'title 1', content: 'Content 1'}, + {name: 'title 2', content: 'Content 2'} + ]; + + $compile(element)(scope); + scope.$digest(); + }); + + it('should have no groups initially', function () { + groups = element.find('.accordion-group'); + expect(groups.length).toEqual(0); + }); + + it('should have a group for each model item', function() { + scope.groups = model; + scope.$digest(); + groups = element.find('.accordion-group'); + expect(groups.length).toEqual(2); + expect(findGroupLink(0).text()).toEqual('title 1'); + expect(findGroupBody(0).text().trim()).toEqual('Content 1'); + expect(findGroupLink(1).text()).toEqual('title 2'); + expect(findGroupBody(1).text().trim()).toEqual('Content 2'); + }); + + it('should react properly on removing items from the model', function () { + scope.groups = model; + scope.$digest(); + groups = element.find('.accordion-group'); + expect(groups.length).toEqual(2); + + scope.groups.splice(0,1); + scope.$digest(); + groups = element.find('.accordion-group'); + expect(groups.length).toEqual(1); + }); + }); + + describe('is-open attribute', function() { + beforeEach(function () { + var tpl = + "" + + "Content 1" + + "Content 2" + + ""; + element = angular.element(tpl); + scope.open1 = false; + scope.open2 = true; + $compile(element)(scope); + scope.$digest(); + groups = element.find('.accordion-group'); + }); + + it('should open the group with isOpen set to true', function () { + expect(findGroupBody(0).scope().isOpen).toBe(false); + expect(findGroupBody(1).scope().isOpen).toBe(true); + }); + }); + + describe('is-open attribute with dynamic content', function() { + beforeEach(function () { + var tpl = + "" + + "
{{item}}
" + + "Static content" + + "
"; + element = angular.element(tpl); + scope.items = ['Item 1', 'Item 2', 'Item 3']; + scope.open1 = true; + scope.open2 = false; + angular.element(document.body).append(element); + $compile(element)(scope); + scope.$digest(); + groups = element.find('.accordion-group'); + }); + + afterEach(function() { + element.remove(); + }); + + it('should have visible group body when the group with isOpen set to true', function () { + expect(findGroupBody(0)[0].clientHeight).not.toBe(0); + expect(findGroupBody(1)[0].clientHeight).toBe(0); + }); + }); + + describe('accordion-heading element', function() { + beforeEach(function() { + var tpl = + '' + + '' + + 'Heading Element {{x}} ' + + 'Body' + + '' + + ''; + element = $compile(tpl)(scope); + scope.$digest(); + groups = element.find('.accordion-group'); + }); + it('transcludes the content into the heading link', function() { + expect(findGroupLink(0).text()).toBe('Heading Element 123 '); + }); + it('attaches the same scope to the transcluded heading and body', function() { + expect(findGroupLink(0).find('span').scope().$id).toBe(findGroupBody(0).find('span').scope().$id); + }); + + }); + + describe('accordion-heading, with repeating accordion-groups', function() { + it('should clone the accordion-heading for each group', function() { + element = $compile('{{x}}')(scope); + scope.$digest(); + groups = element.find('.accordion-group'); + expect(groups.length).toBe(3); + expect(findGroupLink(0).text()).toBe('1'); + expect(findGroupLink(1).text()).toBe('2'); + expect(findGroupLink(2).text()).toBe('3'); + }); + }); + }); +}); diff --git a/src/buttons/buttons.js b/src/buttons/buttons.js index 426b2996cd..8a3eef898f 100644 --- a/src/buttons/buttons.js +++ b/src/buttons/buttons.js @@ -1,76 +1,76 @@ -angular.module('ui.bootstrap.buttons', []) - - .constant('buttonConfig', { - activeClass:'active', - toggleEvent:'click' - }) - - .directive('btnRadio', ['buttonConfig', function (buttonConfig) { - var activeClass = buttonConfig.activeClass || 'active'; - var toggleEvent = buttonConfig.toggleEvent || 'click'; - - return { - - require:'ngModel', - link:function (scope, element, attrs, ngModelCtrl) { - - var value = scope.$eval(attrs.btnRadio); - - //model -> UI - scope.$watch(function () { - return ngModelCtrl.$modelValue; - }, function (modelValue) { - if (angular.equals(modelValue, value)){ - element.addClass(activeClass); - } else { - element.removeClass(activeClass); - } - }); - - //ui->model - element.bind(toggleEvent, function () { - if (!element.hasClass(activeClass)) { - scope.$apply(function () { - ngModelCtrl.$setViewValue(value); - }); - } - }); - } - }; -}]) - - .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { - - var activeClass = buttonConfig.activeClass || 'active'; - var toggleEvent = buttonConfig.toggleEvent || 'click'; - - return { - require:'ngModel', - link:function (scope, element, attrs, ngModelCtrl) { - - var trueValue = scope.$eval(attrs.btnCheckboxTrue); - var falseValue = scope.$eval(attrs.btnCheckboxFalse); - - trueValue = angular.isDefined(trueValue) ? trueValue : true; - falseValue = angular.isDefined(falseValue) ? falseValue : false; - - //model -> UI - scope.$watch(function () { - return ngModelCtrl.$modelValue; - }, function (modelValue) { - if (angular.equals(modelValue, trueValue)) { - element.addClass(activeClass); - } else { - element.removeClass(activeClass); - } - }); - - //ui->model - element.bind(toggleEvent, function () { - scope.$apply(function () { - ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); - }); - }); - } - }; +angular.module('ui.bootstrap.buttons', []) + + .constant('buttonConfig', { + activeClass:'active', + toggleEvent:'click' + }) + + .directive('btnRadio', ['buttonConfig', function (buttonConfig) { + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var value = scope.$eval(attrs.btnRadio); + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, value)){ + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + if (!element.hasClass(activeClass)) { + scope.$apply(function () { + ngModelCtrl.$setViewValue(value); + }); + } + }); + } + }; +}]) + + .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) { + + var activeClass = buttonConfig.activeClass || 'active'; + var toggleEvent = buttonConfig.toggleEvent || 'click'; + + return { + require:'ngModel', + link:function (scope, element, attrs, ngModelCtrl) { + + var trueValue = scope.$eval(attrs.btnCheckboxTrue); + var falseValue = scope.$eval(attrs.btnCheckboxFalse); + + trueValue = angular.isDefined(trueValue) ? trueValue : true; + falseValue = angular.isDefined(falseValue) ? falseValue : false; + + //model -> UI + scope.$watch(function () { + return ngModelCtrl.$modelValue; + }, function (modelValue) { + if (angular.equals(modelValue, trueValue)) { + element.addClass(activeClass); + } else { + element.removeClass(activeClass); + } + }); + + //ui->model + element.bind(toggleEvent, function () { + scope.$apply(function () { + ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue); + }); + }); + } + }; }]); \ No newline at end of file diff --git a/src/buttons/docs/demo.html b/src/buttons/docs/demo.html index bd3d455e0c..2093446d04 100644 --- a/src/buttons/docs/demo.html +++ b/src/buttons/docs/demo.html @@ -1,21 +1,21 @@ -
-

Single toggle

-
{{singleModel}}
- -

Checkbox

-
{{checkModel}}
-
- - - -
-

Radio

-
{{radioModel}}
-
- - - -
+
+

Single toggle

+
{{singleModel}}
+ +

Checkbox

+
{{checkModel}}
+
+ + + +
+

Radio

+
{{radioModel}}
+
+ + + +
\ No newline at end of file diff --git a/src/buttons/docs/demo.js b/src/buttons/docs/demo.js index 71bc1880ee..b04546a810 100644 --- a/src/buttons/docs/demo.js +++ b/src/buttons/docs/demo.js @@ -1,12 +1,12 @@ -var ButtonsCtrl = function ($scope) { - - $scope.singleModel = 1; - - $scope.radioModel = 'Middle'; - - $scope.checkModel = { - left: false, - middle: true, - right: false - }; +var ButtonsCtrl = function ($scope) { + + $scope.singleModel = 1; + + $scope.radioModel = 'Middle'; + + $scope.checkModel = { + left: false, + middle: true, + right: false + }; }; \ No newline at end of file diff --git a/src/buttons/docs/readme.md b/src/buttons/docs/readme.md index 8bf3c23e32..73ef6003d1 100644 --- a/src/buttons/docs/readme.md +++ b/src/buttons/docs/readme.md @@ -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. + diff --git a/src/buttons/test/buttons.spec.js b/src/buttons/test/buttons.spec.js index 21748457f9..6b2cdd899e 100644 --- a/src/buttons/test/buttons.spec.js +++ b/src/buttons/test/buttons.spec.js @@ -1,94 +1,94 @@ -describe('buttons', function () { - - var $scope, $compile; - - beforeEach(module('ui.bootstrap.buttons')); - beforeEach(inject(function (_$rootScope_, _$compile_) { - $scope = _$rootScope_; - $compile = _$compile_; - })); - - describe('checkbox', function () { - - var compileButton = function (markup, scope) { - var el = $compile(markup)(scope); - scope.$digest(); - return el; - }; - - //model -> UI - it('should work correctly with default model values', function () { - $scope.model = false; - var btn = compileButton('', $scope); - expect(btn).not.toHaveClass('active'); - - $scope.model = true; - $scope.$digest(); - expect(btn).toHaveClass('active'); - }); - - it('should bind custom model values', function () { - $scope.model = 1; - var btn = compileButton('', $scope); - expect(btn).toHaveClass('active'); - - $scope.model = 0; - $scope.$digest(); - expect(btn).not.toHaveClass('active'); - }); - - //UI-> model - it('should toggle default model values on click', function () { - $scope.model = false; - var btn = compileButton('', $scope); - - btn.click(); - expect($scope.model).toEqual(true); - btn.click(); - expect($scope.model).toEqual(false); - }); - - it('should toggle custom model values on click', function () { - $scope.model = 0; - var btn = compileButton('', $scope); - - btn.click(); - expect($scope.model).toEqual(1); - btn.click(); - expect($scope.model).toEqual(0); - }); - }); - - describe('radio', function () { - - var compileButtons = function (markup, scope) { - var el = $compile('
'+markup+'
')(scope); - scope.$digest(); - return el.find('button'); - }; - - //model -> UI - it('should work correctly set active class based on model', function () { - var btns = compileButtons('', $scope); - expect(btns.eq(0)).not.toHaveClass('active'); - expect(btns.eq(1)).not.toHaveClass('active'); - - $scope.model = 2; - $scope.$digest(); - expect(btns.eq(0)).not.toHaveClass('active'); - expect(btns.eq(1)).toHaveClass('active'); - }); - - //UI->model - it('should work correctly set active class based on model', function () { - var btns = compileButtons('', $scope); - expect($scope.model).toBeUndefined(); - - btns.eq(0).click(); - expect($scope.model).toEqual(1); - - btns.eq(1).click(); - expect($scope.model).toEqual(2); - }); - }); +describe('buttons', function () { + + var $scope, $compile; + + beforeEach(module('ui.bootstrap.buttons')); + beforeEach(inject(function (_$rootScope_, _$compile_) { + $scope = _$rootScope_; + $compile = _$compile_; + })); + + describe('checkbox', function () { + + var compileButton = function (markup, scope) { + var el = $compile(markup)(scope); + scope.$digest(); + return el; + }; + + //model -> UI + it('should work correctly with default model values', function () { + $scope.model = false; + var btn = compileButton('', $scope); + expect(btn).not.toHaveClass('active'); + + $scope.model = true; + $scope.$digest(); + expect(btn).toHaveClass('active'); + }); + + it('should bind custom model values', function () { + $scope.model = 1; + var btn = compileButton('', $scope); + expect(btn).toHaveClass('active'); + + $scope.model = 0; + $scope.$digest(); + expect(btn).not.toHaveClass('active'); + }); + + //UI-> model + it('should toggle default model values on click', function () { + $scope.model = false; + var btn = compileButton('', $scope); + + btn.click(); + expect($scope.model).toEqual(true); + btn.click(); + expect($scope.model).toEqual(false); + }); + + it('should toggle custom model values on click', function () { + $scope.model = 0; + var btn = compileButton('', $scope); + + btn.click(); + expect($scope.model).toEqual(1); + btn.click(); + expect($scope.model).toEqual(0); + }); + }); + + describe('radio', function () { + + var compileButtons = function (markup, scope) { + var el = $compile('
'+markup+'
')(scope); + scope.$digest(); + return el.find('button'); + }; + + //model -> UI + it('should work correctly set active class based on model', function () { + var btns = compileButtons('', $scope); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).not.toHaveClass('active'); + + $scope.model = 2; + $scope.$digest(); + expect(btns.eq(0)).not.toHaveClass('active'); + expect(btns.eq(1)).toHaveClass('active'); + }); + + //UI->model + it('should work correctly set active class based on model', function () { + var btns = compileButtons('', $scope); + expect($scope.model).toBeUndefined(); + + btns.eq(0).click(); + expect($scope.model).toEqual(1); + + btns.eq(1).click(); + expect($scope.model).toEqual(2); + }); + }); }); \ No newline at end of file diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html index 064e7339fe..524bf8bf95 100644 --- a/src/modal/docs/demo.html +++ b/src/modal/docs/demo.html @@ -1,16 +1,16 @@ -
- -
- - - -
+
+ +
+ + + +
\ No newline at end of file diff --git a/src/pagination/docs/demo.html b/src/pagination/docs/demo.html index bb7e608676..92e1dfc07c 100644 --- a/src/pagination/docs/demo.html +++ b/src/pagination/docs/demo.html @@ -1,9 +1,9 @@ -
- - - - - - - The selected page no: {{currentPage}} +
+ + + + + + + The selected page no: {{currentPage}}
\ No newline at end of file diff --git a/src/pagination/docs/demo.js b/src/pagination/docs/demo.js index 4dc192121f..e80756526f 100644 --- a/src/pagination/docs/demo.js +++ b/src/pagination/docs/demo.js @@ -1,9 +1,9 @@ -var PaginationDemoCtrl = function ($scope) { - $scope.noOfPages = 7; - $scope.currentPage = 4; - $scope.maxSize = 5; - - $scope.setPage = function (pageNo) { - $scope.currentPage = pageNo; - }; -}; +var PaginationDemoCtrl = function ($scope) { + $scope.noOfPages = 7; + $scope.currentPage = 4; + $scope.maxSize = 5; + + $scope.setPage = function (pageNo) { + $scope.currentPage = pageNo; + }; +}; diff --git a/src/pagination/docs/readme.md b/src/pagination/docs/readme.md index b40617116f..b4fab8b7f0 100644 --- a/src/pagination/docs/readme.md +++ b/src/pagination/docs/readme.md @@ -1,5 +1,5 @@ -A lightweight pagination directive that is focused on ... providing pagination! - -It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next and First / Last buttons (if exist) is maintained correctly. - +A lightweight pagination directive that is focused on ... providing pagination! + +It will take care of visualising a pagination bar. Additionally it will make sure that the state (enabled / disabled) of the Previous / Next and First / Last buttons (if exist) is maintained correctly. + It also provides optional attribute max-size to limit the size of pagination bar. \ No newline at end of file diff --git a/src/position/position.js b/src/position/position.js index c62c12b210..d5200a4076 100644 --- a/src/position/position.js +++ b/src/position/position.js @@ -1,79 +1,79 @@ -angular.module('ui.bootstrap.position', []) - -/** - * A set of utility methods that can be use to retrieve position of DOM elements. - * It is meant to be used where we need to absolute-position DOM elements in - * relation to other, existing elements (this is the case for tooltips, popovers, - * typeahead suggestions etc.). - */ - .factory('$position', ['$document', '$window', function ($document, $window) { - - function getStyle(el, cssprop) { - if (el.currentStyle) { //IE - return el.currentStyle[cssprop]; - } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; - } - // finally try and get inline style - return el.style[cssprop]; - } - - /** - * Checks if a given element is statically positioned - * @param element - raw DOM element - */ - function isStaticPositioned(element) { - return (getStyle(element, "position") || 'static' ) === 'static'; - } - - /** - * returns the closest, non-statically positioned parentOffset of a given element - * @param element - */ - var parentOffsetEl = function (element) { - var docDomEl = $document[0]; - var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docDomEl; - }; - - return { - /** - * Provides read-only equivalent of jQuery's position function: - * http://api.jquery.com/position/ - */ - position: function (element) { - var elBCR = this.offset(element); - var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); - if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); - offsetParentBCR.top += offsetParentEl.clientTop; - offsetParentBCR.left += offsetParentEl.clientLeft; - } - - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: elBCR.top - offsetParentBCR.top, - left: elBCR.left - offsetParentBCR.left - }; - }, - - /** - * Provides read-only equivalent of jQuery's offset function: - * http://api.jquery.com/offset/ - */ - offset: function (element) { - var boundingClientRect = element[0].getBoundingClientRect(); - return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), - top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) - }; - } - }; - }]); +angular.module('ui.bootstrap.position', []) + +/** + * A set of utility methods that can be use to retrieve position of DOM elements. + * It is meant to be used where we need to absolute-position DOM elements in + * relation to other, existing elements (this is the case for tooltips, popovers, + * typeahead suggestions etc.). + */ + .factory('$position', ['$document', '$window', function ($document, $window) { + + function getStyle(el, cssprop) { + if (el.currentStyle) { //IE + return el.currentStyle[cssprop]; + } else if ($window.getComputedStyle) { + return $window.getComputedStyle(el)[cssprop]; + } + // finally try and get inline style + return el.style[cssprop]; + } + + /** + * Checks if a given element is statically positioned + * @param element - raw DOM element + */ + function isStaticPositioned(element) { + return (getStyle(element, "position") || 'static' ) === 'static'; + } + + /** + * returns the closest, non-statically positioned parentOffset of a given element + * @param element + */ + var parentOffsetEl = function (element) { + var docDomEl = $document[0]; + var offsetParent = element.offsetParent || docDomEl; + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + offsetParent = offsetParent.offsetParent; + } + return offsetParent || docDomEl; + }; + + return { + /** + * Provides read-only equivalent of jQuery's position function: + * http://api.jquery.com/position/ + */ + position: function (element) { + var elBCR = this.offset(element); + var offsetParentBCR = { top: 0, left: 0 }; + var offsetParentEl = parentOffsetEl(element[0]); + if (offsetParentEl != $document[0]) { + offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR.top += offsetParentEl.clientTop; + offsetParentBCR.left += offsetParentEl.clientLeft; + } + + return { + width: element.prop('offsetWidth'), + height: element.prop('offsetHeight'), + top: elBCR.top - offsetParentBCR.top, + left: elBCR.left - offsetParentBCR.left + }; + }, + + /** + * Provides read-only equivalent of jQuery's offset function: + * http://api.jquery.com/offset/ + */ + offset: function (element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: element.prop('offsetWidth'), + height: element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft) + }; + } + }; + }]); diff --git a/src/position/test/test.html b/src/position/test/test.html index 4bdaca490e..5076e9b30c 100644 --- a/src/position/test/test.html +++ b/src/position/test/test.html @@ -1,118 +1,118 @@ - - - - - - - - - - -

Within body

- -
Content
- -

Within statically positioned DIV

- -
-
Content
-
- -

Within relative-positioned DIV - position specified in CSS

- -
-
Content
-
- -

Within relative-positioned DIV

- -
-
Content
-
- -

Within absolute-positioned DIV

- -
-
Content
-
- -

Next to a float element

- -
-
Content
-
- -

Within a table

- - - - - -
Some other content -
Content
-
- -

Within a table that is inside a relative-positioned DIV

- -
- - - - - -
Some other content -
Content
-
-
- -

Inside looong text

-

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.

-

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.

-

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.

-

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.

-

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.

-
HERE
-

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.

- -
-

Within fixed div

-
Content
-
- - - + + + + + + + + + + +

Within body

+ +
Content
+ +

Within statically positioned DIV

+ +
+
Content
+
+ +

Within relative-positioned DIV - position specified in CSS

+ +
+
Content
+
+ +

Within relative-positioned DIV

+ +
+
Content
+
+ +

Within absolute-positioned DIV

+ +
+
Content
+
+ +

Next to a float element

+ +
+
Content
+
+ +

Within a table

+ + + + + +
Some other content +
Content
+
+ +

Within a table that is inside a relative-positioned DIV

+ +
+ + + + + +
Some other content +
Content
+
+
+ +

Inside looong text

+

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.

+

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.

+

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.

+

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.

+

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.

+
HERE
+

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.

+ +
+

Within fixed div

+
Content
+
+ + + diff --git a/src/tabs/docs/demo.html b/src/tabs/docs/demo.html index ee8a435ee9..a2ea432981 100644 --- a/src/tabs/docs/demo.html +++ b/src/tabs/docs/demo.html @@ -1,23 +1,23 @@ -
- Select a tab by setting active binding to true: -
- - -

- - Static content - - {{tab.content}} - - - - Select me for alert! - - I've got an HTML heading, and a select callback. Pretty cool! - - -
+
+ Select a tab by setting active binding to true: +
+ + +

+ + Static content + + {{tab.content}} + + + + Select me for alert! + + I've got an HTML heading, and a select callback. Pretty cool! + + +
diff --git a/src/tabs/docs/demo.js b/src/tabs/docs/demo.js index 509b746aa0..e68cd8e2dc 100644 --- a/src/tabs/docs/demo.js +++ b/src/tabs/docs/demo.js @@ -1,12 +1,12 @@ -var TabsDemoCtrl = function ($scope) { - $scope.tabs = [ - { title:"Dynamic Title 1", content:"Dynamic content 1" }, - { title:"Dynamic Title 2", content:"Dynamic content 2" } - ]; - - $scope.alertMe = function() { - setTimeout(function() { - alert("You've selected the alert tab!"); - }); - }; -}; +var TabsDemoCtrl = function ($scope) { + $scope.tabs = [ + { title:"Dynamic Title 1", content:"Dynamic content 1" }, + { title:"Dynamic Title 2", content:"Dynamic content 2" } + ]; + + $scope.alertMe = function() { + setTimeout(function() { + alert("You've selected the alert tab!"); + }); + }; +}; diff --git a/src/typeahead/docs/demo.html b/src/typeahead/docs/demo.html index d14f6a46e4..1e698af354 100644 --- a/src/typeahead/docs/demo.html +++ b/src/typeahead/docs/demo.html @@ -1,4 +1,4 @@ -
-
Model: {{selected| json}}
- +
+
Model: {{selected| json}}
+
\ No newline at end of file diff --git a/src/typeahead/docs/demo.js b/src/typeahead/docs/demo.js index 370655c956..db898236ce 100644 --- a/src/typeahead/docs/demo.js +++ b/src/typeahead/docs/demo.js @@ -1,5 +1,5 @@ -function TypeaheadCtrl($scope) { - - $scope.selected = undefined; - $scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; +function TypeaheadCtrl($scope) { + + $scope.selected = undefined; + $scope.states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Dakota', 'North Carolina', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; } \ No newline at end of file diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md index efb8a1f930..e7918e3378 100644 --- a/src/typeahead/docs/readme.md +++ b/src/typeahead/docs/readme.md @@ -1,8 +1,8 @@ -Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead) - -This directive can be used to quickly create elegant typeheads with any form text input. - -It is very well integrated into the AngularJS as: - -* it uses the same, flexible syntax as the [select directive](http://docs.angularjs.org/api/ng.directive:select) +Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead) + +This directive can be used to quickly create elegant typeheads with any form text input. + +It is very well integrated into the AngularJS as: + +* it uses the same, flexible syntax as the [select directive](http://docs.angularjs.org/api/ng.directive:select) * works with promises and it means that you can retrieve matches using the `$http` service with minimal effort \ No newline at end of file diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 746a5a2854..b69e4d5c9d 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -1,403 +1,403 @@ -describe('typeahead tests', function () { - - beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(module('template/typeahead/typeahead.html')); - - describe('syntax parser', function () { - - var typeaheadParser, scope, filterFilter; - beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) { - typeaheadParser = _typeaheadParser_; - scope = _$rootScope_; - filterFilter = _filterFilter_; - })); - - it('should parse the simplest array-based syntax', function () { - scope.states = ['Alabama', 'California', 'Delaware']; - var result = typeaheadParser.parse('state for state in states | filter:$viewValue'); - - var itemName = result.itemName; - var locals = {$viewValue:'al'}; - expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); - - locals[itemName] = 'Alabama'; - expect(result.viewMapper(scope, locals)).toEqual('Alabama'); - expect(result.modelMapper(scope, locals)).toEqual('Alabama'); - }); - - it('should parse the simplest function-based syntax', function () { - scope.getStates = function ($viewValue) { - return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue); - }; - var result = typeaheadParser.parse('state for state in getStates($viewValue)'); - - var itemName = result.itemName; - var locals = {$viewValue:'al'}; - expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); - - locals[itemName] = 'Alabama'; - expect(result.viewMapper(scope, locals)).toEqual('Alabama'); - expect(result.modelMapper(scope, locals)).toEqual('Alabama'); - }); - - it('should allow to specify custom model mapping that is used as a label as well', function () { - - scope.states = [ - {code:'AL', name:'Alabama'}, - {code:'CA', name:'California'}, - {code:'DE', name:'Delaware'} - ]; - var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true"); - - var itemName = result.itemName; - expect(itemName).toEqual('state'); - expect(result.source(scope, {$viewValue:'al'})).toEqual([ - {code:'CA', name:'California'}, - {code:'AL', name:'Alabama'} - ]); - - var locals = {$viewValue:'al'}; - locals[itemName] = {code:'AL', name:'Alabama'}; - expect(result.viewMapper(scope, locals)).toEqual('Alabama'); - expect(result.modelMapper(scope, locals)).toEqual('Alabama'); - }); - - it('should allow to specify custom view and model mappers', function () { - - scope.states = [ - {code:'AL', name:'Alabama'}, - {code:'CA', name:'California'}, - {code:'DE', name:'Delaware'} - ]; - var result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true"); - - var itemName = result.itemName; - expect(result.source(scope, {$viewValue:'al'})).toEqual([ - {code:'CA', name:'California'}, - {code:'AL', name:'Alabama'} - ]); - - var locals = {$viewValue:'al'}; - locals[itemName] = {code:'AL', name:'Alabama'}; - expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)'); - expect(result.modelMapper(scope, locals)).toEqual('AL'); - }); - }); - - describe('typeaheadPopup - result rendering', function () { - - var scope, $rootScope, $compile; - beforeEach(inject(function (_$rootScope_, _$compile_) { - $rootScope = _$rootScope_; - scope = $rootScope.$new(); - $compile = _$compile_; - })); - - it('should render initial results', function () { - - scope.matches = ['foo', 'bar', 'baz']; - scope.active = 1; - - var el = $compile("
")(scope); - $rootScope.$digest(); - - var liElems = el.find('li'); - expect(liElems.length).toEqual(3); - expect(liElems.eq(0)).not.toHaveClass('active'); - expect(liElems.eq(1)).toHaveClass('active'); - expect(liElems.eq(2)).not.toHaveClass('active'); - }); - - it('should change active item on mouseenter', function () { - - scope.matches = ['foo', 'bar', 'baz']; - scope.active = 1; - - var el = $compile("
")(scope); - $rootScope.$digest(); - - var liElems = el.find('li'); - expect(liElems.eq(1)).toHaveClass('active'); - expect(liElems.eq(2)).not.toHaveClass('active'); - - liElems.eq(2).trigger('mouseenter'); - - expect(liElems.eq(1)).not.toHaveClass('active'); - expect(liElems.eq(2)).toHaveClass('active'); - }); - - it('should select an item on mouse click', function () { - - scope.matches = ['foo', 'bar', 'baz']; - scope.active = 1; - $rootScope.select = angular.noop; - spyOn($rootScope, 'select'); - - var el = $compile("
")(scope); - $rootScope.$digest(); - - var liElems = el.find('li'); - liElems.eq(2).find('a').trigger('click'); - expect($rootScope.select).toHaveBeenCalledWith(2); - }); - }); - - describe('typeaheadHighlight', function () { - - var highlightFilter; - beforeEach(inject(function (typeaheadHighlightFilter) { - highlightFilter = typeaheadHighlightFilter; - })); - - it('should higlight a match', function () { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); - }); - - it('should higlight a match with mixed case', function () { - expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); - }); - - it('should higlight all matches', function () { - expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); - }); - - it('should do nothing if no match', function () { - expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); - }); - - it('issue 316 - should work correctly for regexp reserved words', function () { - expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); - }); - }); - - describe('typeahead', function () { - - var $scope, $compile, $document; - var changeInputValueTo; - - beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) { - $scope = _$rootScope_; - $scope.source = ['foo', 'bar', 'baz']; - $compile = _$compile_; - $document = _$document_; - changeInputValueTo = function (element, value) { - var inputEl = findInput(element); - inputEl.val(value); - inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); - $scope.$digest(); - }; - })); - - //utility functions - var prepareInputEl = function(inputTpl) { - var el = $compile(angular.element(inputTpl))($scope); - $scope.$digest(); - return el; - }; - - var findInput = function(element) { - return element.find('input'); - }; - - var findDropDown = function(element) { - return element.find('ul.typeahead'); - }; - - var findMatches = function(element) { - return findDropDown(element).find('li'); - }; - - var triggerKeyDown = function(element, keyCode) { - var inputEl = findInput(element); - var e = $.Event("keydown"); - e.which = keyCode; - inputEl.trigger(e); - }; - - //custom matchers - beforeEach(function () { - this.addMatchers({ - toBeClosed: function() { - var typeaheadEl = findDropDown(this.actual); - this.message = function() { - return "Expected '" + angular.mock.dump(this.actual) + "' to be closed."; - }; - return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0; - - }, toBeOpenWithActive: function(noOfMatches, activeIdx) { - - var typeaheadEl = findDropDown(this.actual); - var liEls = findMatches(this.actual); - - this.message = function() { - return "Expected '" + angular.mock.dump(this.actual) + "' to be opened."; - }; - return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active'); - } - }); - }); - - //coarse grained, "integration" tests - describe('initial state and model changes', function () { - - it('should be closed by default', function () { - var element = prepareInputEl("
"); - expect(element).toBeClosed(); - }); - - it('should correctly render initial state if the "as" keyword is used', function () { - - $scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}]; - $scope.result = $scope.states[0]; - - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - expect(inputEl.val()).toEqual('Alaska'); - }); - - it('should not get open on model change', function () { - var element = prepareInputEl("
"); - $scope.$apply(function(){ - $scope.result = 'foo'; - }); - expect(element).toBeClosed(); - }); - }); - - describe('basic functionality', function () { - - it('should open and close typeahead based on matches', function () { - var element = prepareInputEl("
"); - changeInputValueTo(element, 'ba'); - expect(element).toBeOpenWithActive(2, 0); - }); - - it('should not open typeahead if input value smaller than a defined threshold', function () { - var element = prepareInputEl("
"); - changeInputValueTo(element, 'b'); - expect(element).toBeClosed(); - }); - - it('should support custom model selecting function', function () { - $scope.updaterFn = function(selectedItem) { - return 'prefix' + selectedItem; - }; - var element = prepareInputEl("
"); - changeInputValueTo(element, 'f'); - triggerKeyDown(element, 13); - expect($scope.result).toEqual('prefixfoo'); - }); - - it('should support custom label rendering function', function () { - $scope.formatterFn = function(sourceItem) { - return 'prefix' + sourceItem; - }; - - var element = prepareInputEl("
"); - changeInputValueTo(element, 'fo'); - var matchHighlight = findMatches(element).find('a').html(); - expect(matchHighlight).toEqual('prefixfoo'); - }); - - it('should by default bind view value to model even if not part of matches', function () { - var element = prepareInputEl("
"); - changeInputValueTo(element, 'not in matches'); - expect($scope.result).toEqual('not in matches'); - }); - - it('should support the editable property to limit model bindings to matches only', function () { - var element = prepareInputEl("
"); - changeInputValueTo(element, 'not in matches'); - expect($scope.result).toEqual(undefined); - }); - - it('should bind loading indicator expression', inject(function ($timeout) { - - $scope.isLoading = false; - $scope.loadMatches = function(viewValue) { - return $timeout(function() { return [];}, 1000); - }; - - var element = prepareInputEl("
"); - changeInputValueTo(element, 'foo'); - - expect($scope.isLoading).toBeTruthy(); - $timeout.flush(); - expect($scope.isLoading).toBeFalsy(); - })); - }); - - describe('selecting a match', function () { - - it('should select a match on enter', function () { - - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - changeInputValueTo(element, 'b'); - triggerKeyDown(element, 13); - - expect($scope.result).toEqual('bar'); - expect(inputEl.val()).toEqual('bar'); - }); - - it('should select a match on tab', function () { - - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - changeInputValueTo(element, 'b'); - triggerKeyDown(element, 9); - - expect($scope.result).toEqual('bar'); - expect(inputEl.val()).toEqual('bar'); - }); - - it('should select match on click', function () { - - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - changeInputValueTo(element, 'b'); - var match = $(findMatches(element)[1]).find('a')[0]; - - $(match).click(); - $scope.$digest(); - - expect($scope.result).toEqual('baz'); - expect(inputEl.val()).toEqual('baz'); - }); - - it('should correctly update inputs value on mapping where label is not derived from the model', function () { - - $scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}]; - - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - changeInputValueTo(element, 'Alas'); - triggerKeyDown(element, 13); - - expect($scope.result).toEqual('AL'); - expect(inputEl.val()).toEqual('Alaska'); - }); - }); - - describe('regressions tests', function () { - - it('issue 231 - closes matches popup on click outside typeahead', function () { - var element = prepareInputEl("
"); - var inputEl = findInput(element); - - changeInputValueTo(element, 'b'); - - $document.find('body').click(); - $scope.$digest(); - - expect(element).toBeClosed(); - }); - }); - - }); +describe('typeahead tests', function () { + + beforeEach(module('ui.bootstrap.typeahead')); + beforeEach(module('template/typeahead/typeahead.html')); + + describe('syntax parser', function () { + + var typeaheadParser, scope, filterFilter; + beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) { + typeaheadParser = _typeaheadParser_; + scope = _$rootScope_; + filterFilter = _filterFilter_; + })); + + it('should parse the simplest array-based syntax', function () { + scope.states = ['Alabama', 'California', 'Delaware']; + var result = typeaheadParser.parse('state for state in states | filter:$viewValue'); + + var itemName = result.itemName; + var locals = {$viewValue:'al'}; + expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); + + locals[itemName] = 'Alabama'; + expect(result.viewMapper(scope, locals)).toEqual('Alabama'); + expect(result.modelMapper(scope, locals)).toEqual('Alabama'); + }); + + it('should parse the simplest function-based syntax', function () { + scope.getStates = function ($viewValue) { + return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue); + }; + var result = typeaheadParser.parse('state for state in getStates($viewValue)'); + + var itemName = result.itemName; + var locals = {$viewValue:'al'}; + expect(result.source(scope, locals)).toEqual(['Alabama', 'California']); + + locals[itemName] = 'Alabama'; + expect(result.viewMapper(scope, locals)).toEqual('Alabama'); + expect(result.modelMapper(scope, locals)).toEqual('Alabama'); + }); + + it('should allow to specify custom model mapping that is used as a label as well', function () { + + scope.states = [ + {code:'AL', name:'Alabama'}, + {code:'CA', name:'California'}, + {code:'DE', name:'Delaware'} + ]; + var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true"); + + var itemName = result.itemName; + expect(itemName).toEqual('state'); + expect(result.source(scope, {$viewValue:'al'})).toEqual([ + {code:'CA', name:'California'}, + {code:'AL', name:'Alabama'} + ]); + + var locals = {$viewValue:'al'}; + locals[itemName] = {code:'AL', name:'Alabama'}; + expect(result.viewMapper(scope, locals)).toEqual('Alabama'); + expect(result.modelMapper(scope, locals)).toEqual('Alabama'); + }); + + it('should allow to specify custom view and model mappers', function () { + + scope.states = [ + {code:'AL', name:'Alabama'}, + {code:'CA', name:'California'}, + {code:'DE', name:'Delaware'} + ]; + var result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true"); + + var itemName = result.itemName; + expect(result.source(scope, {$viewValue:'al'})).toEqual([ + {code:'CA', name:'California'}, + {code:'AL', name:'Alabama'} + ]); + + var locals = {$viewValue:'al'}; + locals[itemName] = {code:'AL', name:'Alabama'}; + expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)'); + expect(result.modelMapper(scope, locals)).toEqual('AL'); + }); + }); + + describe('typeaheadPopup - result rendering', function () { + + var scope, $rootScope, $compile; + beforeEach(inject(function (_$rootScope_, _$compile_) { + $rootScope = _$rootScope_; + scope = $rootScope.$new(); + $compile = _$compile_; + })); + + it('should render initial results', function () { + + scope.matches = ['foo', 'bar', 'baz']; + scope.active = 1; + + var el = $compile("
")(scope); + $rootScope.$digest(); + + var liElems = el.find('li'); + expect(liElems.length).toEqual(3); + expect(liElems.eq(0)).not.toHaveClass('active'); + expect(liElems.eq(1)).toHaveClass('active'); + expect(liElems.eq(2)).not.toHaveClass('active'); + }); + + it('should change active item on mouseenter', function () { + + scope.matches = ['foo', 'bar', 'baz']; + scope.active = 1; + + var el = $compile("
")(scope); + $rootScope.$digest(); + + var liElems = el.find('li'); + expect(liElems.eq(1)).toHaveClass('active'); + expect(liElems.eq(2)).not.toHaveClass('active'); + + liElems.eq(2).trigger('mouseenter'); + + expect(liElems.eq(1)).not.toHaveClass('active'); + expect(liElems.eq(2)).toHaveClass('active'); + }); + + it('should select an item on mouse click', function () { + + scope.matches = ['foo', 'bar', 'baz']; + scope.active = 1; + $rootScope.select = angular.noop; + spyOn($rootScope, 'select'); + + var el = $compile("
")(scope); + $rootScope.$digest(); + + var liElems = el.find('li'); + liElems.eq(2).find('a').trigger('click'); + expect($rootScope.select).toHaveBeenCalledWith(2); + }); + }); + + describe('typeaheadHighlight', function () { + + var highlightFilter; + beforeEach(inject(function (typeaheadHighlightFilter) { + highlightFilter = typeaheadHighlightFilter; + })); + + it('should higlight a match', function () { + expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + }); + + it('should higlight a match with mixed case', function () { + expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); + }); + + it('should higlight all matches', function () { + expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); + }); + + it('should do nothing if no match', function () { + expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); + }); + + it('issue 316 - should work correctly for regexp reserved words', function () { + expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); + }); + }); + + describe('typeahead', function () { + + var $scope, $compile, $document; + var changeInputValueTo; + + beforeEach(inject(function (_$rootScope_, _$compile_, _$document_, $sniffer) { + $scope = _$rootScope_; + $scope.source = ['foo', 'bar', 'baz']; + $compile = _$compile_; + $document = _$document_; + changeInputValueTo = function (element, value) { + var inputEl = findInput(element); + inputEl.val(value); + inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change'); + $scope.$digest(); + }; + })); + + //utility functions + var prepareInputEl = function(inputTpl) { + var el = $compile(angular.element(inputTpl))($scope); + $scope.$digest(); + return el; + }; + + var findInput = function(element) { + return element.find('input'); + }; + + var findDropDown = function(element) { + return element.find('ul.typeahead'); + }; + + var findMatches = function(element) { + return findDropDown(element).find('li'); + }; + + var triggerKeyDown = function(element, keyCode) { + var inputEl = findInput(element); + var e = $.Event("keydown"); + e.which = keyCode; + inputEl.trigger(e); + }; + + //custom matchers + beforeEach(function () { + this.addMatchers({ + toBeClosed: function() { + var typeaheadEl = findDropDown(this.actual); + this.message = function() { + return "Expected '" + angular.mock.dump(this.actual) + "' to be closed."; + }; + return typeaheadEl.css('display')==='none' && findMatches(this.actual).length === 0; + + }, toBeOpenWithActive: function(noOfMatches, activeIdx) { + + var typeaheadEl = findDropDown(this.actual); + var liEls = findMatches(this.actual); + + this.message = function() { + return "Expected '" + angular.mock.dump(this.actual) + "' to be opened."; + }; + return typeaheadEl.css('display')==='block' && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active'); + } + }); + }); + + //coarse grained, "integration" tests + describe('initial state and model changes', function () { + + it('should be closed by default', function () { + var element = prepareInputEl("
"); + expect(element).toBeClosed(); + }); + + it('should correctly render initial state if the "as" keyword is used', function () { + + $scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}]; + $scope.result = $scope.states[0]; + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + expect(inputEl.val()).toEqual('Alaska'); + }); + + it('should not get open on model change', function () { + var element = prepareInputEl("
"); + $scope.$apply(function(){ + $scope.result = 'foo'; + }); + expect(element).toBeClosed(); + }); + }); + + describe('basic functionality', function () { + + it('should open and close typeahead based on matches', function () { + var element = prepareInputEl("
"); + changeInputValueTo(element, 'ba'); + expect(element).toBeOpenWithActive(2, 0); + }); + + it('should not open typeahead if input value smaller than a defined threshold', function () { + var element = prepareInputEl("
"); + changeInputValueTo(element, 'b'); + expect(element).toBeClosed(); + }); + + it('should support custom model selecting function', function () { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl("
"); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + }); + + it('should support custom label rendering function', function () { + $scope.formatterFn = function(sourceItem) { + return 'prefix' + sourceItem; + }; + + var element = prepareInputEl("
"); + changeInputValueTo(element, 'fo'); + var matchHighlight = findMatches(element).find('a').html(); + expect(matchHighlight).toEqual('prefixfoo'); + }); + + it('should by default bind view value to model even if not part of matches', function () { + var element = prepareInputEl("
"); + changeInputValueTo(element, 'not in matches'); + expect($scope.result).toEqual('not in matches'); + }); + + it('should support the editable property to limit model bindings to matches only', function () { + var element = prepareInputEl("
"); + changeInputValueTo(element, 'not in matches'); + expect($scope.result).toEqual(undefined); + }); + + it('should bind loading indicator expression', inject(function ($timeout) { + + $scope.isLoading = false; + $scope.loadMatches = function(viewValue) { + return $timeout(function() { return [];}, 1000); + }; + + var element = prepareInputEl("
"); + changeInputValueTo(element, 'foo'); + + expect($scope.isLoading).toBeTruthy(); + $timeout.flush(); + expect($scope.isLoading).toBeFalsy(); + })); + }); + + describe('selecting a match', function () { + + it('should select a match on enter', function () { + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'b'); + triggerKeyDown(element, 13); + + expect($scope.result).toEqual('bar'); + expect(inputEl.val()).toEqual('bar'); + }); + + it('should select a match on tab', function () { + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'b'); + triggerKeyDown(element, 9); + + expect($scope.result).toEqual('bar'); + expect(inputEl.val()).toEqual('bar'); + }); + + it('should select match on click', function () { + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'b'); + var match = $(findMatches(element)[1]).find('a')[0]; + + $(match).click(); + $scope.$digest(); + + expect($scope.result).toEqual('baz'); + expect(inputEl.val()).toEqual('baz'); + }); + + it('should correctly update inputs value on mapping where label is not derived from the model', function () { + + $scope.states = [{code: 'AL', name: 'Alaska'}, {code: 'CL', name: 'California'}]; + + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'Alas'); + triggerKeyDown(element, 13); + + expect($scope.result).toEqual('AL'); + expect(inputEl.val()).toEqual('Alaska'); + }); + }); + + describe('regressions tests', function () { + + it('issue 231 - closes matches popup on click outside typeahead', function () { + var element = prepareInputEl("
"); + var inputEl = findInput(element); + + changeInputValueTo(element, 'b'); + + $document.find('body').click(); + $scope.$digest(); + + expect(element).toBeClosed(); + }); + }); + + }); }); \ No newline at end of file diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 6bb4ad3eaf..a506d4c54a 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -1,237 +1,237 @@ -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) - -/** - * A helper service that can parse typeahead's syntax (string provided by users) - * Extracted to a separate service for ease of unit testing - */ - .factory('typeaheadParser', ['$parse', function ($parse) { - - // 00000111000000000000022200000000000000003333333333333330000000000044000 - var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; - - return { - parse:function (input) { - - var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; - if (!match) { - throw new Error( - "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + - " but got '" + input + "'."); - } - - return { - itemName:match[3], - source:$parse(match[4]), - viewMapper:$parse(match[2] || match[1]), - modelMapper:$parse(match[1]) - }; - } - }; -}]) - - .directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { - - var selected; - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - //should it restrict model values to the ones selected from the popup only? - var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; - - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - - //pop-up element used to display matches - var popUpEl = angular.element( - ""+ - ""); - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - originalScope.$on('$destroy', function(){ - scope.$destroy(); - }); - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - }; - - var getMatchesAsync = function(inputValue) { - - var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(scope, locals)).then(function(matches) { - - //it might happen that several async queries were in progress if a user were typing fast - //but we are interested only in responses that correspond to the current view value - if (inputValue === modelCtrl.$viewValue) { - if (matches.length > 0) { - - scope.activeIdx = 0; - scope.matches.length = 0; - - //transform labels - for(var i=0; i= minSearch) { - getMatchesAsync(inputValue); - } - } - - return isEditable ? inputValue : undefined; - }); - - modelCtrl.$render = function () { - var locals = {}; - locals[parserResult.itemName] = selected || modelCtrl.$viewValue; - element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); - selected = undefined; - }; - - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var locals = {}; - locals[parserResult.itemName] = selected = scope.matches[activeIdx].model; - - modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals)); - modelCtrl.$render(); - }; - - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.bind('keydown', function (evt) { - - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } - - evt.preventDefault(); - - if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - - } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - - } else if (evt.which === 13 || evt.which === 9) { - scope.$apply(function () { - scope.select(scope.activeIdx); - }); - - } else if (evt.which === 27) { - evt.stopPropagation(); - - resetMatches(); - scope.$digest(); - } - }); - - $document.bind('click', function(){ - resetMatches(); - scope.$digest(); - }); - - element.after($compile(popUpEl)(scope)); - } - }; - -}]) - - .directive('typeaheadPopup', function () { - return { - restrict:'E', - scope:{ - matches:'=', - query:'=', - active:'=', - position:'=', - select:'&' - }, - replace:true, - templateUrl:'template/typeahead/typeahead.html', - link:function (scope, element, attrs) { - - scope.isOpen = function () { - return scope.matches.length > 0; - }; - - scope.isActive = function (matchIdx) { - return scope.active == matchIdx; - }; - - scope.selectActive = function (matchIdx) { - scope.active = matchIdx; - }; - - scope.selectMatch = function (activeIdx) { - scope.select({activeIdx:activeIdx}); - }; - } - }; - }) - - .filter('typeaheadHighlight', function() { - - function escapeRegexp(queryToEscape) { - return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); - } - - return function(matchItem, query) { - return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : query; - }; +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) + +/** + * A helper service that can parse typeahead's syntax (string provided by users) + * Extracted to a separate service for ease of unit testing + */ + .factory('typeaheadParser', ['$parse', function ($parse) { + + // 00000111000000000000022200000000000000003333333333333330000000000044000 + var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/; + + return { + parse:function (input) { + + var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source; + if (!match) { + throw new Error( + "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" + + " but got '" + input + "'."); + } + + return { + itemName:match[3], + source:$parse(match[4]), + viewMapper:$parse(match[2] || match[1]), + modelMapper:$parse(match[1]) + }; + } + }; +}]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$document', '$position', 'typeaheadParser', function ($compile, $parse, $q, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require:'ngModel', + link:function (originalScope, element, attrs, modelCtrl) { + + var selected; + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + + //pop-up element used to display matches + var popUpEl = angular.element( + ""+ + ""); + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + originalScope.$on('$destroy', function(){ + scope.$destroy(); + }); + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + }; + + var getMatchesAsync = function(inputValue) { + + var locals = {$viewValue: inputValue}; + isLoadingSetter(originalScope, true); + $q.when(parserResult.source(scope, locals)).then(function(matches) { + + //it might happen that several async queries were in progress if a user were typing fast + //but we are interested only in responses that correspond to the current view value + if (inputValue === modelCtrl.$viewValue) { + if (matches.length > 0) { + + scope.activeIdx = 0; + scope.matches.length = 0; + + //transform labels + for(var i=0; i= minSearch) { + getMatchesAsync(inputValue); + } + } + + return isEditable ? inputValue : undefined; + }); + + modelCtrl.$render = function () { + var locals = {}; + locals[parserResult.itemName] = selected || modelCtrl.$viewValue; + element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue); + selected = undefined; + }; + + scope.select = function (activeIdx) { + //called from within the $digest() cycle + var locals = {}; + locals[parserResult.itemName] = selected = scope.matches[activeIdx].model; + + modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals)); + modelCtrl.$render(); + }; + + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.bind('keydown', function (evt) { + + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + + evt.preventDefault(); + + if (evt.which === 40) { + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + + } else if (evt.which === 38) { + scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + + } else if (evt.which === 13 || evt.which === 9) { + scope.$apply(function () { + scope.select(scope.activeIdx); + }); + + } else if (evt.which === 27) { + evt.stopPropagation(); + + resetMatches(); + scope.$digest(); + } + }); + + $document.bind('click', function(){ + resetMatches(); + scope.$digest(); + }); + + element.after($compile(popUpEl)(scope)); + } + }; + +}]) + + .directive('typeaheadPopup', function () { + return { + restrict:'E', + scope:{ + matches:'=', + query:'=', + active:'=', + position:'=', + select:'&' + }, + replace:true, + templateUrl:'template/typeahead/typeahead.html', + link:function (scope, element, attrs) { + + scope.isOpen = function () { + return scope.matches.length > 0; + }; + + scope.isActive = function (matchIdx) { + return scope.active == matchIdx; + }; + + scope.selectActive = function (matchIdx) { + scope.active = matchIdx; + }; + + scope.selectMatch = function (activeIdx) { + scope.select({activeIdx:activeIdx}); + }; + } + }; + }) + + .filter('typeaheadHighlight', function() { + + function escapeRegexp(queryToEscape) { + return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); + } + + return function(matchItem, query) { + return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : query; + }; }); \ No newline at end of file diff --git a/template/accordion/accordion-group.html b/template/accordion/accordion-group.html index 85e1a2a51b..89338d3463 100644 --- a/template/accordion/accordion-group.html +++ b/template/accordion/accordion-group.html @@ -1,5 +1,5 @@ -
- -
-
+
+ +
+
\ No newline at end of file diff --git a/template/typeahead/typeahead.html b/template/typeahead/typeahead.html index d535f0e438..2a7b9d7725 100644 --- a/template/typeahead/typeahead.html +++ b/template/typeahead/typeahead.html @@ -1,5 +1,5 @@ -