Skip to content

Instantly share code, notes, and snippets.

@jmahc
Created August 28, 2016 22:19
Show Gist options
  • Select an option

  • Save jmahc/fb5d41d82ad2837253107a3161c6e117 to your computer and use it in GitHub Desktop.

Select an option

Save jmahc/fb5d41d82ad2837253107a3161c6e117 to your computer and use it in GitHub Desktop.
eager.io's JS file for smart-underline
(function() {
var PHI, backgroundPositionYCache, calculateBaselineYRatio, calculateTextHighestY, calculateTypeMetrics, clearCanvas, containerIdAttrName, containsAnyNonInlineElements, containsInvalidElements, countParentContainers, destroy, fontAvailable, getBackgroundColor, getBackgroundColorNode, getFirstAvailableFont, getLinkColor, getUnderlineBackgroundPositionY, hasValidLinkContent, init, initLink, initLinkOnHover, isTransparent, isUnderlined, linkAlwysAttrName, linkBgPosAttrName, linkColorAttrName, linkContainers, linkHoverAttrName, linkLargeAttrName, linkSmallAttrName, performanceTimes, renderStyles, selectionColor, sortContainersForCSSPrecendence, styleNode, time, uniqueLinkContainerID;
window.SmartUnderline = {
init: function() {},
destroy: function() {}
};
if (!(window['getComputedStyle'] && document.documentElement.getAttribute)) {
return;
}
PHI = 1.618034;
selectionColor = '#b4d5fe';
linkColorAttrName = 'data-smart-underline-link-color';
linkSmallAttrName = 'data-smart-underline-link-small';
linkLargeAttrName = 'data-smart-underline-link-large';
linkAlwysAttrName = 'data-smart-underline-link-always';
linkBgPosAttrName = 'data-smart-underline-link-background-position';
linkHoverAttrName = 'data-smart-underline-link-hover';
containerIdAttrName = 'data-smart-underline-container-id';
performanceTimes = [];
time = function() {
return +(new Date);
};
linkContainers = {};
uniqueLinkContainerID = (function() {
var id;
id = 0;
return function() {
return id += 1;
};
})();
clearCanvas = function(canvas, context) {
return context.clearRect(0, 0, canvas.width, canvas.height);
};
calculateTextHighestY = function(text, canvas, context) {
var alpha, highestY, i, j, pixelData, r, ref, ref1, textWidth, x, y;
clearCanvas(canvas, context);
context.fillStyle = 'red';
textWidth = context.measureText(text).width;
context.fillText(text, 0, 0);
highestY = void 0;
for (x = i = 0, ref = textWidth; 0 <= ref ? i <= ref : i >= ref; x = 0 <= ref ? ++i : --i) {
for (y = j = 0, ref1 = canvas.height; 0 <= ref1 ? j <= ref1 : j >= ref1; y = 0 <= ref1 ? ++j : --j) {
pixelData = context.getImageData(x, y, x + 1, y + 1);
r = pixelData.data[0];
alpha = pixelData.data[3];
if (r === 255 && alpha > 50) {
if (!highestY) {
highestY = y;
}
if (y > highestY) {
highestY = y;
}
}
}
}
clearCanvas(canvas, context);
return highestY;
};
calculateTypeMetrics = function(computedStyle) {
var baselineY, canvas, context, descenderHeight, gLowestPixel;
canvas = document.createElement('canvas');
context = canvas.getContext('2d');
canvas.height = canvas.width = 2 * parseInt(computedStyle.fontSize, 10);
context.textBaseline = 'top';
context.textAlign = 'start';
context.fontStretch = 1;
context.angle = 0;
context.font = computedStyle.fontVariant + " " + computedStyle.fontStyle + " " + computedStyle.fontWeight + " " + computedStyle.fontSize + "/" + computedStyle.lineHeight + " " + computedStyle.fontFamily;
baselineY = calculateTextHighestY('I', canvas, context);
gLowestPixel = calculateTextHighestY('g', canvas, context);
descenderHeight = gLowestPixel - baselineY;
return {
baselineY: baselineY,
descenderHeight: descenderHeight
};
};
calculateBaselineYRatio = function(node) {
var baselinePositionY, baselineYRatio, height, large, largeRect, small, smallRect, test;
test = document.createElement('div');
test.style.display = 'block';
test.style.position = 'absolute';
test.style.bottom = 0;
test.style.right = 0;
test.style.width = 0;
test.style.height = 0;
test.style.margin = 0;
test.style.padding = 0;
test.style.visibility = 'hidden';
test.style.overflow = 'hidden';
test.style.wordWrap = 'normal';
test.style.whiteSpace = 'nowrap';
small = document.createElement('span');
large = document.createElement('span');
small.style.display = 'inline';
large.style.display = 'inline';
small.style.fontSize = '0px';
large.style.fontSize = '2000px';
small.innerHTML = 'X';
large.innerHTML = 'X';
test.appendChild(small);
test.appendChild(large);
node.appendChild(test);
smallRect = small.getBoundingClientRect();
largeRect = large.getBoundingClientRect();
node.removeChild(test);
baselinePositionY = smallRect.top - largeRect.top;
height = largeRect.height;
return baselineYRatio = Math.abs(baselinePositionY / height);
};
backgroundPositionYCache = {};
getFirstAvailableFont = function(fontFamily) {
var font, fonts, i, len;
fonts = fontFamily.split(',');
for (i = 0, len = fonts.length; i < len; i++) {
font = fonts[i];
if (fontAvailable(font)) {
return font;
}
}
return false;
};
fontAvailable = function(font) {
var baselineSize, canvas, context, newSize, text;
canvas = document.createElement('canvas');
context = canvas.getContext('2d');
text = 'abcdefghijklmnopqrstuvwxyz0123456789';
context.font = '72px monospace';
baselineSize = context.measureText(text).width;
context.font = "72px " + font + ", monospace";
newSize = context.measureText(text).width;
if (newSize === baselineSize) {
return false;
}
return true;
};
getUnderlineBackgroundPositionY = function(node) {
var adjustment, backgroundPositionY, backgroundPositionYPercent, baselineY, baselineYRatio, cache, cacheKey, clientRects, computedStyle, descenderHeight, descenderY, firstAvailableFont, fontSizeInt, minimumCloseness, ref, textHeight;
computedStyle = getComputedStyle(node);
firstAvailableFont = getFirstAvailableFont(computedStyle.fontFamily);
if (!firstAvailableFont) {
cacheKey = "" + (Math.random());
} else {
cacheKey = "font:" + firstAvailableFont + "size:" + computedStyle.fontSize + "weight:" + computedStyle.fontWeight;
}
cache = backgroundPositionYCache[cacheKey];
if (cache) {
return cache;
}
ref = calculateTypeMetrics(computedStyle), baselineY = ref.baselineY, descenderHeight = ref.descenderHeight;
clientRects = node.getClientRects();
if (!(clientRects != null ? clientRects.length : void 0)) {
return;
}
adjustment = 1;
textHeight = clientRects[0].height - adjustment;
if (-1 < navigator.userAgent.toLowerCase().indexOf('firefox')) {
adjustment = .98;
baselineYRatio = calculateBaselineYRatio(node);
baselineY = baselineYRatio * textHeight * adjustment;
}
descenderY = baselineY + descenderHeight;
fontSizeInt = parseInt(computedStyle.fontSize, 10);
minimumCloseness = 3;
backgroundPositionY = baselineY + Math.max(minimumCloseness, descenderHeight / PHI);
if (descenderHeight === 4) {
backgroundPositionY = descenderY - 1;
}
if (descenderHeight === 3) {
backgroundPositionY = descenderY;
}
backgroundPositionYPercent = Math.round(100 * backgroundPositionY / textHeight);
if (descenderHeight > 2 && fontSizeInt > 10 && backgroundPositionYPercent <= 100) {
backgroundPositionYCache[cacheKey] = backgroundPositionYPercent;
return backgroundPositionYPercent;
}
};
isTransparent = function(color) {
var alpha, rgbaAlphaMatch;
if (color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
return true;
}
rgbaAlphaMatch = color.match(/^rgba\(.*,(.+)\)/i);
if ((rgbaAlphaMatch != null ? rgbaAlphaMatch.length : void 0) === 2) {
alpha = parseFloat(rgbaAlphaMatch[1]);
if (alpha < .0001) {
return true;
}
}
return false;
};
getBackgroundColorNode = function(node) {
var backgroundColor, computedStyle, parentNode, reachedRootNode;
computedStyle = getComputedStyle(node);
backgroundColor = computedStyle.backgroundColor;
parentNode = node.parentNode;
reachedRootNode = !parentNode || parentNode === document.documentElement || parentNode === node;
if (computedStyle.backgroundImage !== 'none') {
return null;
}
if (isTransparent(backgroundColor)) {
if (reachedRootNode) {
return node.parentNode || node;
} else {
return getBackgroundColorNode(parentNode);
}
} else {
return node;
}
};
hasValidLinkContent = function(node) {
return containsInvalidElements(node) || containsAnyNonInlineElements(node);
};
containsInvalidElements = function(node) {
var child, i, len, ref, ref1, ref2;
ref = node.children;
for (i = 0, len = ref.length; i < len; i++) {
child = ref[i];
if ((ref1 = (ref2 = child.tagName) != null ? ref2.toLowerCase() : void 0) === 'img' || ref1 === 'video' || ref1 === 'canvas' || ref1 === 'embed' || ref1 === 'object' || ref1 === 'iframe') {
return true;
}
return containsInvalidElements(child);
}
return false;
};
containsAnyNonInlineElements = function(node) {
var child, i, len, ref, style;
ref = node.children;
for (i = 0, len = ref.length; i < len; i++) {
child = ref[i];
style = getComputedStyle(child);
if (style.display !== 'inline') {
return true;
}
return containsAnyNonInlineElements(child);
}
return false;
};
getBackgroundColor = function(node) {
var backgroundColor;
backgroundColor = getComputedStyle(node).backgroundColor;
if (node === document.documentElement && isTransparent(backgroundColor)) {
return 'rgb(255, 255, 255)';
} else {
return backgroundColor;
}
};
getLinkColor = function(node) {
return getComputedStyle(node).color;
};
styleNode = document.createElement('style');
countParentContainers = function(node, count) {
var parentNode, reachedRootNode;
if (count == null) {
count = 0;
}
parentNode = node.parentNode;
reachedRootNode = !parentNode || parentNode === document || parentNode === node;
if (reachedRootNode) {
return count;
} else {
if (parentNode.hasAttribute(containerIdAttrName)) {
count += 1;
}
return count + countParentContainers(parentNode);
}
};
sortContainersForCSSPrecendence = function(containers) {
var container, id, sorted;
sorted = [];
for (id in containers) {
container = containers[id];
container.depth = countParentContainers(container.container);
sorted.push(container);
}
sorted.sort(function(a, b) {
if (a.depth < b.depth) {
return -1;
}
if (a.depth > b.depth) {
return 1;
}
return 0;
});
return sorted;
};
isUnderlined = function(style) {
var i, len, property, ref, ref1;
ref = ['textDecorationLine', 'textDecoration'];
for (i = 0, len = ref.length; i < len; i++) {
property = ref[i];
if ((ref1 = style[property]) != null ? ref1.match(/\bunderline\b/) : void 0) {
return true;
}
}
return false;
};
initLink = function(link) {
var backgroundPositionY, container, fontSize, id, style;
style = getComputedStyle(link);
fontSize = parseFloat(style.fontSize);
if (isUnderlined(style) && style.display === 'inline' && fontSize >= 10 && !hasValidLinkContent(link)) {
container = getBackgroundColorNode(link);
if (container) {
backgroundPositionY = getUnderlineBackgroundPositionY(link);
if (backgroundPositionY) {
link.setAttribute(linkColorAttrName, getLinkColor(link));
link.setAttribute(linkBgPosAttrName, backgroundPositionY);
id = container.getAttribute(containerIdAttrName);
if (id) {
linkContainers[id].links.push(link);
} else {
id = uniqueLinkContainerID();
container.setAttribute(containerIdAttrName, id);
linkContainers[id] = {
id: id,
container: container,
links: [link]
};
}
return true;
}
}
}
return false;
};
renderStyles = function() {
var backgroundColor, backgroundPositionY, color, container, containersWithPrecedence, i, j, len, len1, link, linkBackgroundPositionYs, linkColors, linkSelector, ref, styles;
styles = '';
containersWithPrecedence = sortContainersForCSSPrecendence(linkContainers);
linkBackgroundPositionYs = {};
for (i = 0, len = containersWithPrecedence.length; i < len; i++) {
container = containersWithPrecedence[i];
linkColors = {};
ref = container.links;
for (j = 0, len1 = ref.length; j < len1; j++) {
link = ref[j];
linkColors[getLinkColor(link)] = true;
linkBackgroundPositionYs[getUnderlineBackgroundPositionY(link)] = true;
}
backgroundColor = getBackgroundColor(container.container);
for (color in linkColors) {
linkSelector = function(modifier) {
if (modifier == null) {
modifier = '';
}
return "[" + containerIdAttrName + "=\"" + container.id + "\"] a[" + linkColorAttrName + "=\"" + color + "\"][" + linkAlwysAttrName + "]" + modifier + ",\n[" + containerIdAttrName + "=\"" + container.id + "\"] a[" + linkColorAttrName + "=\"" + color + "\"][" + linkHoverAttrName + "]" + modifier + ":hover";
};
styles += (linkSelector()) + ", " + (linkSelector(':visited')) + " {\n color: " + color + ";\n text-decoration: none !important;\n text-shadow: 0.03em 0 " + backgroundColor + ", -0.03em 0 " + backgroundColor + ", 0 0.03em " + backgroundColor + ", 0 -0.03em " + backgroundColor + ", 0.06em 0 " + backgroundColor + ", -0.06em 0 " + backgroundColor + ", 0.09em 0 " + backgroundColor + ", -0.09em 0 " + backgroundColor + ", 0.12em 0 " + backgroundColor + ", -0.12em 0 " + backgroundColor + ", 0.15em 0 " + backgroundColor + ", -0.15em 0 " + backgroundColor + ";\n background-color: transparent;\n background-image: -webkit-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -webkit-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -webkit-linear-gradient(" + color + ", " + color + ");\n background-image: -moz-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -moz-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -moz-linear-gradient(" + color + ", " + color + ");\n background-image: -o-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -o-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -o-linear-gradient(" + color + ", " + color + ");\n background-image: -ms-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -ms-linear-gradient(" + backgroundColor + ", " + backgroundColor + "), -ms-linear-gradient(" + color + ", " + color + ");\n background-image: linear-gradient(" + backgroundColor + ", " + backgroundColor + "), linear-gradient(" + backgroundColor + ", " + backgroundColor + "), linear-gradient(" + color + ", " + color + ");\n -webkit-background-size: 0.05em 1px, 0.05em 1px, 1px 1px;\n -moz-background-size: 0.05em 1px, 0.05em 1px, 1px 1px;\n background-size: 0.05em 1px, 0.05em 1px, 1px 1px;\n background-repeat: no-repeat, no-repeat, repeat-x;\n}\n\n" + (linkSelector('::selection')) + " {\n text-shadow: 0.03em 0 " + selectionColor + ", -0.03em 0 " + selectionColor + ", 0 0.03em " + selectionColor + ", 0 -0.03em " + selectionColor + ", 0.06em 0 " + selectionColor + ", -0.06em 0 " + selectionColor + ", 0.09em 0 " + selectionColor + ", -0.09em 0 " + selectionColor + ", 0.12em 0 " + selectionColor + ", -0.12em 0 " + selectionColor + ", 0.15em 0 " + selectionColor + ", -0.15em 0 " + selectionColor + ";\n background: " + selectionColor + ";\n}\n\n" + (linkSelector('::-moz-selection')) + " {\n text-shadow: 0.03em 0 " + selectionColor + ", -0.03em 0 " + selectionColor + ", 0 0.03em " + selectionColor + ", 0 -0.03em " + selectionColor + ", 0.06em 0 " + selectionColor + ", -0.06em 0 " + selectionColor + ", 0.09em 0 " + selectionColor + ", -0.09em 0 " + selectionColor + ", 0.12em 0 " + selectionColor + ", -0.12em 0 " + selectionColor + ", 0.15em 0 " + selectionColor + ", -0.15em 0 " + selectionColor + ";\n background: " + selectionColor + ";\n}";
}
}
for (backgroundPositionY in linkBackgroundPositionYs) {
styles += "a[" + linkBgPosAttrName + "=\"" + backgroundPositionY + "\"] {\n background-position: 0% " + backgroundPositionY + "%, 100% " + backgroundPositionY + "%, 0% " + backgroundPositionY + "%;\n}";
}
return styleNode.innerHTML = styles;
};
initLinkOnHover = function() {
var alreadyMadeSmart, link, madeSmart;
link = this;
alreadyMadeSmart = link.hasAttribute(linkHoverAttrName);
if (!alreadyMadeSmart) {
madeSmart = initLink(link);
if (madeSmart) {
link.setAttribute(linkHoverAttrName, '');
return renderStyles();
}
}
};
init = function(options) {
var i, len, link, links, madeSmart, startTime;
startTime = time();
links = document.querySelectorAll((options.location ? options.location + ' ' : '') + "a");
if (!links.length) {
return;
}
linkContainers = {};
for (i = 0, len = links.length; i < len; i++) {
link = links[i];
madeSmart = initLink(link);
if (madeSmart) {
link.setAttribute(linkAlwysAttrName, '');
} else {
link.removeEventListener('mouseover', initLinkOnHover);
link.addEventListener('mouseover', initLinkOnHover);
}
}
renderStyles();
document.body.appendChild(styleNode);
return performanceTimes.push(time() - startTime);
};
destroy = function() {
var attribute, i, len, ref, ref1, results;
if ((ref = styleNode.parentNode) != null) {
ref.removeChild(styleNode);
}
Array.prototype.forEach.call(document.querySelectorAll("[" + linkHoverAttrName + "]"), function(node) {
return node.removeEventListener(initLinkOnHover);
});
ref1 = [linkColorAttrName, linkSmallAttrName, linkLargeAttrName, linkAlwysAttrName, linkHoverAttrName, containerIdAttrName];
results = [];
for (i = 0, len = ref1.length; i < len; i++) {
attribute = ref1[i];
results.push(Array.prototype.forEach.call(document.querySelectorAll("[" + attribute + "]"), function(node) {
return node.removeAttribute(attribute);
}));
}
return results;
};
window.SmartUnderline = {
init: function(options) {
if (options == null) {
options = {};
}
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', function() {
return init(options);
});
return window.addEventListener('load', function() {
destroy();
return init(options);
});
} else {
destroy();
return init(options);
}
},
destroy: function() {
return destroy();
},
performanceTimes: function() {
return performanceTimes;
}
};
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment