/*! * clamp.js 0.5.1 * * copyright 2011-2013, joseph schmitt http://joe.sh * released under the wtfpl license * http://sam.zoy.org/wtfpl/ */ (function(){ /** * clamps a text node. * @param {htmlelement} element. element containing the text node to clamp. * @param {object} options. options to pass to the clamper. */ function clamp(element, options) { options = options || {}; var self = this, win = window, opt = { clamp: options.clamp || 2, usenativeclamp: typeof(options.usenativeclamp) != 'undefined' ? options.usenativeclamp : true, splitonchars: options.splitonchars || ['.', '-', '–', '—', ' '], //split on sentences (periods), hypens, en-dashes, em-dashes, and words (spaces). animate: options.animate || false, truncationchar: options.truncationchar || '…', truncationhtml: options.truncationhtml }, sty = element.style, originaltext = element.innerhtml, supportsnativeclamp = typeof(element.style.webkitlineclamp) != 'undefined', clampvalue = opt.clamp, iscssvalue = clampvalue.indexof && (clampvalue.indexof('px') > -1 || clampvalue.indexof('em') > -1), truncationhtmlcontainer; if (opt.truncationhtml) { truncationhtmlcontainer = document.createelement('span'); truncationhtmlcontainer.innerhtml = opt.truncationhtml; } // utility functions __________________________________________________________ /** * return the current style for an element. * @param {htmlelement} elem the element to compute. * @param {string} prop the style property. * @returns {number} */ function computestyle(elem, prop) { if (!win.getcomputedstyle) { win.getcomputedstyle = function(el, pseudo) { this.el = el; this.getpropertyvalue = function(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 this; } } return win.getcomputedstyle(elem, null).getpropertyvalue(prop); } /** * 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. */ function getmaxlines(height) { var availheight = height || element.clientheight, lineheight = getlineheight(element); return math.max(math.floor(availheight/lineheight), 0); } /** * returns the maximum height a given element should have based on the line- * height of the text and the given clamp value. */ function getmaxheight(clmp) { var lineheight = getlineheight(element); return lineheight * clmp; } /** * returns the line-height of an element as an integer. */ function getlineheight(elem) { var 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. lh = parseint(computestyle(elem, 'font-size')) * 1.2; } return parseint(lh); } // meat and potatoes (mmmm, potatoes...) ______________________________________ var splitonchars = opt.splitonchars.slice(0), splitchar = splitonchars[0], chunks, lastchunk; /** * gets an element's last child. that may be another node or a node's contents. */ function getlastchild(elem) { //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()); } //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 == opt.truncationchar) { elem.lastchild.parentnode.removechild(elem.lastchild); return getlastchild(element); } //this is the last child we want, return it else { return elem.lastchild; } } /** * removes one character at a time from the text until its width or * height is beneath the passed-in max param. */ function truncate(target, maxheight) { if (!maxheight) {return;} /** * resets global variables. */ function reset() { splitonchars = opt.splitonchars.slice(0); splitchar = splitonchars[0]; chunks = null; lastchunk = null; } var nodevalue = target.nodevalue.replace(opt.truncationchar, ''); //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) { // console.log('chunks', chunks); lastchunk = chunks.pop(); // console.log('lastchunk', lastchunk); applyellipsis(target, chunks.join(splitchar)); } //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(opt.truncationchar, ''); element.innerhtml = target.nodevalue + ' ' + truncationhtmlcontainer.innerhtml + opt.truncationchar; } //search produced valid chunks if (chunks) { //it fits if (element.clientheight <= 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); 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, ''); target = getlastchild(element); reset(); } } //if you get here it means still too big, let's keep truncating if (opt.animate) { settimeout(function() { truncate(target, maxheight); }, opt.animate === true ? 10 : opt.animate); } else { return truncate(target, maxheight); } } function applyellipsis(elem, str) { elem.nodevalue = str + opt.truncationchar; } // constructor ________________________________________________________________ if (clampvalue == 'auto') { clampvalue = getmaxlines(); } else if (iscssvalue) { clampvalue = getmaxlines(parseint(clampvalue)); } var clampedtext; if (supportsnativeclamp && opt.usenativeclamp) { sty.overflow = 'hidden'; sty.textoverflow = 'ellipsis'; sty.webkitboxorient = 'vertical'; sty.display = '-webkit-box'; sty.webkitlineclamp = clampvalue; if (iscssvalue) { sty.height = opt.clamp + 'px'; } } else { var height = getmaxheight(clampvalue); if (height <= element.clientheight) { clampedtext = truncate(getlastchild(element), height); } } return { 'original': originaltext, 'clamped': clampedtext } } window.$clamp = clamp; })();