Skip to content

Instantly share code, notes, and snippets.

@egm0121
Created August 12, 2021 15:44
Show Gist options
  • Save egm0121/5877dc10b6fc005ee1123a2930dbf05d to your computer and use it in GitHub Desktop.
Save egm0121/5877dc10b6fc005ee1123a2930dbf05d to your computer and use it in GitHub Desktop.
linkify-content-script for chrome extensions: wraps a page phone numbers into clickable links ( uses XPATH and event delegation for performance)
var linkifyExtension = (function(){
var LINKIFY_EXTENSION_CLASS = 'linkify-phone-match';
var EXCLUDE_NODES = ['script', 'style', 'input', 'select', 'textarea', 'button', 'a', 'code', 'head', 'noscript'];
var PHONE_MATCHERS = {
US : {
reg : /(^|\s)((\+1\d{10})|((\+1[ \.])?\(?\d{3}\)?[ \-\.\/]{1,3}\d{3}[ \-\.]{1,2}\d{4}))(\s|$)|(^|\s)\+(?:[0-9] ?){6,14}[0-9](\s|$)/gm,
match : 2
} ,
PERMISSIVE : {
reg : /(^|\s)(?:\+?(\d{1,3}))?([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)(\s|$)/gm,
match : 0
}
};
var XPATH_TEXT_MATCHER = '//*[not('+EXCLUDE_NODES.map(function(i){ return 'ancestor-or-self::'+i; }).join(' or ')+')]/text()[normalize-space()]';
// http://james.padolsey.com/javascript/replacing-text-in-the-dom-its-not-that-simple/
var generateLinkable = (function(){
var uuid = 0;
return function(nbr){
uuid++;
return ["<a href='javascript:void()' class='"+LINKIFY_EXTENSION_CLASS+"' id='linkify-phone-id-"+uuid+"'>"+nbr+"</a>",uuid];
};
})();
var wrapMatch = function(textNode,nbrArr) {
var temp = document.createElement('div');
var linkables = nbrArr.map(generateLinkable);
var tempHTML = textNode.data;
linkables.map(function(linkable,i){
tempHTML = tempHTML.replace(nbrArr[i],linkable[0]);
});
temp.innerHTML = tempHTML;
while (temp.firstChild) {
textNode.parentNode.insertBefore( temp.firstChild, textNode );
}
// Remove original text-node:
textNode.parentNode.removeChild(textNode);
};
var handleClickToCall = function(e){
console.log('send event to bg page');
chrome.runtime.sendMessage({e:'contentScript.request.call',data:e.target.innerText});
};
var mapTextNodes = function(rootEl,callback){
var textNodes = document.evaluate(XPATH_TEXT_MATCHER, rootEl, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE);
var l = textNodes.snapshotLength;
for(var i = 0; i < l; i++){
var currItem = textNodes.snapshotItem(i);
if(typeof callback === "function")callback(currItem);
};
};
return {
lastLinkRef : false,
isParsing:false,
linkify : function() {
console.time("linkify took");
this.isParsing = true;
console.log('linkify!');
mapTextNodes(document,function(currItem){
if(currItem.parentNode && currItem.parentNode.classList.contains(LINKIFY_EXTENSION_CLASS) ){
console.log('skip it!');
return;
}
var currText = currItem.textContent , matches = false, nbrArr = [];
while(matches = PHONE_MATCHERS.PERMISSIVE.reg.exec(currText)){
var match = matches[PHONE_MATCHERS.PERMISSIVE.match];
digitLength = match.replace(/\D/g, '');
if(digitLength.length > 6){
nbrArr.push( match );
}
}
if(nbrArr.length && nbrArr.filter(function(i){return i;}).length){
console.log('FOUND :',nbrArr);
wrapMatch(currItem,nbrArr);
}
});
this.isParsing = false;
console.timeEnd("linkify took");
},
bindDomEvents : function(){
var that = this;
console.log("XPATH using : "+XPATH_TEXT_MATCHER);
document.addEventListener('DOMNodeInserted', function(){
if(!that.isParsing){
if(that.lastLinkRef)clearTimeout(that.lastLinkRef);
that.lastLinkRef = setTimeout(function(){that.linkify();},10);
}
});
document.querySelector('body').addEventListener("click",function(e){
if(e.target && e.target.classList &&
e.target.classList.contains(LINKIFY_EXTENSION_CLASS)){
handleClickToCall(e);
}
});
},
};
})();
setTimeout(function(){
linkifyExtension.linkify();
},10);
linkifyExtension.bindDomEvents();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment