josephschmitt/Clamp.js

Feature request: unclamp or reclamp

jazbek opened this issue · 6 comments

This project seems like it might be dead, but I thought I'd request a feature.

I'm currently using clamp.js (love it by the way) in a site with responsive design, and I want to clamp some text when the layout changes (i.e. when someone switches from portrait to landscape on iPad, or vice versa). I've accomplished this without a problem.

When someone changes the layout back, however, I want to unclamp their text, but it's actually been deleted. Would be great if clamp could keep track of the original text (in an html data attribute?) so it could be restored or re-clamped at a higher # of lines.

Yeah, clamping text seems a bit useless if you can't unclamp it. In Chrome with the CSS method, the original text is kept, but with the fallback method, it looks like it's entirely getting rid of the rest of the text so you can't even clamp with 999 or auto to unclamp.

👍

//edit:
I temporarily 'solved' this by saving the original text into a custom data-attribute and resetting it right before 'unclamping'

The plugin makes no sense without responsive functionality like jQuery's dot dot dot plugin.

javascript```

/*!

(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;
        truncationHTMLContainer.addEventListener('click', () => {
            element.innerHTML = originalText;
        });
    }

// 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;
            element.appendChild(truncationHTMLContainer);
        }

        //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;

})();



relevant diff

javascript ```

truncationHTMLContainer.addEventListener('click', () => {element.innerHTML = originalText;});



if (truncationHTMLContainer) {

  target.nodeValue = target.nodeValue.replace(opt.truncationChar, ''); |   | target.nodeValue = target.nodeValue.replace(opt.truncationChar, '');
  element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + opt.truncationChar; |   | element.innerHTML = target.nodeValue;
  element.appendChild(truncationHTMLContainer);
  }
}