jtclampy
Version:
Changing clampy.js to support multiple instructions using different parameters
429 lines (383 loc) • 14.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global['vue-clampy'] = factory());
}(this, (function () { 'use strict';
var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
var clampy_umd = createCommonjsModule(function (module, exports) {
(function (global, factory) {
factory(exports);
}(commonjsGlobal, (function (exports) { 'use strict';
var ClampOptions = /** @class */ (function () {
function ClampOptions(clamp, truncationChar, truncationHTML, splitOnChars) {
this.clamp = clamp || "auto";
this.truncationChar = truncationChar || "…";
this.truncationHTML = truncationHTML;
this.splitOnChars = splitOnChars || [".", "-", "–", "—", " "];
}
return ClampOptions;
}());
var ClampResponse = /** @class */ (function () {
function ClampResponse(original, clamped) {
this.original = original;
this.clamped = clamped;
}
return ClampResponse;
}());
/**
* Clamps (ie. cuts off) an HTML element's content by adding ellipsis to it if the content inside is too long.
*
* @export
* @param {HTMLElement} element The HTMLElement that should be clamped.
* @param {ClampOptions} [options] The Clamp options
* @returns {ClampResponse} The Clamp response
*/
function clamp(element, options) {
var win = window;
if (!options) {
options = {
clamp: "auto",
truncationChar: "…",
splitOnChars: [".", "-", "–", "—", " "]
};
}
var opt = {
clamp: options.clamp || "auto",
splitOnChars: options.splitOnChars || [".", "-", "–", "—", " "],
truncationChar: options.truncationChar || "…",
truncationHTML: options.truncationHTML
};
var splitOnChars = opt.splitOnChars.slice(0);
var splitChar = splitOnChars[0];
var chunks;
var lastChunk;
var sty = element.style;
var originalText = element.innerHTML;
var clampValue = opt.clamp;
var isCSSValue = clampValue.indexOf && (clampValue.indexOf("px") > -1 || clampValue.indexOf("em") > -1);
var 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) {
return win.getComputedStyle(elem).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;
var 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 = parseFloat(parseFloat(computeStyle(elem, "font-size")).toFixed(0)) * 1.1;
}
return parseFloat(parseFloat(lh).toFixed(0));
}
/**
* Returns the height of an element as an integer (max of scroll/offset/client).
* Note: inline elements return 0 for scrollHeight and clientHeight
*/
function getElemHeight(elem) {
// The '- 4' is a hack to deal with the element height when the browser(especially IE) zoom level is not 100%.
// It also doesn't impact clamping when the browser zoom level is 100%.
return Math.max(elem.scrollHeight, elem.clientHeight) - 4;
}
/**
* Gets an element's last child. That may be another node or a node's contents.
*/
function getLastChild(elem) {
if (!elem.lastChild) {
return;
}
// 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) {
if (!elem.lastChild.nodeValue) {
// Check for void/empty element (such as <br> tag) or if it's the ellipsis and remove it.
if ((elem.lastChild.firstChild === null ||
elem.lastChild.firstChild.nodeValue === opt.truncationChar) &&
elem.lastChild.parentNode) {
elem.lastChild.parentNode.removeChild(elem.lastChild);
// Check if the element has no more children and remove it if it's the case.
// This can happen for instance with lists (i.e. <ul> and <ol>) with no items.
if ((!elem.children || elem.children.length === 0) && elem.parentNode) {
elem.parentNode.removeChild(elem);
return getLastChild(element);
}
}
// Check if it's a text node
if (elem.lastChild.nodeType === 3) {
return elem.lastChild;
}
else {
return getLastChild(elem.lastChild);
}
}
if (elem.lastChild &&
elem.lastChild.parentNode &&
elem.lastChild.nodeValue === opt.truncationChar) {
elem.lastChild.parentNode.removeChild(elem.lastChild);
}
else {
return elem;
}
return getLastChild(element);
}
// This is the last child we want, return it
else {
return elem.lastChild;
}
}
/**
* Apply the ellipsis to the element
* @param elem the element to apply the ellipsis on
* @param str The string that will be set to the element
*/
function applyEllipsis(elem, str) {
elem.nodeValue = str + opt.truncationChar;
}
/**
* 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) {
/**
* Resets global variables.
*/
function reset() {
splitOnChars = opt.splitOnChars.slice(0);
splitChar = splitOnChars[0];
chunks = null;
lastChunk = null;
}
if (!target || !maxHeight || !target.nodeValue) {
return;
}
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();
}
else {
// No characters to chunk by. Go character-by-character
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));
}
else {
// No more chunks can be removed using this character
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;
}
else {
// Finished!
return element.innerHTML;
}
}
}
else {
// No valid chunks produced
// No valid chunks even when splitting by letter, time to move
// on to the next node
if (splitChar === "") {
applyEllipsis(target, "");
target = getLastChild(element);
reset();
}
}
return truncate(target, maxHeight);
}
// CONSTRUCTOR ________________________________________________________________
if (clampValue === "auto") {
clampValue = getMaxLines().toString();
}
else if (isCSSValue) {
clampValue = getMaxLines(parseInt(clampValue, 10)).toString();
}
var clampedText;
var height = getMaxHeight(Number(clampValue));
if (height < getElemHeight(element)) {
clampedText = truncate(getLastChild(element), height);
}
return new ClampResponse(originalText, clampedText);
}
exports.ClampOptions = ClampOptions;
exports.ClampResponse = ClampResponse;
exports.clamp = clamp;
Object.defineProperty(exports, '__esModule', { value: true });
})));
});
var clampy_umd$1 = unwrapExports(clampy_umd);
var clampy_ = Object.freeze({
default: clampy_umd$1,
__moduleExports: clampy_umd
});
// import * as elementResizeDetectorMaker_ from 'element-resize-detector';
// https://github.com/rollup/rollup/issues/670#issuecomment-284621537
var clampy = clampy_umd$1 || clampy_;
// const elementResizeDetectorMaker = (elementResizeDetectorMaker_).default || elementResizeDetectorMaker_;
// const resizeDetector = elementResizeDetectorMaker({ strategy: 'scroll' });
var clampValue;
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
var defaults = {
clamp: 'auto',
truncationChar: '…',
splitOnChars: ['.', '-', '–', '—', ' '],
useNativeClamp: false
};
function setDefaults(options) {
defaults = _extends({}, defaults, options);
}
function setup(el, clampValue, option) {
tearDown(el);
var resizeListener = function resizeListener() {
clampElement(el, clampValue, option);
};
el.__VueClampy = {
clampValue: clampValue,
resizeListener: resizeListener
};
// Re-clamp on element resize
// resizeDetector.listenTo(el, () => {
// clampElement(el, clampValue);
// });
// Also re-clamp on window resize
window.addEventListener('resize', resizeListener);
clampElement(el, clampValue, option);
}
function tearDown(el) {
if (!el || !el.__VueClampy) return;
// Remove all listeners
// resizeDetector.removeAllListeners(el);
window.removeEventListener('resize', el.__VueClampy.resizeListener);
}
function setInitialContent(el) {
if (el.clampInitialContent === undefined) {
el.clampInitialContent = el.innerHTML.trim();
}
}
function clampElement(el, clamp, option) {
// We use element-resize-detector to trigger the ellipsis.
// Element-resize-detector adds an inner div to monitor
// it's scroll events.
// The process of truncating the text for ellipsis removes this div, so we need to remove and readd it
var scrollNode = el.querySelector('.erd_scroll_detection_container');
if (scrollNode) {
el.removeChild(scrollNode);
}
setInitialContent(el);
if (el.clampInitialContent !== undefined) {
el.innerHTML = el.clampInitialContent;
}
var _defaults = _extends({}, defaults, { clamp: clamp ? clamp : 'auto' }, option);
// Set the opacity to 0 to avoid content to flick when clamping.
el.style.opacity = '0';
var result = clampy.clamp(el, _defaults);
// Set the opacity back to 1 now that the content is clamped.
el.style.opacity = '1';
if (scrollNode) {
el.appendChild(scrollNode);
}
}
var VueClampy$1 = function (option) {
return {
inserted: function inserted(el, binding, vnode) {
clampValue = binding.value;
setup(el, clampValue, option);
},
update: function update(el, binding, vnode) {
clampValue = binding.value;
setup(el, clampValue, option);
},
unbind: function unbind(el, binding, vnode) {
tearDown(el);
delete el.__VueClampy;
}
}
};
var install = function install(Vue, options) {
if (options) setDefaults(options);
for (var key in options){
Vue.directive(key, VueClampy$1(options[key]));
}
// Vue.directive('clampy', VueClampy$1);
Vue.prototype.$clampy = VueClampy$1.clampy;
};
if (typeof window !== 'undefined' && window.Vue) {
window.VueClampy = VueClampy$1;
window.VueClampy.setDefaults = setDefaults;
Vue.use(install);
}
VueClampy$1.install = install;
return VueClampy$1;
})));