I have created a access key Angular directive.
angular.module('tcne.common').directive("accessKey", function () {
return {
restrict: "A",
scope: {
},
link: function (scope, element, attrs) {
var $element = $(element);
$element.attr("accesskey", attrs.accessKey);
var content = $element.html();
for (var i = 0; i < content.length; i++) {
var char = content[i];
if (char.toLowerCase() === attrs.accessKey.toLowerCase()) {
content = content.substr(0, i) + "<u>" + char + "</u>" + content.substr(i + 1);
break;
}
}
$element.html(content);
},
replace: false
};
});
It underscores the access key in the button Label and adds the access key attribute to the element. Can I somehow prevent the accesskey from setting the button in focus? It kills the purpose of keyboard short cuts
edit: Rolled my own acccess key
angular.module('tcne.common').directive('accessKey', ['$compile', '$interval', function ($compile, $interval) {
var modifierPressed = false;
$("body").keyup(function (e) {
if (modifierPressed && !e.altKey) {
modifierPressed = false;
digestScopes();
}
});
$("body").keydown(function (e) {
modifierPressed = e.altKey;
if (modifierPressed && scopes.hasOwnProperty(String.fromCharCode(e.which).toLowerCase())) {
var scope = scopes[String.fromCharCode(e.which).toLowerCase()];
scope.handle();
return;
}
if (modifierPressed) {
e.preventDefault();
digestScopes();
}
});
function digestScopes() {
for (var index in scopes) {
if (scopes.hasOwnProperty(index)) {
var scope = scopes[index]
scope.$digest();
}
}
}
function isModifierPressed() {
return modifierPressed;
}
var scopes = {};
return {
restrict: 'A',
scope: {
},
link: function (scope, element, attrs) {
var key = attrs.accessKey.toLowerCase();
var content = element.html();
var char;
for (var i = 0; i < content.length; i++) {
char = content[i];
if (char.toLowerCase() === key) {
content = content.substr(0, i) + '<u><strong ng-if="highlight()">{{char}}</strong><span ng-if="!highlight()">{{char}}</span></u>' + content.substr(i + 1);
break;
}
}
element.html(content);
var underscoreScope = scope.$new();
underscoreScope.char = char;
underscoreScope.highlight = isModifierPressed;
underscoreScope.handle = element.click.bind(element);
scopes[key] = underscoreScope;
scope.$on('$destroy', function () {
delete scopes[key];
});
$compile(element.find("u"))(underscoreScope);
},
replace: false
};
}]);
It also highlights the access key button when alt key is pressed which is nice
Any pit falls with this code? Thanks
Found a pitfall, element.html and then $compile will break any directives inside the element that is already compiled. So I changed to
var captionElement = element.contents().first(":text");
var content = captionElement.text();
And then I add my custom content like
var view = $("<span>").html(content);
captionElement.replaceWith(view);
$compile(view)(vm);
Please let me know if this is considered bad practice
Related
I am trying to create a directive that works like the ng-if directive, so I would like to grab it's functionality. I created a directive like this:
(function () {
'use strict';
angular.module('sapphire.directives').directive('ifScreensize', directive);
function directive(ngIfDirective, $window) {
var ngIf = ngIfDirective[0];
return {
controller: 'IfScreensizeController',
prority: 1,
scope: {
options: '=ifScreensize'
},
link: function (scope, element, attrs, controller) {
scope.$watch('options', function (options) {
controller.handle(element, options, ngIf);
});
var window = angular.element($window)
window.bind('resize', function () {
$timeout(function () {
controller.handle(element, scope.options, ngIf);
}, 500);
});
}
};
};
})();
And then the controller looks like this:
(function () {
'use strict';
angular.module('sapphire.directives').controller('IfScreensizeController', controller);
function controller(ifScreensizeService) {
this.handle = ifScreensizeService.handle;
};
})();
And finally, the service looks like this:
(function () {
'use strict';
angular.module('sapphire.directives').service('ifScreensizeService', service);
function service($window) {
return {
handle: handle
};
//////////////////////////////////////////////////
function handle(element, options, ngIf) {
var window = angular.element($window),
width = $window.innerWidth,
value = true;
switch (options.operator) {
case '>':
value = options.width >= width;
break;
case '>=':
value = options.width > width;
break;
case '<':
value = options.width < width;
break;
case '<=':
value = options.width <= width;
break;
default:
break;
}
ngIf.link.apply(ngIf, value);
};
};
})();
The problem is, when I try to use the directive I get an error:
TypeError: CreateListFromArrayLike called on non-object
Which is on the ngIf.link.apply(ngIf, value); line.
Can someone tell me what i need to do to get the directive to work?
.apply takes an array. Try calling it this way ngIf.link.apply(ngIf, [value]);
Ok, so I used the actual ng-if directive code to create my directive.
So, I changed the directive to this:
angular.module('sapphire.directives').directive('ifScreensize', directive);
function directive($timeout, $window) {
return {
controller: 'IfScreensizeController',
multiElement: true,
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
bindToController: {
options: '=ifScreensize'
},
link: function (scope, element, attrs, controller, transclude) {
scope.$watch('options', function (options) {
controller.handle(element, attrs, transclude);
});
var window = angular.element($window)
window.bind('resize', function () {
$timeout(function () {
controller.handle(element, attrs, transclude);
}, 500);
});
}
};
};
And I changed the controller to mimic the ng-if directive like this:
angular.module('sapphire.directives').controller('IfScreensizeController', controller);
function controller($animate, $compile, ifScreensizeService) {
var self = this;
var block, childScope, previousElements;
self.handle = function handle($element, $attr, $transclude) {
var value = ifScreensizeService.evaulate(self.options);
console.log(value);
if (value) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (previousElements) {
console.log(previousElements);
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = ifScreensizeService.getBlockNodes(block.clone);
$animate.leave(previousElements).done(function(response) {
if (response !== false) previousElements = null;
});
block = null;
}
}
};
};
The bulk of the code there, is found in the ng-if directive. I just modified it slightly to work with my directive.
One thing to notice is that in the original ng-if directive, it invokes getBlockNodes which we do not have access to, so I added this to my service:
angular.module('sapphire.directives').service('ifScreensizeService', service);
function service($window) {
var slice = [].slice;
return {
evaulate: evaulate,
getBlockNodes: getBlockNodes
};
//////////////////////////////////////////////////
function evaulate(options) {
var window = angular.element($window),
width = $window.innerWidth,
value = true;
switch (options.operator) {
case '>':
value = width >= options.width;
break;
case '>=':
value = width > options.width;
break;
case '<':
value = width < options.width;
break;
case '<=':
value = width <= options.width;
break;
default:
break;
}
console.log(options, width, value);
return value;
};
function getBlockNodes(nodes) {
// TODO(perf): update `nodes` instead of creating a new object?
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes;
for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
console.log(nodes);
blockNodes = angular.element(slice.call(nodes, 0, i));
}
blockNodes.push(node);
}
}
return blockNodes || nodes;
};
};
And the last caveat here was this line:
blockNodes = angular.element(slice.call(nodes, 0, i));
In the original ng-if, it is actually:
blockNodes = jqLite(slice.call(nodes, 0, i));
I struggled to get this to work, but basically the method jqLite is actually doing an angular.element() call. The slice method won't work unless you do var slice = [].slice; which I have done at the top of the service.
I hope this helps someone else :)
The Problem
Unable to update Model;
Using the set function I can change the ngModel value, and through the pipeline function $parsers, however the Del function uses splice to process the array, but it can not pass the pipeline function and realizes the update model value.
I tried to use $scope.$apply () to execute after ngModel.$setViewValue ($scope.images) and still can't be solved.
The version of the angular used is 1.6.6.
Online code, online code links, I hope you can help me to see where the problem is in the end.
View code
<div ng-controller="appController">
<div image-uploads ng-model="files"></div>
<p style="display: block; color: red">{{files}}</p>
</div>
Javascript code
var app = angular.module('app', []);
app.controller('appController', function ($scope) {
$scope.files = '1,2,3,4';
});
app.directive('imageUploads', function () {
return {
require: '?^ngModel',
restrict: 'EA',
template: '<div class="image-upload-box"><p class="image_upload" ng-repeat="image in images track by $index"><button ng-click="set()">setModel</button><button ng-click="del($index)">{{image}}</button></p></div>',
link: function ($scope, element, attrs, ngModel) {
ngModel.$formatters.push(function (modelValue) {
var images = new Array();
if (!ngModel.$isEmpty(modelValue)) {
var values = modelValue.split(",");
for (var j = 0; j < values.length; j++) {
images.push({
'id': values[j]
});
}
}
return images;
});
ngModel.$parsers.push(function (viewValue) {
var s = "";
for (var j = 0; j < viewValue.length; j++) {
if (viewValue[j].id != null) {
if (j > 0) {
s += ",";
}
s += viewValue[j].id;
}
}
return s;
});
ngModel.$render = function () {
$scope.images = ngModel.$viewValue;
};
$scope.del = function (i) {
$scope.images.splice(i, 1);
ngModel.$setViewValue($scope.images);
};
$scope.set = function () {
console.log('set');
$scope.images = [{id: 5}, {id: 6}, {id: 7}];
ngModel.$setViewValue($scope.images);
}
}
};
});
I have some anchor tags where I'm using an angular directive to decorate (underline) the text(to indicate a keyboard shortcut). So far my code only works if the specified character (for "amt-alt-key") is at the beginning of the first word.
What I need to do is search the whole string and underline the first occurrence of the specified character. So right now if I specified an amt-alt-key="A" in the example below it would work fine as is. However, the problem is the first occurrence could be anywhere in the anchor text. Any help with writing the correct JavaScript would be greatly appreciated.
--Jason
In my html
Agent Data
In my angular code
app.directive("amtAltKey", function () {
return {
link: function (scope, elem, attrs) {
var altKey = attrs.amtAltKey.toUpperCase();
var text = el.innerText;
var textUpper = text.toUpperCase();
var indexOfKey = textUpper.indexOf(altKey);
var newText = text.substring(0, indexOfKey);
newText += '<u>' + text.substring(indexOfKey, 1) + '</u>';
if (indexOfKey + 1 < text.length) { newText += text.substring(indexOfKey + 1); }
el.innerHTML = newText;
keyListeners[altKey] = el;
}
};
});
You can use Regular expression to check the required pattern and a replaceText utility function to replace the matched pattern and once you have the text, replace the existing content of the element as below:
.directive('amtAltKey', function () {
return {
link: function (scope, elem, attrs) {
var altKey = new RegExp(attrs.amtAltKey, 'i');
var text = elem.text();
function replaceText (txt) {
function underline(match) {
return '<u>' + match +'</u>';
}
return txt.replace(altKey, underline);
}
var newText = replaceText(text);
elem.html(newText);
}
};
});
Here is a working example: https://jsbin.com/zefodo/2/edit?html,js,console,output
Use this:
app.directive("amtAltKey", function () {
return {
link: function (scope, elem, attrs) {
var altKey = attrs.amtAltKey;
var text = elem.innerText;
elem.innerHTML = text.replace(new RegExp(altKey, 'i'), '<u>$&</u>');
keyListeners[altKey] = elem;
}
};
});
Try this
app.directive("amtAltKey", function() {
return {
link: function(scope, elem, attrs) {
var el = elem[0];
var altKey = "" + attrs.amtAltKey.toUpperCase();
var text = el.innerText;
var textUpper = text.toUpperCase();
var indexOfKey = textUpper.indexOf(altKey);
if (indexOfKey > -1) {
var newText = text.substr(0, indexOfKey);
newText += '<u>' + text.substr(indexOfKey, 1) + '</u>';
if (indexOfKey + 1 < text.length) {
newText += text.substr(indexOfKey + 1);
}
el.innerHTML = newText;
keyListeners[altKey] = el;
}
}
};
});
Below is my directive, which is an utility (reusable).
var ChartNavigationDirective = { 'ChartNavigation': function (Items) {
return {
restrict: 'EA',
require: '?ngModel',
replace: false,
template: '<div class="chart-nav">' +
' </div>',
scope: {
options: '=ChartNavigation'
},
link: function (scope, element, attrs) {
scope.parameters = {
"prev_arrow": "#prev-arrow",
"next_arrow": "#next-arrow"
};
Items.getItems(scope.options.Source, scope.options.Type).then(function (d) {
scope.links = d.Items;
for (var link in scope.links) {
scope.links[link].Id = link;
}
var chartList = scope.links;
setNavigation(chartList);
});
var setNavigation = function (chartList) {
scope.totalCharts = chartList.length;
if (scope.totalCharts <= 0) {
$(scope.parameters.next_arrow).removeClass("on").addClass("off");
$(scope.parameters.prev_arrow).removeClass("on").addClass("off");
}
if (scope.totalCharts > 0) {
scope.currentItem = scope.links[0];
scope.currentIndex = Number(scope.currentItem.Id) + 1;
$(scope.parameters.prev_arrow).removeClass("off").addClass("on");
$(scope.parameters.next_arrow).removeClass("off").addClass("on");
}
updateNavigation();
};
var updateNavigation = function () {
if (scope.currentIndex <= 1) {
$(scope.parameters.prev_arrow).removeClass("on").addClass("off");
}
else {
$(scope.parameters.prev_arrow).removeClass("off").addClass("on");
}
if (scope.currentIndex >= scope.totalCharts) {
$(scope.parameters.next_arrow).removeClass("on").addClass("off");
}
else {
$(scope.parameters.next_arrow).removeClass("off").addClass("on");
}
};
scope.Previous = function () {
var currentIdx = scope.currentIndex;
var previousIdx = currentIdx - 1;
if (previousIdx >= 0) {
scope.currentItem = scope.links[previousIdx - 1];
scope.currentIndex = Number(scope.currentItem.Id) + 1;
}
updateNavigation();
};
scope.Next = function () {
var currentIdx = scope.currentIndex;
var nextIdx = currentIdx + 1;
if (nextIdx <= scope.totalCharts) {
scope.currentItem = scope.links[nextIdx - 1];
scope.currentIndex = Number(scope.currentItem.Id) + 1; ;
}
updateNavigation();
};
}
};
}
};
I would like to watch scope.currentItem from my controller. I did try using broadcast it's working fine. But I would like use watch instead. Here is my controller.
var myController = function ($scope) {
$scope.templateUrl = '/_layouts/AngularControls/myController/Views/Charts.html';
$scope.currentConfig = '';
// $rootScope.$on('curentConfig', function (event, args) {
// $scope.currentConfig = args.data;
// });
$scope.$watch("currentItem", function () {
alert(JSON.stringify($scope.currentItem));
});
}
Can anyone point out where I am doing mistake ? If there any suggestions Please let me know.
You're trying to watch a directive scope variable from a controller. That won't work because they are two different scopes (you are using an isolate scope in your directive).
You can watch from the directive controller (but I'm not sure that's what you want), or simply pass in the main controller scope variable into the directive, and have the directive manipulate it instead of its own scope variable. I guess using scope.$parent.currentItem is also possible, but definitely not recommended because of the directive reusability.
Broadcasting is also fine, not sure why you don't want to do it.
This is your structure:
controller scope
- directive scope
currentItem
And you're watching for this:
controller scope
watching for currentItem here, it does not exist
- directive scope
currentItem
I figured it my self. Using $rootScope in both directive an controller and watching for the $rootScope variable has resolved the issue.
In Direcive :
$rootScope.currentItem= myItem;
In Controller:
$scope.$watch("currentItem", function () {
$scope.currentConfig = $rootScope.currentItem;
});
I am trying to do the custom star rating with angular.js, where I will have different set of images. I need to change it dynamically on hover the image. I am having 5 images
X X X X X
if I move the mouse pointer to 4th X I should be able to dynamically change
X
I used directive to achieve it.
.directive('fundooRating', function () {
return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" ng-class="star"
ng-click="toggle($index)"><img ng-mouseenter="hoveringOver($index)"
ng-src="{{con}}" />' +
'',
scope: {
ratingValue: '=',
max: '=',
readonly: '#',
onRatingSelected: '&'
},
link: function (scope, elem, attrs) {
var updateStars = function() {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({filled: i < scope.ratingValue});
}
};
scope.con = "Images/Rating/empty.png";
scope.hoveringOver = function(index){
var countIndex = index+1;
scope.Condition = "Good.png"
console.log("Hover " + countIndex);
};
scope.toggle = function(index) {
if (scope.readonly && scope.readonly === 'true') {
return;
}
scope.ratingValue = index + 1;
scope.onRatingSelected({rating: index + 1});
};
scope.$watch('ratingValue', function(oldVal, newVal) {
if (newVal) {
updateStars();
}
});
}
} });
How can I able to find which image my mouse pointer is and how to change the rest of Images. I want to do the custom rating option.
Angular UI gives you premade directives for the same purpose, did you try it?
http://angular-ui.github.io/bootstrap/
Go down to the Rating Title in the same page, i think it might solve your purpose.
You'll need a condition for each star in your updateStars function, either as a property for each, or a separate array. Then, you can do something like this:
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.stars[i].Condition = "Good.png";
}
};
Or the separate array way (assuming the array is scope.conditions):
scope.hoveringOver = function(index){
for (var i = 0; i <= index; i++) {
scope.conditions[i] = "Good.png";
}
};
You also need a function opposite of hoveringOver to remove the states to default/previous versions.