I am new to angular and I am trying to figure out the following problem. When the user highlights some text, I would like a ui-bootstrap popover to surround the highlighted text. Since this would manipulate the DOM I think I need to use a directive for this. I was able to successfully implement a simpler version of this problem here
app.directive('selectOnClick', function ($window) {
return {
link: function (scope, element) {
element.on('click', function () {
var span = document.createElement("span");
span.style.fontWeight = "bold";
span.style.color = "green";
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents(span);
sel.removeAllRanges();
sel.addRange(range);
}
}
});
}
}
});
In the above code I am able to surround the highlighted text with a span tag. However I would like to instead use a ui-bootstrap popover. I tried replacing the span part with var popover=angular.element("<a href=#' uib-popover='hello' popover-title='hello'></a>"); but this did not work. Am I on the right track or would this approach not work with a ui-bootstrap element?
UPDATED
Here is my attempt at adding the the popover element
app.directive('selectOnClick', function ($window, $compile) {
return {
link: function (scope, element) {
element.on('click', function () {
var popover=angular.element("<a href=#' uib-popover='hello' popover-title='hello'></a>");
if (window.getSelection) {
var sel = window.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents($compile(popover));
sel.removeAllRanges();
sel.addRange(range);
}
}
});
}
}
});
Unfortunately I am getting the error TypeError: Failed to execute 'surroundContents' on 'Range': parameter 1 is not of type 'Node'. on the line range.surroundContents($compile(popover)); So I suppose $compile(popover) is not the correct type. Can it be converted to Node type somehow?
Related
A am attempting to attach the ngBindHtml directive in an application within a link function of a directive. The module in which the directive is located injects ngSanitize like such:
angular.module('ui.bootstrap.contextMenu', ['ngSanitize'])
.directive('contextMenu', cm);
where cm is the directive function. The link function looks like:
var link = function ($scope, element, attrs) {
element.on('contextmenu', function (event) {
event.stopPropagation();
$scope.$apply(function () {
event.preventDefault();
var options = $scope.$eval(attrs.contextMenu);
var model = $scope.$eval(attrs.model);
if (options instanceof Array) {
if (options.length === 0) {
return;
}
renderContextMenu($scope, event, options, model);
} else {
throw '"' + attrs.contextMenu + '" not an array';
}
});
});
};
where renderContextMenu sketches out the html that will be attached to the body of the page. Within this function I have the following lines of code:
$div.attr('ng-bind-html', text);
$a.append($div);
which should produce something that looks like:
<a><div ng-bind-html="the text"></div></a>
and it does. The problem is that the text is not actually displayed. Does anyone have any thoughts on this?
I think the "angular-y" way of doing this is to put the html code for the context menu that you're hoping to bind directly into the template, and just hide/show it as appropriate: only show it if a valid contextmenu event occurs and has options.length > 0.
I am not very familiar with the CKEDITOR API yet and now I got stuck trying to find the way to create placeholders inside of the CKEDITOR editable area.The expected behaviour for the placeholder - to dissappear on user interaction with it, allowing to edit the content instead.
I know that there is already a placeholder plugin (http://ckeditor.com/addon/placeholder) but its behaviour is not what I am looking for.
To be more specific, the question is: is it possible to subscribe for some events on the particular element inside of the CKEDITOR?
Working in the angular context I am able to compile my html before it is passed to the CKEDITOR ng-model
$scope.html = $compile('<div><span text-placeholder >Placeholder</span></div>')($scope).html();
But then I fail trying to set click events inside of the directive:
.directive('textPlaceholder', [ function () {
return {
restrict: 'A',
link: function ($scope, $element) {
//THIS DOES NOT WORK UNFORTUNATELY
$element.on('click', function () {
console.log('clicked');
})
}
}
}])
Any thoughts?
UPDATE: For now I came up with the solution to implement simple plugin and then reference it in the CKEDITOR config:
(function () {
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
editor.on('key', function (evt) {
var el = $(CKEDITOR.instances.editor1.getSelection().getNative().baseNode.parentElement);
if (el.hasClass('text-placeholder')) {
el.remove();
}
});
}
});
})();
Looks ugly for me. Any feedback is appreciated.
This seems to be a final Solution:
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
editor.on('contentDom', function () {
var editable = editor.editable();
editable.attachListener(editable, 'click', function (event) {
var $placeholder = $(event.data.$.target).closest('.text-placeholder');
if ($placeholder.length > 0) {
var selection = editor.getSelection();
selection.selectElement(selection.getStartElement());
}
});
});
}
});
This applies the selection on the element with "text-placeholder" class when user focuses it inside of the editable area
Update:
See example
You inspired me to write one myself, using the above example as a starting point. In my use case I wanted to take placeholder text from an attribute on the editor -- data-placeholder -- and display it in the editor. When the editor gets focus, the placeholder text disappears. When the editor blurs -- if no user content has been entered -- the placeholder text is displayed again. Additionally, I set a data-placeholder-showing attribute so that I can, for example, use CSS to make the placeholder text gray. Here's my code:
CKEDITOR.plugins.add('text-placeholder', {
init: function (editor) {
var placeholder = editor.element.getAttribute('data-placeholder');
editor.on('contentDom', function () {
if (placeholder) {
editor.setData(placeholder);
editor.element.setAttribute('data-placeholder-showing', true);
}
});
editor.on('focus', function() {
if (editor.getData() === placeholder) {
editor.element.setAttribute('data-placeholder-showing', false);
editor.setData('');
}
});
editor.on('blur', function() {
if (placeholder && editor.getData().length === 0) {
editor.element.setAttribute('data-placeholder-showing', true);
editor.setData(placeholder);
}
});
}
});
I'm trying to build a simple infinite scroll. It loads the data fine but after loading, new added elements' directives don't work.
This is relevant part of the scroll checking and data loading directive.
.directive("scrollCheck", function ($window, $http) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
// calculating windowBottom and docHeight here then
if (windowBottom >= (docHeight - 100)) {
// doing some work here then
$http.get('service page').then(function (result) {
if (result.data.trim() != "") {
var newDiv = angular.element(result.data);
element.append(newDiv);
}
// doing some other work
},function () {
// error handling here
});
}
scope.$apply();
});
};
})
Service page returns some repeats of this structure as result.data
<div ...>
<div ... ng-click="test($event)"></div>
<div ...>...</div>
</div>
As i said data loads just fine but those test() functions in ng-clickdirectives don't work. How to get em work?
I believe you are going to need to compile the html element returned. Something like this
$compile(newDiv)(scope); // Corrected. Thanks
You'll need to be sure and pass in $compile into your function
im having a problem with two directives:
slimScroll:
directive("slimScroll", [
function() {
return {
link: function(scope, ele, attrs) {
return ele.slimScroll({
height: attrs.scrollHeight || "100%"
});
}
};
}
gives me the following error:
TypeError: undefined is not a function at link (http://localhost/Sistema/js/directives.js:144:32)
The line that references is:
return ele.slimScroll({
And this directive:
.directive("collapseNav", [
function() {
return {
link: function(scope, ele) {
var $a, $aRest, $lists, $listsRest, app;
return $lists = ele.find("ul").parent("li"),
$lists.append('<i class="fa fa-arrow-circle-o-right icon-has-ul"></i>'),
$a = $lists.children("a"),
$listsRest = ele.children("li").not($lists),
$aRest = $listsRest.children("a"),
app = $("#app"),
$a.on("click", function(event) {
var $parent, $this;
return app.hasClass("nav-min") ? !1 : ($this = $(this),
$parent = $this.parent("li"),
$lists.not($parent).removeClass("open").find("ul").slideUp(),
$parent.toggleClass("open").find("ul").stop().slideToggle(), event.preventDefault());
}), $aRest.on("click", function() {
return $lists.removeClass("open").find("ul").slideUp();
}), scope.$on("minNav:enabled", function() {
return $lists.removeClass("open").find("ul").slideUp();
});
}
};
}
])
gives me this error
TypeError: undefined is not a function
at link (http://localhost/Sistema/js/directives.js:93:57)
The line that references is: $listsRest = ele.children("li").not($lists),
When you're referencing the second argument of the link function in a directive (in your case ele), you are not referencing a full-scale jQuery object reference of the element. Angular bundles with jqLite, meaning that only some jQuery methods are available.
In your two cases, that means:
slimScroll is a jQuery plugin that won't get automatically registered with jqLite
.not() is not available in jqLite
If you want full jQuery support, you'll have to include it in your application and then create a real jQuery object, by doing jQuery(ele[0]). The ele[0] references the native DOM element, which is suitable for creating a jQuery element.
I'm sure this is going to be a "dont do that!" but I am trying to display the style on an angular element.
<div ng-repeat="x in ['blue', 'green']" class="{{x}}">
<h3 insert-style>{{theStyle['background-color']}}</h3>
</div>
Result would be
<div class='blue'><h3>blue(psudeo code hex code)</h3></div>
<div class='green'><h3>green(psudeo code hex code)</h3></div>
I basically need to get the style attributes and display them.
Directive Code...
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0], null);
}
}
}];
Fiddle example: http://jsfiddle.net/ncapito/G33PE/
My final solution (using a single prop didn't work, but when I use the whole obj it works fine)...
Markup
<div insert-style class="box blue">
<h4 > {{ theStyle['color'] | toHex}} </h4>
</div>
Directive
directives.insertStyle = [ "$window", function($window){
return {
link: function(scope, element, attrs) {
var elementStyleMap = $window.getComputedStyle(element[0], null);
scope.theStyle = elementStyleMap
}
}
}];
Eureka!
http://jsfiddle.net/G33PE/5/
var leanwxApp = angular.module('LeanwxApp', [], function () {});
var controllers = {};
var directives = {};
directives.insertStyle = [ function(){
return {
link: function(scope, element, attrs) {
scope.theStyle = window.getComputedStyle(element[0].parentElement, null)
}
}
}];
leanwxApp.controller(controllers);
leanwxApp.directive(directives);
So that just took lots of persistence and guessing. Perhaps the timeout is unnecessary but while debugging it seemed I only got the style value from the parent after the timeout occurred.
Also I'm not sure why but I had to go up to the parentElement to get the style (even though it would realistically be inherited (shrug)?)
Updated fiddle again
Did one without the timeout but just looking at the parentElement for the style and it seems to still work, so scratch the suspicions about the style not being available at all, it's just not available where I would expect it.
Also holy cow there are a lot of ways to debug in Chrome:
https://developers.google.com/chrome-developer-tools/docs/javascript-debugging
I used
debugger;
statements in the code to drop in breakpoints without having to search all the fiddle files.
One more quick update
The code below comes out of Boostrap-UI from the AngularUI team and claims to provide a means to watch the appropriate events (haven't tried this but it looks like it should help).
http://angular-ui.github.io/bootstrap/
/**
* $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
* #param {DOMElement} element The DOMElement that will be animated.
* #param {string|object|function} trigger The thing that will cause the transition to start:
* - As a string, it represents the css class to be added to the element.
* - As an object, it represents a hash of style attributes to be applied to the element.
* - As a function, it represents a function to be called that will cause the transition to occur.
* #return {Promise} A promise that is resolved when the transition finishes.
*/
.factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
var $transition = function(element, trigger, options) {
options = options || {};
var deferred = $q.defer();
var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
var transitionEndHandler = function(event) {
$rootScope.$apply(function() {
element.unbind(endEventName, transitionEndHandler);
deferred.resolve(element);
});
};
if (endEventName) {
element.bind(endEventName, transitionEndHandler);
}
// Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
$timeout(function() {
if ( angular.isString(trigger) ) {
element.addClass(trigger);
} else if ( angular.isFunction(trigger) ) {
trigger(element);
} else if ( angular.isObject(trigger) ) {
element.css(trigger);
}
//If browser does not support transitions, instantly resolve
if ( !endEventName ) {
deferred.resolve(element);
}
});
// Add our custom cancel function to the promise that is returned
// We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
// i.e. it will therefore never raise a transitionEnd event for that transition
deferred.promise.cancel = function() {
if ( endEventName ) {
element.unbind(endEventName, transitionEndHandler);
}
deferred.reject('Transition cancelled');
};
return deferred.promise;
};
// Work out the name of the transitionEnd event
var transElement = document.createElement('trans');
var transitionEndEventNames = {
'WebkitTransition': 'webkitTransitionEnd',
'MozTransition': 'transitionend',
'OTransition': 'oTransitionEnd',
'transition': 'transitionend'
};
var animationEndEventNames = {
'WebkitTransition': 'webkitAnimationEnd',
'MozTransition': 'animationend',
'OTransition': 'oAnimationEnd',
'transition': 'animationend'
};
function findEndEventName(endEventNames) {
for (var name in endEventNames){
if (transElement.style[name] !== undefined) {
return endEventNames[name];
}
}
}
$transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
$transition.animationEndEventName = findEndEventName(animationEndEventNames);
return $transition;
}]);
The issue you'll face is that getComputedStyle is considered a very slow running method, so you will run into performance issues if using that, especially if you want angularjs to update the view whenever getComputedStyle changes.
Also, getComputedStyle will resolve every single style declaration possible, which i think will not be very useful. So i think a method to reduce the number of possible style is needed.
Definitely consider this an anti-pattern, but if you still insist in this foolishness:
module.directive('getStyleProperty', function($window){
return {
//Child scope so properties are not leaked to parent
scope : true,
link : function(scope, element, attr){
//A map of styles you are interested in
var styleProperties = ['text', 'border'];
scope.$watch(function(){
//A watch function to get the styles
//Since this runs every single time there is an angularjs loop, this would not be a very performant way to do this
var obj = {};
var computedStyle = $window.getComputedStyle(element[0]);
angular.forEach(styleProperties, function(value){
obj[value] = computedStyle.getPropertyValue(value);
});
return obj;
}, function(newValue){
scope.theStyle = newValue;
});
}
}
});
This solution works if you don't HAVE to have the directive on the child element. If you just place the declaration on the ng-repeat element itself, your solution works:
<div insert-style ng-repeat="x in ['blue', 'green']" class="{{x}}">
Fiddle