Angularjs directive to replace text - javascript

How would I create a directive in angularjs that for example takes this element:
<div>Example text http://example.com</div>
And convert it in to this
<div>Example text http://example.com</div>
I already have the functionality written to auto link the text in a function and return the html (let's call the function "autoLink" ) but i'm not up to scratch on my directives.
I would also like to add a attribute to the element to pass a object in to the directive. e.g.
<div linkprops="link.props" >Example text http://example.com</div>
Where link.props is object like {a: 'bla bla', b: 'waa waa'} which is to be passed to the autoLink function as a second param (the first been the text).

Two ways of doing it:
Directive
app.directive('parseUrl', function () {
var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?/gi;
return {
restrict: 'A',
require: 'ngModel',
replace: true,
scope: {
props: '=parseUrl',
ngModel: '=ngModel'
},
link: function compile(scope, element, attrs, controller) {
scope.$watch('ngModel', function (value) {
var html = value.replace(urlPattern, '<a target="' + scope.props.target + '" href="$&">$&</a>') + " | " + scope.props.otherProp;
element.html(html);
});
}
};
});
HTML:
<p parse-url="props" ng-model="text"></p>
Filter
app.filter('parseUrlFilter', function () {
var urlPattern = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?/gi;
return function (text, target, otherProp) {
return text.replace(urlPattern, '<a target="' + target + '" href="$&">$&</a>') + " | " + otherProp;
};
});
HTML:
<p ng-bind-html-unsafe="text | parseUrlFilter:'_blank':'otherProperty'"></p>
Note: The 'otherProperty' is just for example, in case you want to pass more properties into the filter.
jsFiddle
Update: Improved replacing algorithm.

To answer the first half of this question, without the additional property requirement, one can use Angular's linky filter: https://docs.angularjs.org/api/ngSanitize/filter/linky

The top voted answer does not work if there are multiple links. Linky already does 90% of the work for us, the only problem is that it sanitizes the html thus removing html/newlines. My solution was to just edit the linky filter (below is Angular 1.2.19) to not sanitize the input.
app.filter('linkyUnsanitized', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+#)\S*[^\s.;,(){}<>]/,
MAILTO_REGEXP = /^mailto:/;
return function(text, target) {
if (!text) return text;
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return html.join('');
function addText(text) {
if (!text) {
return;
}
html.push(text);
}
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
}
html.push('href="');
html.push(url);
html.push('">');
addText(text);
html.push('</a>');
}
};
}]);

I wanted a pause button that swaps text. here is how I did it:
in CSS:
.playpause.paused .pause, .playpause .play { display:none; }
.playpause.paused .play { display:inline; }
in template:
<button class="playpause" ng-class="{paused:paused}" ng-click="paused = !paused">
<span class="play">play</span><span class="pause">pause</span>
</button>

Inspired by #Neal I made this "no sanitize" filter from the newer Angular 1.5.8. It also recognizes addresses without ftp|http(s) but starting with www. This means that both https://google.com and www.google.com will be linkyfied.
angular.module('filter.parselinks',[])
.filter('parseLinks', ParseLinks);
function ParseLinks() {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+#)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
MAILTO_REGEXP = /^mailto:/i;
var isDefined = angular.isDefined;
var isFunction = angular.isFunction;
var isObject = angular.isObject;
var isString = angular.isString;
return function(text, target, attributes) {
if (text == null || text === '') return text;
if (typeof text !== 'string') return text;
var attributesFn =
isFunction(attributes) ? attributes :
isObject(attributes) ? function getAttributesObject() {return attributes;} :
function getEmptyAttributesObject() {return {};};
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/www/mailto then assume mailto
if (!match[2] && !match[4]) {
url = (match[3] ? 'http://' : 'mailto:') + url;
}
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return html.join('');
function addText(text) {
if (!text) {
return;
}
html.push(text);
}
function addLink(url, text) {
var key, linkAttributes = attributesFn(url);
html.push('<a ');
for (key in linkAttributes) {
html.push(key + '="' + linkAttributes[key] + '" ');
}
if (isDefined(target) && !('target' in linkAttributes)) {
html.push('target="',
target,
'" ');
}
html.push('href="',
url.replace(/"/g, '"'),
'">');
addText(text);
html.push('</a>');
}
};
}

I would analyze the text in the link function on the directive:
directive("myDirective", function(){
return {
restrict: "A",
link: function(scope, element, attrs){
// use the 'element' to manipulate it's contents...
}
}
});

Related

Trying to underline the first occurence of a character with Javascript/Angular

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;
}
}
};
});

Disable that acceess key sets focus to button

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

JS: Replace a link with another word. Nested quotes + escape codes

Begin with a fresh plain HTML document, and only use HTML and Javascript.
Place on it the hyperlinked word ''food''
Upon clicking ''food'', it should be replaced with ''meat and vegetables''
Upon clicking ''meat'', it should be replaced with ''pork with bacon''
Upon clicking ''vegetables'', it should be replaced with ''carrots plus peas''
Upon clicking ''pork'', it should be replaced with ''tough and chewy''
Upon clicking ''tough'', it should be replaced with ''burnt and salty''
(And so on)
I've been trying to do this as far as I can, but I'm having escapecode problems.
Here is my code:
<span id="food">food</span>
Here it is in action: http://jsfiddle.net/jshflynn/L6r5rrfx/
I'm sorry it's not spaced, but that threw up errors.
Notice that ''alert(2)'' has no delimiting characters around it, I don't know how to make it say alert(''Hello'').
I feel there must be some recursive way to do this, but I'm not sure.
Thanks in advance. Especially so if you can do the full problem.
Here you go, you get the idea: http://jsfiddle.net/8bhd8njh/
function bind(obj, evt, fnc) {
// W3C model
if (obj.addEventListener) {
obj.addEventListener(evt, fnc, !1);
return !0;
}
// Microsoft model
else if (obj.attachEvent) {
return obj.attachEvent('on' + evt, fnc);
}
// Browser don't support W3C or MSFT model, go on with traditional
else {
evt = 'on'+evt;
if(typeof obj[evt] === 'function'){
// Object already has a function on traditional
// Let's wrap it with our own function inside another function
fnc = (function(f1,f2){
return function(){
f1.apply(this,arguments);
f2.apply(this,arguments);
}
})(obj[evt], fnc);
}
obj[evt] = fnc;
return !0;
}
}
String.prototype.supplant = function (a, b) {
return this.replace(/{([^{}]*)}/g, function (c, d) {
return void 0!=a[d]?a[d]:b?'':c
})
};
var data = {
food : '{meat} and {vegetables}',
meat : '{pork} and {beef}',
pork : '{tough} and {chewy}',
tough : '{burnt} and {salty}',
vegetables : '{carrots} and {peas}'
};
var classname = 'game-clickable';
var init = function(obj, data) {
var template = '<span class="{classname}">{text}</span>';
obj.innerHTML = obj.innerHTML.replace(/{([^{}]*)}/g, function(a,b) {
return template.supplant({
classname : data[b] ? classname : '',
text : b
}, !0)
});
var objects = document.getElementsByClassName('game-clickable');
for (var i = 0; i < objects.length; i++) {
bind(objects[i], 'click', (function(o) {
return function() {
if (!data[o.innerHTML]) {
return;
}
var parent = o.parentNode;
var span = document.createElement('SPAN');
span.innerHTML = data[o.innerHTML];
parent.insertBefore(span, o);
parent.removeChild(o);
init(parent, data);
}
})(objects[i]));
}
};
init(document.getElementById('word-game'), data);
.game-clickable {
cursor: pointer;
text-decoration: underline;
}
<div id="word-game">
{food}
</div>
I think you are looking for something like the following:
var replacements = {
"food" : "meat and vegetables",
"meat" : "pork with bacon",
"vegetables" : "carrots plus peas",
"pork" : "tough and chewy",
"tough" : "burnt and salty"
};
function replaceAnchor(a) {
var elementType = "";
var lastElementType = "";
var target = a.innerHTML;
var replacement = replacements[target];
var words = replacement.split(' ');
var newElement = {};
for(var i = 0; i < words.length; i++) {
var word = words[i];
if (replacements[word]) {
elementType = "a";
} else {
elementType = "span";
}
if (elementType === "a" || elementType != lastElementType) {
newElement = document.createElement(elementType);
if (elementType === "a") {
newElement.onclick = function(e) {
replaceAnchor(this);
e.preventDefault();
};
}
a.parentNode.insertBefore(newElement, a);
}
if (elementType == "span") {
newElement.innerHTML = newElement.innerHTML + " " + word + " ";
} else {
newElement.innerHTML += word;
}
lastElementType = elementType;
}
a.parentElement.removeChild(a);
return false;
}
a {
text-decoration : underline;
color : blue;
cursor: pointer;
}
<a onclick="return replaceAnchor(this);">food</a>

Javascript regex breaking on selection of forward slash

Using regex to link #something and #something in a string. In both cases it's working fine. Trying to use same method to link /something, but it breaks everything.
Works for # and #
http://jsbin.com/todomuca/3/edit
Doesn't work for / due to link 44:
http://jsbin.com/todomuca/4/edit
Any idea why it's breaking?
<!DOCTYPE html>
<html ng-app="app">
<head>
<title>Title</title>
</head>
<body>
<p linkify="twitter">some #username and #hashtag and now trying /test</p>
<!-- javascript -->
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
<script>
angular.module('linkify', []);
angular.module('linkify')
.filter('linkify', function () {
'use strict';
function linkify (_str, type) {
if (!_str) {
return;
}
var _text = _str.replace( /(?:https?\:\/\/|www\.)+(?![^\s]*?")([\w.,#?^=%&:\/~+#-]*[\w#?^=%&\/~+#-])?/ig, function(url) {
var wrap = document.createElement('div');
var anch = document.createElement('a');
anch.href = url;
anch.target = "_blank";
anch.innerHTML = url;
wrap.appendChild(anch);
return wrap.innerHTML;
});
// bugfix
if (!_text) {
return '';
}
// Twitter
if (type === 'twitter') {
_text = _text.replace(/(|\s)*#(\w+)/g, '$1#$2');
_text = _text.replace(/(^|\s)*#(\w+)/g, '$1#$2');
_text = _text.replace(/(^|\s)*\/(\w+)/g, '$1#$2');
}
// Github
if (type === 'github') {
_text = _text.replace(/(|\s)*#(\w+)/g, '$1#$2');
}
return _text;
}
//
return function (text, type) {
return linkify(text, type);
};
})
.factory('linkify', ['$filter', function ($filter) {
'use strict';
function _linkifyAsType (type) {
return function (str) {(type, str);
return $filter('linkify')(str, type);
};
}
return {
twitter: _linkifyAsType('twitter'),
github: _linkifyAsType('github'),
normal: _linkifyAsType()
};
}])
.directive('linkify', ['$filter', '$timeout', 'linkify', function ($filter, $timeout, linkify) {
'use strict';
return {
restrict: 'A',
link: function (scope, element, attrs) {
var type = attrs.linkify || 'normal';
$timeout(function () { element.html(linkify[type](element.html())); });
}
};
}]);
var app = angular.module("app", ['linkify']);
</script>
</body>
</html>
You’re running replacements on raw HTML, which, after earlier replacements, include </a>, which matches your /-matching regular expression. You can try matching them all at the same time:
var TWITTER_LINK = /(?:^|\s)([##/])(\w+)/g;
var html = text.replace(TWITTER_LINK, function (match, type, name) {
var url = {
'#': 'https://twitter.com/' + name,
'#': 'https://twitter.com/search?q=%23' + name,
'/': 'https://twitter.com/search?q=' + name
}[type];
return '' + match + '';
});
That’s still not really pleasant, though; I would use the DOM.
var TWITTER_LINK = /(?:^|\s)([##/])(\w+)/g;
var result = document.createDocumentFragment();
var lastIndex = 0;
var match;
while ((match = TWITTER_LINK.exec(text)) {
result.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
lastIndex = match.index;
var type = match[1];
var name = match[2];
var url = {
'#': 'https://twitter.com/' + encodeURIComponent(name),
'#': 'https://twitter.com/search?q=%23' + encodeURIComponent(name),
'/': 'https://twitter.com/search?q=' + encodeURIComponent(name)
}[type];
var link = document.createElement('a');
link.href = url;
link.appendChild(document.createTextNode(match[0]));
result.appendChild(link);
}
result.appendChild(document.createTextNode(text.substring(lastIndex)));

Properly update the source Option in bootstrap-typeahead.js

In the following demo, after inserting "Alaska" value,
the source is updated so that the autocomplete is not showing again Alaska value.
var newSource = this.source
.slice(0,pos)
.concat(this.source.slice(pos+1));
this.source = newSource;
Anyway if I remove Alaska from the textarea, the value Alaska should be displayed again in the source.
Any hints how to restore the source data if the user delete the data from the textarea?
My idea is to access the options `source option from
$('.typeahead').on('change', function () { })
Any hints?
P.S.:
I am using jquery and underscore
You should probably rather change your matcher function in order to test over the already selected states :
var tabPresentStates = this.query.split(','),
nbPresentStates = tabPresentStates.length;
for(var iState = 0; iState < nbPresentStates; iState++) {
if(item === tabPresentStates[iState].trim())
return false;
}
See this fiddle.
Instead of changing the source you can use the sorter to exclude the values you've already selected.
http://jsfiddle.net/BwDmM/71/
P.S. I'll probably include your code in next version of Jasny's extended Bootstrap http://jasny.github.com/bootstrap :)
!function(source) {
function extractor(query) {
var result = /([^,]+)$/.exec(query);
if(result && result[1])
return result[1].trim();
return '';
}
$('.typeahead').typeahead({
source: source,
updater: function(item) {
return this.$element.val().replace(/[^,]*$/,'')+item+',';
},
matcher: function (item) {
var tquery = extractor(this.query);
if(!tquery) return false;
return ~item.toLowerCase().indexOf(tquery)
},
highlighter: function (item) {
var query = extractor(this.query).replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
return '<strong>' + match + '</strong>'
})
},
sorter: function(items) {
var beginswith = []
, caseSensitive = []
, caseInsensitive = []
, existing = $.each(this.$element.val().split(','), function(i, val) { return val.trim() })
, item
while (item = items.shift()) {
if ($.inArray(item, existing) >= 0) continue;
if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
else if (~item.indexOf(this.query)) caseSensitive.push(item)
else caseInsensitive.push(item)
}
return beginswith.concat(caseSensitive, caseInsensitive)
}
});
}(["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"]);
Agreed with #SamuelCaillerie's approach to use matcher - this is just using your extractor function. So:
updater: function(item) {
return this.$element.val().replace(/[^,]*$/,'')+item+',';
},
matcher: function (item) {
var previouslySelected = (this.query.indexOf(item) != -1);
var tquery = (previouslySelected ? "" : extractor(this.query));
if(!tquery) return false;
return ~item.toLowerCase().indexOf(tquery)
},

Categories