From 67ba621bfa0602c04e4cdadc1fb1c87ca24bfbdd Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Thu, 13 Aug 2015 17:58:33 -0700 Subject: [PATCH] fix(dialog): enable support for html content for alert and confirm dialogs * dialog`::onShow( )` now uses `wrapSimpleContent( )` to wrap simple strings in `

` tags * dialog basic demo `showConfirm( )` uses HTML span tag to illustrate use of HMTL content * implemented generic `md-template` to safely inject HTML content into an element BREAKING CHANGE: dialog content text is now injected into **div.md-dialog-content-body** Before the template used was: ```html

``` Now uses: ```html

``` Fixes #1495. --- .../dialog/demoBasicUsage/script.js | 2 +- .../dialog/demoBasicUsage/style.css | 6 ++ src/components/dialog/dialog.js | 37 +++++++--- src/components/dialog/dialog.scss | 4 ++ src/components/dialog/dialog.spec.js | 70 +++++++++++++++---- src/components/tabs/js/tabsDirective.js | 6 +- ...eDirective.js => tabsTemplateDirective.js} | 6 +- src/core/core.js | 35 ++++++++-- 8 files changed, 132 insertions(+), 34 deletions(-) rename src/components/tabs/js/{templateDirective.js => tabsTemplateDirective.js} (88%) diff --git a/src/components/dialog/demoBasicUsage/script.js b/src/components/dialog/demoBasicUsage/script.js index 8df86c77bc9..a617d1e5aa0 100644 --- a/src/components/dialog/demoBasicUsage/script.js +++ b/src/components/dialog/demoBasicUsage/script.js @@ -23,7 +23,7 @@ angular.module('dialogDemo1', ['ngMaterial']) // Appending dialog to document.body to cover sidenav in docs app var confirm = $mdDialog.confirm() .title('Would you like to delete your debt?') - .content('All of the banks have agreed to forgive you your debts.') + .content('All of the banks have agreed to forgive you your debts.') .ariaLabel('Lucky day') .targetEvent(ev) .ok('Please do it!') diff --git a/src/components/dialog/demoBasicUsage/style.css b/src/components/dialog/demoBasicUsage/style.css index 69d9bcb8961..103dcbd5563 100644 --- a/src/components/dialog/demoBasicUsage/style.css +++ b/src/components/dialog/demoBasicUsage/style.css @@ -1,3 +1,9 @@ #popupContainer { position:relative; } + +.debt-be-gone { + font-weight: bold; + color:blue; + text-decoration: underline; +} diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 052c671a5ba..10121554dc9 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -2,10 +2,11 @@ * @ngdoc module * @name material.components.dialog */ -angular.module('material.components.dialog', [ - 'material.core', - 'material.components.backdrop' -]) +angular + .module('material.components.dialog', [ + 'material.core', + 'material.components.backdrop' + ]) .directive('mdDialog', MdDialogDirective) .provider('$mdDialog', MdDialogProvider); @@ -32,8 +33,6 @@ function MdDialogDirective($$rAF, $mdTheming) { }; } - - /** * @ngdoc service * @name $mdDialog @@ -408,7 +407,7 @@ function MdDialogProvider($$interimElementProvider) { '', ' ', '

{{ dialog.title }}

', - '

{{ dialog.content }}

', + '
', '
', '
', '

tags. + * otherwise accept HTML content within the dialog content area... + * NOTE: Dialog uses the md-template directive to safely inject HTML content. + */ + function wrapSimpleContent() { + if ( controller ) { + var HTML_END_TAG = /<\/[\w-]*>/gm; + var content = controller.content; + + var hasHTML = HTML_END_TAG.test(content); + if (!hasHTML) { + content = $mdUtil.supplant("

{0}

", [content]); + } + + // Publish updated dialog content body... to be compiled by mdTemplate directive + controller.content = content; + } + } + } /** @@ -538,8 +559,6 @@ function MdDialogProvider($$interimElementProvider) { // In case the user provides a raw dom element, always wrap it in jqLite options.parent = angular.element(options.parent || $rootElement); - - } /** diff --git a/src/components/dialog/dialog.scss b/src/components/dialog/dialog.scss index 3daca41d71e..d57b7a3ec95 100644 --- a/src/components/dialog/dialog.scss +++ b/src/components/dialog/dialog.scss @@ -76,6 +76,10 @@ md-dialog { padding-top: 0; } } + + .md-dialog-content-body { + width:100%; + } } .md-actions { diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index 27ed79b097f..fb8db593063 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -19,6 +19,7 @@ describe('$mdDialog', function() { $timeout.flush(); // flush to start animations $$rAF.flush(); // flush animations $animate.triggerCallbacks(); + //$rootScope.$digest(); $timeout.flush(); // flush responses after animation completions } })); @@ -42,8 +43,8 @@ describe('$mdDialog', function() { .theme('some-theme') .ok('Next') ).then(function() { - resolved = true; - }); + resolved = true; + }); $rootScope.$apply(); runAnimation(); @@ -130,7 +131,7 @@ describe('$mdDialog', function() { 'ok', 'cancel', 'targetEvent', 'theme' ]); - it('shows a basic confirm dialog', inject(function($rootScope, $mdDialog, $animate) { + it('shows a basic confirm dialog with simple text content', inject(function($rootScope, $mdDialog, $animate) { var parent = angular.element('
'); var rejected = false; $mdDialog.show( @@ -149,15 +150,13 @@ describe('$mdDialog', function() { var container = angular.element(parent[0].querySelector('.md-dialog-container')); var dialog = parent.find('md-dialog'); - expect(dialog.attr('role')).toBe('dialog'); - var title = parent.find('h2'); - expect(title.text()).toBe('Title'); - var content = parent.find('p'); - expect(content.text()).toBe('Hello world'); - var buttons = parent.find('md-button'); + + expect(dialog.attr('role')).toBe('dialog'); + expect(title.text()).toBe('Title'); + expect(content.text()).toBe('Hello world'); expect(buttons.length).toBe(2); expect(buttons.eq(0).text()).toBe('Next'); expect(buttons.eq(1).text()).toBe('Forget it'); @@ -169,17 +168,60 @@ describe('$mdDialog', function() { expect(rejected).toBe(true); })); + it('shows a basic confirm dialog with HTML content', inject(function($rootScope, $mdDialog, $animate) { + var parent = angular.element('
'); + + $mdDialog.show( + $mdDialog.confirm({ + parent: parent, + ok: 'Next', + cancel: 'Back', + title: 'Which Way ', + content: '
Choose
' + }) + ); + + runAnimation(); + + var container = angular.element(parent[0].querySelector('.md-dialog-container')); + var content = angular.element(container[0].querySelector('.mine')); + + expect(content.text()).toBe('Choose'); + })); + + it('shows a basic confirm dialog with HTML content using custom types', inject(function($rootScope, $mdDialog, $animate) { + var parent = angular.element('
'); + + $mdDialog.show( + $mdDialog.confirm({ + parent: parent, + ok: 'Next', + cancel: 'Back', + title: 'Which Way ', + content: 'Choose' + }) + ); + + runAnimation(); + + var container = angular.element(parent[0].querySelector('.md-dialog-container')); + var content = angular.element(container[0].querySelector('.mine')); + + expect(content.text()).toBe('Choose'); + })); + it('should focus `md-button.dialog-close` on open', inject(function($mdDialog, $rootScope, $document, $timeout, $mdConstant) { jasmine.mockElementFocus(this); var parent = angular.element('
'); $mdDialog.show({ - template: '' + - '
' + - '' + - '
' + + template: '' + + '' + + '
' + + ' ' + + '
' + '
', - parent: parent, + parent: parent }); runAnimation(); diff --git a/src/components/tabs/js/tabsDirective.js b/src/components/tabs/js/tabsDirective.js index a7948b93848..27007d4c91e 100644 --- a/src/components/tabs/js/tabsDirective.js +++ b/src/components/tabs/js/tabsDirective.js @@ -149,7 +149,7 @@ function MdTabs () { ng-disabled="tab.scope.disabled"\ md-swipe-left="$mdTabsCtrl.nextPage()"\ md-swipe-right="$mdTabsCtrl.previousPage()"\ - md-template="::tab.label"\ + md-tabs-template="::tab.label"\ md-scope="::tab.parent">\ \ \ @@ -165,7 +165,7 @@ function MdTabs () { ng-focus="$mdTabsCtrl.hasFocus = true"\ ng-blur="$mdTabsCtrl.hasFocus = false"\ ng-repeat="tab in $mdTabsCtrl.tabs"\ - md-template="::tab.label"\ + md-tabs-template="::tab.label"\ md-scope="::tab.parent">\
\ \ @@ -187,7 +187,7 @@ function MdTabs () { \'md-no-scroll\': $mdTabsCtrl.dynamicHeight\ }">\
\ diff --git a/src/components/tabs/js/templateDirective.js b/src/components/tabs/js/tabsTemplateDirective.js similarity index 88% rename from src/components/tabs/js/templateDirective.js rename to src/components/tabs/js/tabsTemplateDirective.js index 8c421c4fb85..6d0f54923f7 100644 --- a/src/components/tabs/js/templateDirective.js +++ b/src/components/tabs/js/tabsTemplateDirective.js @@ -1,13 +1,13 @@ angular .module('material.components.tabs') - .directive('mdTemplate', MdTemplate); + .directive('mdTabsTemplate', MdTabsTemplate); -function MdTemplate ($compile, $mdUtil) { +function MdTabsTemplate ($compile, $mdUtil) { return { restrict: 'A', link: link, scope: { - template: '=mdTemplate', + template: '=mdTabsTemplate', connected: '=?mdConnectedIf', compileScope: '=mdScope' }, diff --git a/src/core/core.js b/src/core/core.js index 48b82a50dd0..7a200adb4b5 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -1,4 +1,3 @@ - /** * Initialization function that validates environment * requirements. @@ -10,8 +9,8 @@ angular 'material.core.gestures', 'material.core.theming' ]) - .config( MdCoreConfigure ); - + .directive('mdTemplate', MdTemplateDirective) + .config(MdCoreConfigure); function MdCoreConfigure($provide, $mdThemingProvider) { @@ -24,7 +23,35 @@ function MdCoreConfigure($provide, $mdThemingProvider) { .backgroundPalette('grey'); } -function rAFDecorator( $delegate ) { +function MdTemplateDirective($compile) { + return { + restrict: 'A', + scope: { + template: '=mdTemplate' + }, + link: function postLink(scope, element) { + scope.$watch('template', assignSafeHTML); + + /** + * To add safe HTML: assign and compile in + * isolated scope. + */ + function assignSafeHTML(value) { + // when the 'compile' expression changes + // assign it into the current DOM + element.html(value); + + // Compile the new DOM and link it to the current scope. + // NOTE: we only compile .childNodes so that we don't get + // into infinite loop compiling ourselves + $compile(element.contents())(scope); + } + } + }; + +} + +function rAFDecorator($delegate) { /** * Use this to throttle events that come in often. * The throttled function will always use the *last* invocation before the