How do I create an AngularJS UI bootstrap popover with HTML content? - angularjs

I want to create a bootstrap popover with a pre tag containing a prettified JSON object. The naive implementation,
<span popover='<pre>{[ some_obj | json:" " ]}</pre>'
popover-trigger='mouseenter'>
escapes the content before inserting it into the popup. What's the best way of specifying a popover body with html content?

UPDATE:
As can been seen in this, you should now be able to do this without overriding the default template.
ORIGINAL:
As of angular 1.2+ ng-bind-html-unsafe has been removed. You should be using the $sce service Reference.
Here is a filter for creating trusted HTML.
MyApp.filter('unsafe', ['$sce', function ($sce) {
return function (val) {
return $sce.trustAsHtml(val);
};
}]);
Here is the overwritten Angular Bootstrap 0.11.2 template making use of this filter
// update popover template for binding unsafe html
angular.module("template/popover/popover.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("template/popover/popover.html",
"<div class=\"popover {{placement}}\" ng-class=\"{ in: isOpen(), fade: animation() }\">\n" +
" <div class=\"arrow\"></div>\n" +
"\n" +
" <div class=\"popover-inner\">\n" +
" <h3 class=\"popover-title\" ng-bind-html=\"title | unsafe\" ng-show=\"title\"></h3>\n" +
" <div class=\"popover-content\"ng-bind-html=\"content | unsafe\"></div>\n" +
" </div>\n" +
"</div>\n" +
"");
}]);
EDIT: Here is a Plunker implementation.
EDIT 2: As this answer keeps getting hits, I'll keep it updated as best I can. As a reference Here is the template from the angular-ui bootstrap repo. If this changes, the override template will require matching updates and the addition of the ng-bind-html=\"title | unsafe\" and ng-bind-html=\"content | unsafe\" attributes to continue working.
For updated conversation check the issue here.

Use the popover-template directive
If you are using a version of angular-ui equal or above 0.13.0, your best option is to use the popover-template directive. Here is how to use it:
<button popover-template="'popover.html'">My HTML popover</button>
<script type="text/ng-template" id="popover.html">
<div>
Popover content
</div>
</script>
NB: Do not forget the quotes around the template name in popover-template="'popover.html'".
See demo plunker
As a side note, it is possible to externalize the popover template in a dedicated html file, instead of declaring it in a <script type="text/ng-template> element as above.
See second demo plunker

I have posted a solution on the github project: https://github.com/angular-ui/bootstrap/issues/520
I you want to add this functionality to your project, here is a patch.
Add those directives:
angular.module("XXX")
.directive("popoverHtmlUnsafePopup", function () {
return {
restrict: "EA",
replace: true,
scope: { title: "#", content: "#", placement: "#", animation: "&", isOpen: "&" },
templateUrl: "template/popover/popover-html-unsafe-popup.html"
};
})
.directive("popoverHtmlUnsafe", [ "$tooltip", function ($tooltip) {
return $tooltip("popoverHtmlUnsafe", "popover", "click");
}]);
And add the template:
<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>
<div class="popover-inner">
<h3 class="popover-title" ng-bind="title" ng-show="title"></h3>
<div class="popover-content" bind-html-unsafe="content"></div>
</div>
</div>
Usage: <button popover-placement="top" popover-html-unsafe="On the <b>Top!</b>" class="btn btn-default">Top</button>
View it on plunkr: http://plnkr.co/edit/VhYAD04ETQsJ2dY3Uum3?p=preview

You need to alter the default popover template to specify you want to allow Html content.
Look the popover-content div, it now has its binding done to the content property allowing unsafe html:
angular.module("template/popover/popover.html", []).run(["$templateCache", function ($templateCache) {
$templateCache.put("template/popover/popover.html",
"<div class='popover {{placement}}' ng-class='{ in: isOpen(), fade: animation() }'>" +
"<div class='arrow'></div><div class='popover-inner'>" +
"<h3 class='popover-title' ng-bind='title' ng-show='title'></h3>" +
"<div class='popover-content' ng-bind-html-unsafe='content'></div>" +
"<button class='btn btn-cancel' ng-click='manualHide()'>Cancel</button>" +
"<button class='btn btn-apply' ng-click='manualHide()'>Apply</button></div></div>");
}]);

For all your conventional Bootstrap popover needs you could utilize the following angular directive. It removes clutter from the HTML template and is very easy to use.
You can configure popover's title, content, placement, fade in/out delay, trigger event and whether content should be treated as html. It also prevents content overflow & clipping.
Related plunker with all teh codes here http://plnkr.co/edit/MOqhJi
Screencap
Usage
<!-- HTML -->
<div ng-model="popup.content" popup="popup.options">Some element</div>
/* JavaScript */
this.popup = {
content: 'Popup content here',
options: {
title: null,
placement: 'right',
delay: { show: 800, hide: 100 }
}
};
JavaScript
/**
* Popup, a Bootstrap popover wrapper.
*
* Usage:
* <div ng-model="model" popup="options"></div>
*
* Remarks:
* To prevent content overflow and clipping, use CSS
* .popover { word-wrap: break-word; }
* Popup without title and content will not be shown.
*
* #param {String} ngModel popup content
* #param {Object} options popup options
* #param {String} options.title title
* #param {Boolean} options.html content should be treated as html markup
* #param {String} options.placement placement (top, bottom, left or right)
* #param {String} options.trigger trigger event, default is hover
* #param {Object} options.delay milliseconds or { show:<ms>, hide:<ms> }
*/
app.directive('popup', function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
ngModel: '=',
options: '=popup'
},
link: function(scope, element) {
scope.$watch('ngModel', function(val) {
element.attr('data-content', val);
});
var options = scope.options || {} ;
var title = options.title || null;
var placement = options.placement || 'right';
var html = options.html || false;
var delay = options.delay ? angular.toJson(options.delay) : null;
var trigger = options.trigger || 'hover';
element.attr('title', title);
element.attr('data-placement', placement);
element.attr('data-html', html);
element.attr('data-delay', delay);
element.popover({ trigger: trigger });
}
};
});

See https://github.com/jbruni/bootstrap-bower-jbruni, which allow to use a popover-template

The following CSS styling seems to have done what I wanted in my specific case:
.popover-content {
white-space: pre;
font-family: monospace;
}
The general question still remains open.

Here is a fiddle of my solution that:
Is accessible (you can use tab keys to activate/deactivate).
Allows a user to hover the popover and for the popover to remain open.
Allows multiple popovers on the page, but only a single popover to be activated at any given time.
Doesn't rely on any third party, though the bootstrap popover styles have been borrowed.
The way this works is that we instantiate however many popovers we will have on the page in a popover array (see the TODO in the comments on how to wire this up).
Then anytime a user tabs into or hovers into an element that should trigger a popover we activate that specific popover in the popover array. When the user is no longer hovering the element we set a timeout for that specific popover in the array. If that timeout has elapsed it does a quick check to see if the user has re-hovered or re-focused (via tabbing) the element. If so then we keep the popover alive. If not we hide the popover.
For the CSS I did not want to rely on using bootstrap so I borrowed the styles directly from bootstrap. If you try to use bootstrap's popover styles you may run into some weird behavior where bootstrap is running it's own scripts on our custom popover which we do not want.
HTML:
<section>
<a href="#"
ng-mouseover="showPopover(i)"
ng-mouseleave="hidePopover(i)"
ng-focus="showPopover(i)"
ng-blur="hidePopover(i)">
I trigger a popover - {{i}}
</a>
<popover popover-show="popover[i].popoverTracker">
<div class="arrow"></div>
<div class="custom-popover-content"
ng-mouseover="showPopover(i)"
ng-mouseleave="hidePopover(i)"
ng-focus="showPopover(i)"
ng-blur="hidePopover(i)">
<a href="#"
ng-focus="showPopover(i)"
ng-blur="hidePopover(i)">You can tab into me, I'm accessible!</a>
<br/>
<a href="#"
ng-focus="showPopover(i)"
ng-blur="hidePopover(i)">You can tab into me, I'm accessible!</a>
</div>
</popover>
</section>
Angular Controller and Directive:
angular.module('controllers', []);
angular.module('directives', []);
angular.module('myApp', ['ngAnimate', 'controllers', 'directives']);
angular.module('controllers')
.controller('MainCtrl', function ($scope, $timeout) {
$scope.popover = [];
(function init() {
// TODO: Make this dynamic so that we can pass it a value and it will generate the right amount
// Initializing the array of popovers on startup
createPopoverTrackers(20);
})();
// Creating an array of popovers equal to the number of popovers on the page
function createPopoverTrackers(count) {
for(var i = 0; i < count; i++) {
$scope.popover.push({
popoverTracker: false,
popoverKeepAlive: false,
timer: null
})
}
}
// A user has focused on an element that has an associated popover
$scope.queueOpenPopover = function(index) {
// Show our specified tracker
$scope.popover[index].popoverTracker = true;
// Hide the rest
Object.keys($scope.popover)
.filter(function(trackerIndex) {
return trackerIndex != index
})
.forEach(function(trackerIndex) {
$scope.popover[trackerIndex].popoverTracker = false;
$scope.popover[trackerIndex].popoverKeepAlive = false;
const timer = $scope.popover[trackerIndex].timer;
if(timer) {
$timeout.cancel(timer);
$scope.popover[trackerIndex].timer = null;
}
})
};
// Queuing up the demise of the popover
$scope.queueKillPopover = function(index) {
$scope.popover[index].timer = $timeout(function() {
if (!$scope.popover[index].popoverKeepAlive) {
// Popover or the popover trigger were not hovered within the time limit, kill it!
$scope.popover[index].popoverTracker = false;
}
}, 700);
};
// When a user focuses into the actual popover itself or it's trigger, we need to keep it alive
$scope.showPopover = function(index) {
$scope.popover[index].popoverKeepAlive = true;
$scope.queueOpenPopover(index);
};
// The user has removed focus from the popover or it's trigger, set this to false so the timer knows to kill it
$scope.hidePopover = function(index) {
$scope.popover[index].popoverKeepAlive = false;
$scope.queueKillPopover(index);
};
});
angular.module('directives')
.directive('popover', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {
'popoverShow': '='
},
template: '<div class="custom-popover bottom" ng-show="popoverShow" ng-transclude></div>'
};
});
CSS borrowed from bootstrap:
.custom-popover {
position: absolute;
z-index: 1010;
max-width: 276px;
padding: 1px;
text-align: left;
white-space: normal;
background-color: #fff;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 6px;
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
background-clip: padding-box;
}
.custom-popover .arrow,
.custom-popover .arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.custom-popover .arrow {
border-width: 11px;
}
.custom-popover .arrow:after {
border-width: 10px;
content: "";
}
.custom-popover.bottom {
margin-top: 10px;
}
.custom-popover.bottom .arrow {
top: -11px;
left: 50%;
margin-left: -11px;
border-bottom-color: rgba(0, 0, 0, 0.25);
border-top-width: 0;
}
.custom-popover.bottom .arrow:after {
top: 1px;
margin-left: -10px;
border-bottom-color: #ffffff;
border-top-width: 0;
content: " ";
}
.custom-popover-content {
padding: 9px 14px;
}

Related

How to change the color of a button for only a few seconds using angular?

I want my button to change color when clicked. Also only change it for 3 seconds and then go back to the default color. I have been looking at similar questions postet on stack overflow but whatever I tried it didn't work (don't know why my code isn't working). Also Im not sure how to make it change the color for only 3 seconds.
So far I
1. $scope.isActive=false;
2. then in the controller, I changed it to true if clicked:
$scope.copyText = function(text){
$scope.isActive = !$scope.isActive;
console.log('clicked in controller');
Clipboard.copy(text)
}
html:
<div class="inner-single" ng-hide="updateInfo">
<h3>Login Details:</h3>
<h5 ><span class="categories">Username:</span> {{account.username}}<button
ng-click="copyText(account.username)" ng-class="isActive ? 'noColor' : 'hasColor'" >
Copy</button></h5>
<button class="btn btn-large btn-default" ng-click="showForm()">Update Info</button>
CSS
.copy-button {
.copy-button.hasColor {
color:green;
}
.copy-button.noColor {
color: grey; }
font-size: 12px;
padding: 0px, 3px, 0px, 3px;
margin-left: 5px; }
}
For keeping track of the seconds, I would use the setTimeout function, however, not sure how to combine it with angular and changing the color..
Happy about suggestions!
Thank you!
You could use $timeout here with 3000(3 sec), and the again preset isActive flag over there.
Code
$scope.copyText = function(text){
$scope.isActive = !$scope.isActive;
console.log('clicked in controller');
Clipboard.copy(text);
//don't forget to add `$timeout` in controller dependency.
$timeout(function(){
$scope.isActive = !$scope.isActive;
}, 3000);
}
It seems like your CSS rules are incorrect, you have to correct them or otherwise put copy-button class over button
.copy-button.hasColor {
color: green;
}
.copy-button.noColor {
color: grey;
}
Demo Here
better to use $interval then timeout in this case
let stuff = $interval(function() {
do stuff
}, 100);
};

Angular Animation: How to add extra classes to a specific element when animation starts and ends?

I'm using state router to transition between pages.
I need to add a class to the <body> while the animation is running and remove it once the enter and leave animations are completed.
I tried to create a directive an inject the $animate service.
Then I started listening for enter and leave events as suggest in documentation.
The html:
<div class="ui-view-container">
<div ui-view style="height:100%;" class="suffle-page" suffle-page></div>
</div>
The directive:
;(function(){
angular.module('app')
.directive('sufflePage',function($animate){
var $body = $('body');
return {
link: function (scope, element) {
//var $el = $('[ui-view]');
$animate.enter(element,
function callback(element, phase) {
//$body.addClass('animating');
}
);
$animate.leave( element, function(){
function callback(element, phase) {
//$body.removeClass('animating')
}
})
}
}
});
})();
Then I have the CSS that animates those views
//prevents animation in mobile devices to faster performance
.ui-view-container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}
[ui-view].ng-enter, [ui-view].ng-leave {
...
}
[ui-view].ng-enter {
..
}
[ui-view].ng-enter-active {
..
}
[ui-view].ng-leave {
...
}
[ui-view].ng-leave-active {
...
}
body.animating{
/*this element is outter of the animation that's why i must append a class to the top level element. in this case body*/
.special-element{
display: none;
}
}
At $animate.enter(element...) an error is thrown:
TypeError: Cannot read property 'createDocumentFragment' of null
Any help?
I was misunderstanding the use of $animate.enter and $animate.leave and **I also did use an incorrect version of angular because the $animate.leave are part of 1.4.x versions an my project was built on top of version 1.3.0.
After updating the angular.js and angular-animate.js all i had to do was
1) create the directive that will monitor enter:start and enter:end events
2) load the directive into the project
3) and write the piece of code that adds the class to the body during the animation.
I hope it helps.
.directive('sufflePage',function($animate){
var $body = $('body');
return {
link: function (scope, element) {
if (!element){
return;
}
/***
* when the ui-view that is `entering` the page stars it adds the animating class to body
* when it leaves it removes the animating from the body class
*
* IMPORTANT: this works because the enter and exit animation are triggered in parallel with the same duration
*
*/
$animate.on('enter', element,
function callback(element, phase) {
if (phase == 'start'){
$body.addClass('animating');
} else {
$body.removeClass('animating');
}
}
);
scope.$on('$destroy', function(){
$animate.off('enter',element);
});
}
}

Angular directive which resizes elements automatically

I created a directive which makes elements resize automatically according to percents of window. But there are still problems I can't solve.
Here are the files:
autoresize.js:
angular.module('autoresize', [])
.directive('autoresize', function() { return {
scope:{theId:'#id', theClass:'#class', wFactor:'#w', hFactor:'#h'},
link: function(scope, elem, attrs) {
scope.onResize = function() {
if(scope.hFactor != 'w')
{
scope.height = Math.floor($(window).innerHeight()*scope.hFactor);
}
if(scope.wFactor != 'h')
{
scope.width = Math.floor($(window).innerWidth()*scope.wFactor);
}
if(scope.hFactor == 'w')
{
scope.height = scope.width;
}
if(scope.wFactor == 'h')
{
scope.width = scope.height;
}
$(elem).outerHeight(scope.height);
$(elem).outerWidth(scope.width);
}
scope.onResize();
angular.element($(window)).bind('resize', function() {
scope.onResize();
});
}
}
});
test.htm:
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style type="text/css">
*
{
box-sizing: border-box;
}
html, body
{
margin: 0;
padding: 0;
}
#left, #right
{
display: inline-block;
}
#left
{
background: #101010;
}
#right
{
background: #777777;
}
#bottom
{
background: #AAAAAA;
}
</style>
</head>
<body>
<div ng-app="testApp" ng-controller="testCtrl">
<div id="content">
<div id="left" autoresize w="0.5" h="0.8"></div><div id="right" autoresize w="0.5" h="0.8"></div>
<div id="bottom" autoresize w="1" h="0.2">{{width}}</div>
</div>
</div>
<script src="vendor/angular.min.js"></script>
<script src="vendor/jquery-2.1.3.min.js"></script>
<script src="autoresize.js"></script>
<script>
angular.module('testApp', ['autoresize'])
.controller('testCtrl', function($scope, $http) {
});
</script>
</body>
</html>
The first problem is that when I open the webapp, the sizes don't init correctly: I must resize the window to get it work. Why doesn't the line scope.onResize() in the link function init everything correctly?
The second problem is that the scope variables "width" and "height" are not accessible in the element: in the example below, there isn't anything in the div "bottom". How do you get them accessible?
(Here you could think that CSS would be sufficient, however actually my app is a bit more complicated that the example below and CSS isn't sufficient)
Thanks a lot!
Ad 1)
The scope.onResize() does not work, because when the link function on a directive is called, the DOM element handled by that directive is not yet rendered (it will be, once the directive is done processing). Try to wrap that call in $timeout(), like this:
$timeout(function() { scope.onResize(); });
This will cause calling scope.onResize() during the next tick, which is after the directive finishes processing.
Ad 2)
See this question, on how to modify scope from within a directive.
You need call you scope.onResize(); method from load event of element, That would be efficient to initial kick off for setting css of elements
Add below method in directive & remove direct call of scope.onResize(); from link function
Code
element.on('load', function() {
scope.onResize();
});

Angularjs auto-hide navigation

i would like to make my navigation menu that is fixed to the top of my page to auto-hide the same way you can make the taskbar in windows hide when you have "auto-hide taskbar" enabled
I would like it to hide and then when you move your mouse close to the top of the screen for it to become visible again and then hide again when you move your mouse away from the top.
What is the best way i can make this happen?
Thanks in advance for your answers!
So many different ways to do this but a very quick think about it ...
You have your HTML nav bar...
<div nav-directive>
<div class="nag" ng-class="{ 'visible': visible }"></div>
</div>
Directive
.directive('navDirective', function() {
return {
restrict: 'EA',
link: function(scope, el) {
scope.visible = false;
el.bind('mouseover', function() {
scope.visible = true;
// You shouldn't do but may need a scope apply here, not sure...
});
el.bind('mouseout', function() {
scope.visible = false;
// again not sure scope apply?
});
}
}
});
This will get you your basic adding and removing the class visible.
Then you can use some CSS3 to get some sliding motion in.
.nav {
top: 0px;
position: absolute;
transition: transform 1s ease-in;
transform: translate(-100%, 0);
}
.nav.visible {
transform: translate(0, 0);
}

How to access the inner element from an angularjs directive?

I'd like to compute the element scroller width, as the number of children x the width of first child.
To do that, I have designed a directive hscroller.
The HTML is as follows:
<div class="scrollerContainer">
<div id="photos" class="scroller" hscroller="hi!">
<div ng-repeat="photo in stage.photos"
class="imageViewerBackground"
style="background-image: url(rsc/stage/{{stage.id}}/{{photo.file}}.thumb.jpg)"
ng-click="openPopoverImageViewer('#photos', $index)"
>
<div>{{photo.description}}</div>
</div>
</div>
</div>
The directive is as follows:
app.directive('hscroller', function () {
return {
restrict: "A",
link: function(scope, element, attr) {
var pid=$(element).attr("id");
var children = $(element).children();
var id=$(children).attr("id");
var firstChild = $(children[0]);
var width = firstChild.width()*(children.length);
console.log("openPopover hscroller: compute scroller (id="+id
+") width "+children.length+" children of "
+firstChild.width()+"px width, total="+width
+ " (parentid="+pid+")"
);
$(element).css({"width":width+"px"});
}
};
});
While running, it sounds the directive has no children in there (a race condition with the ng-reapeat.?), the log is as follows:
[Log] openPopover hscroller: compute scroller (id=undefined) width 0 children of nullpx width, total=0 (parentid=photos)
I'm stuck with this, any idea?
Note: btw, all this is to adjust the width of the scroller element so that I could have a nice horizontal scroller on ipad device (Is there a fix in CSS?).
.hScrollable {
overflow-x: scroll;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
white-space:nowrap;
}
.scrollerContainer {
width: 100%;
height: #popoverScrollHeight;
.hScrollable;
}
.scroller {
overflow: hidden; // this is really important to avoid vertical scrolling on devices
height: #popoverScrollHeight;
}
You'd propably want to write your code in the link function inside a $timeout callback.
No need to wrap element with jQuery since it's already a jquery object.
app.directive('hscroller', function ($timeout) {
return {
restrict: "A",
link: function(scope, element, attr) {
$timeout(function(){
var pid=element.attr("id");
var children = element.children();
(...)
});
}
};
});

Resources