UNPKG

clamp.ts

Version:

TypeScript fork of clamp.js - Clamps an HTML element by adding ellipsis to it if the content inside is too long.

281 lines (280 loc) 10.2 kB
/*! * Clamp.js ported to TypeScript. * * Original author: Joseph Schmitt http://joe.sh under the WTFPL license * * Ported to TypeScript by Aamir Shah github.com/aamir1995 */ /** * @description Returns the height of an element as an integer (max of scroll/offset/client). * Note: inline elements return 0 for scrollHeight and clientHeight. * @param elem * @returns height in number' * @author github.com/danmana - copied from https://github.com/josephschmitt/Clamp.js/pull/18 */ const getElemHeight = (elem) => { return Math.max(elem.scrollHeight, elem.offsetHeight, elem.clientHeight); }; /******************************************************** * * * UTILITY FUNCTIONS * * * *********************************************************/ /** * @description Return the current style for an element. * @param elem The element to compute. * @param prop The style property. * @returns CSS property values. */ const computeStyle = (elem, prop) => { const win = window; /** * @todo re-enable if needed */ // if (!win.getComputedStyle) { // win.getComputedStyle = (el: Element, pseudo) => { // // this.el = el; // return { // getPropertyValue(prop) { // var re = /(\-([a-z]){1})/g; // if (prop == 'float') // prop = 'styleFloat'; // if (re.test(prop)) { // prop = prop.replace(re, function () { // return arguments[2].toUpperCase(); // }); // } // return el.currentStyle && el.currentStyle[prop] ? el.currentStyle[prop] : null; // } // } // // return get // }; // } return win.getComputedStyle(elem, null).getPropertyValue(prop); }; /** * @description Returns the maximum number of lines of text that should be rendered based * on the current height of the element and the line-height of the text. * @param element any HTML element * @param height element's height * @returns max lines */ const getMaxLines = (element, height) => { const availHeight = height || getElemHeight(element), lineHeight = getLineHeight(element); return Math.max(Math.floor(availHeight / lineHeight), 0); }; /** * @description Returns the maximum height a given element should have based on the line- * height of the text and the given clamp value. * * @param element any HTML element * @param clmp number of clamps * @returns max height */ const getMaxHeight = (element, clmp) => { const lineHeight = getLineHeight(element); return lineHeight * clmp; }; /** * @description Returns the line-height of an element as an integer. * @param elem any HTML Element * @returns line-height of the element */ const getLineHeight = (elem) => { const lh = computeStyle(elem, 'line-height'); if (lh === 'normal') { // Normal line heights vary from browser to browser. The spec recommends // a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff. return parseFloat(computeStyle(elem, 'font-size')) * 1.2; } return parseFloat(lh); }; /** * @description Gets an element's last child. That may be another node or a node's contents. * @param elem any HTML Element * @param options config option * @returns Element's last child. */ const getLastChild = (elem, options) => { //Current element has children, need to go deeper and get last child as a text node if (elem.lastChild.children && elem.lastChild.children.length > 0) { return getLastChild(Array.prototype.slice.call(elem.children).pop(), options); } //This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying else if (!elem.lastChild || !elem.lastChild.nodeValue || elem.lastChild.nodeValue === '' || elem.lastChild.nodeValue === options.truncationChar) { elem.lastChild.parentNode.removeChild(elem.lastChild); return getLastChild(elem, options); } //This is the last child we want, return it else { return elem.lastChild; } }; /** * @description Applies ellipsis to the provided string. * @param elem any HTML element * @param str string that needs to be truncated * @param options config option */ const applyEllipsis = (elem, str, options) => { elem.nodeValue = str + options.truncationChar; }; /** * @description Removes one character at a time from the text until its width or * height is beneath the passed-in max param. * @param target * @param element * @param truncationHTMLContainer * @param maxHeight * @param options * @param config * @returns innerHTML */ const truncate = (target, element, truncationHTMLContainer, maxHeight, options, config = { splitOnChars: options.splitOnChars.slice(0), splitChar: options.splitOnChars.slice(0)[0], chunks: null, lastChunk: null, }) => { if (!target || !maxHeight) { return element.innerHTML; } let nodeValue = target.nodeValue.replace(options.truncationChar, ''); let { splitOnChars, splitChar, chunks, lastChunk } = config; //Grab the next chunks if (!chunks) { //If there are more characters to try, grab the next one if (splitOnChars.length > 0) { splitChar = splitOnChars.shift(); } //No characters to chunk by. Go character-by-character else { splitChar = ''; } chunks = nodeValue.split(splitChar); } //If there are chunks left to remove, remove the last one and see if // the nodeValue fits. if (chunks.length > 1) { lastChunk = chunks.pop(); applyEllipsis(target, chunks.join(splitChar), options); } //No more chunks can be removed using this character else { chunks = null; } // Insert the custom HTML before the truncation character if (truncationHTMLContainer) { target.nodeValue = target.nodeValue.replace(options.truncationChar, ''); element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + options.truncationChar; } // Search produced valid chunks if (chunks) { // It fits if (getElemHeight(element) <= maxHeight) { // There's still more characters to try splitting on, not quite done yet if (splitOnChars.length >= 0 && splitChar !== '') { applyEllipsis(target, chunks.join(splitChar) + splitChar + lastChunk, options); chunks = null; } // Finished! else { return element.innerHTML; } } } // No valid chunks produced else { // No valid chunks even when splitting by letter, time to move // on to the next node if (splitChar === '') { applyEllipsis(target, '', options); target = getLastChild(element, options); // reset vars splitOnChars = options.splitOnChars.slice(0); splitChar = splitOnChars[0]; chunks = null; lastChunk = null; } } // If you get here it means still too big, let's keep truncating if (options.animate) { setTimeout(() => { truncate(target, element, truncationHTMLContainer, maxHeight, options, { splitOnChars, splitChar, chunks, lastChunk, }); }, options.animate === true ? 10 : options.animate); } else { return truncate(target, element, truncationHTMLContainer, maxHeight, options, { splitOnChars, splitChar, chunks, lastChunk }); } // just to suppress TS warning... return element.innerHTML; }; /******************************************************** * * * EXPORTED METHOD * * * *********************************************************/ /** * @description Clamps a text node. * @param element. Element containing the text node to clamp. * @param options. Options to pass to the clamper. */ export function clamp(element, options) { /** * merge default options with provided options (if any). */ options = Object.assign({ clamp: 2, useNativeClamp: true, splitOnChars: ['.', '-', '–', '—', ' '], animate: false, truncationChar: '…' }, options); const sty = element.style; const original = element.innerHTML; const supportsNativeClamp = typeof element.style.webkitLineClamp !== 'undefined'; let clampValue = options.clamp; const isCSSValue = clampValue.indexOf && (clampValue.indexOf('px') > -1 || clampValue.indexOf('em') > -1); let truncationHTMLContainer; if (options.truncationHTML) { truncationHTMLContainer = document.createElement('span'); truncationHTMLContainer.innerHTML = options.truncationHTML; } // CONSTRUCTOR ________________________________________________________________ if (clampValue === 'auto') { clampValue = getMaxLines(element); } else if (isCSSValue) { clampValue = getMaxLines(element, parseInt(clampValue)); } let clamped; if (supportsNativeClamp && options.useNativeClamp) { sty.overflow = 'hidden'; sty.textOverflow = 'ellipsis'; sty.webkitBoxOrient = 'vertical'; sty.display = '-webkit-box'; sty.webkitLineClamp = clampValue; if (isCSSValue) { sty.height = options.clamp; } } else { const height = getMaxHeight(element, clampValue); if (height < getElemHeight(element)) { clamped = truncate(getLastChild(element, options), element, truncationHTMLContainer, height, options); } } return { original, clamped, }; }