├── .gitignore ├── .travis.yml ├── src ├── index.js └── directive.js ├── rollup.config.js ├── LICENSE ├── README.md ├── package.json ├── examples └── index.html └── vue-clampy.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | - /^greenkeeper/.*$/ 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | notifications: 11 | email: false 12 | node_js: 13 | - '9.5' 14 | before_script: 15 | - npm prune 16 | script: 17 | - npm run build 18 | after_success: 19 | - npm run semantic-release -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import VueClampy from './directive'; 2 | import { setDefaults } from './directive'; 3 | 4 | const install = function(Vue, options) { 5 | if (options) setDefaults(options); 6 | Vue.directive('clampy', VueClampy); 7 | Vue.prototype.$clampy = VueClampy.clampy; 8 | }; 9 | 10 | if (typeof window !== 'undefined' && window.Vue) { 11 | window.VueClampy = VueClampy; 12 | window.VueClampy.setDefaults = setDefaults; 13 | Vue.use(install); 14 | } 15 | 16 | VueClampy.install = install; 17 | export default VueClampy; 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import resolve from 'rollup-plugin-node-resolve' 3 | import commonjs from 'rollup-plugin-commonjs' 4 | 5 | export default { 6 | entry: './src/index.js', 7 | dest: 'vue-clampy.js', 8 | 9 | plugins: [ 10 | resolve(), 11 | commonjs(), 12 | babel({ 13 | exclude: 'node_modules/**', 14 | presets: ['es2015-rollup'], 15 | plugins: ['transform-object-assign'] 16 | }) 17 | ], 18 | 19 | format: 'umd', 20 | moduleName: 'vue-clampy' 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexandre Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-clampy 2 | [![Build Status](https://img.shields.io/travis/clampy-js/vue-clampy.svg)](https://travis-ci.org/clampy-js/vue-clampy) 3 | [![GitHub issues](https://img.shields.io/github/issues/clampy-js/vue-clampy.svg)](https://github.com/clampy-js/vue-clampy/issues) 4 | [![GitHub license](https://img.shields.io/github/license/clampy-js/vue-clampy.svg)](https://github.com/clampy-js/vue-clampy/blob/master/LICENSE) 5 | [![npm](https://img.shields.io/npm/dt/@clampy-js/vue-clampy.svg)](https://www.npmjs.com/package/@clampy-js/vue-clampy) 6 | 7 | Vue.js (2+) directive that clamps the content of an element by adding an ellipsis to it if the content inside is too long. 8 | 9 | It uses [@clampy-js/clampy](https://github.com/clampy-js/clampy) library (a fork of [Clamp.js](https://github.com/josephschmitt/Clamp.js)) behind the scene to apply the ellipsis. 10 | 11 | It automatically re-clamps itself when the element or the browser window change size. 12 | 13 | #### Installation 14 | You can install @clampy-js/vue-clampy using NPM or Yarn: 15 | 16 | ``` 17 | npm install @clampy-js/vue-clampy 18 | ``` 19 | 20 | ``` 21 | yarn install @clampy-js/vue-clampy 22 | ``` 23 | 24 | #### Usage 25 | ```typescript 26 | 39 | 42 | ``` 43 | 44 | #### Options 45 | You can also override default options globally: 46 | 47 | ```typescript 48 | 49 | Vue.use(clampy, { 50 | truncationChar: '✂️' 51 | }); 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@clampy-js/vue-clampy", 3 | "description": "Vue.js (2+) directive that clamps the content of an element by adding an ellipsis to it if the content inside is too long.", 4 | "version": "0.0.0-development", 5 | "author": "Alexandre Moore ", 6 | "bugs": { 7 | "url": "https://github.com/clampy-js/vue-clampy/issues" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.0.0", 11 | "@clampy-js/clampy": "^1.0.9" 12 | }, 13 | "devDependencies": { 14 | "babel-plugin-transform-object-assign": "^6.22.0", 15 | "babel-preset-es2015-rollup": "^3.0.0", 16 | "lint-staged": "^3.3.1", 17 | "pre-commit": "^1.2.2", 18 | "prettier": "^0.19.0", 19 | "rollup": "^0.41.6", 20 | "rollup-plugin-babel": "^2.7.1", 21 | "rollup-plugin-commonjs": "^8.0.2", 22 | "rollup-plugin-node-resolve": "^2.0.0", 23 | "semantic-release": "^15.5.0", 24 | "travis-deploy-once": "^5.0.0" 25 | }, 26 | "peerDependencies": { 27 | "vue": "^2.0.0" 28 | }, 29 | "files": [ 30 | "vue-clampy.js" 31 | ], 32 | "homepage": "https://github.com/clampy-js/vue-clampy", 33 | "keywords": [ 34 | "clamp", 35 | "ellipsis", 36 | "clampy-js", 37 | "clampy", 38 | "vue", 39 | "vue-directive", 40 | "vuejs" 41 | ], 42 | "license": "MIT", 43 | "main": "vue-clampy.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/clampy-js/vue-clampy" 47 | }, 48 | "scripts": { 49 | "test": "echo \"Error: no test specified\" && exit 1", 50 | "build": "rollup -c", 51 | "semantic-release": "semantic-release", 52 | "travis-deploy-once": "travis-deploy-once" 53 | }, 54 | "publishConfig": { 55 | "access": "public" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples for vue-clampy 7 | 8 | 9 | 20 | 21 | 22 | 23 | 24 |
25 |

Unclamped content

26 |
27 |

{{originalContent}}

28 |
29 | 30 |

31 | Clamped content (clampy="{{clampyValue}}") 32 | 33 | 34 |

35 |
36 |

{{originalContent}}

37 |
38 |
39 | 40 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/directive.js: -------------------------------------------------------------------------------- 1 | import * as clampy_ from '@clampy-js/clampy/dist/clampy.umd.js'; 2 | // import * as elementResizeDetectorMaker_ from 'element-resize-detector'; 3 | 4 | // https://github.com/rollup/rollup/issues/670#issuecomment-284621537 5 | const clampy = clampy_.default || clampy_; 6 | // const elementResizeDetectorMaker = (elementResizeDetectorMaker_).default || elementResizeDetectorMaker_; 7 | 8 | // const resizeDetector = elementResizeDetectorMaker({ strategy: 'scroll' }); 9 | var clampValue; 10 | 11 | var _extends = Object.assign || 12 | function(target) { 13 | for (var i = 1; i < arguments.length; i++) { 14 | var source = arguments[i]; 15 | 16 | for (var key in source) { 17 | if (Object.prototype.hasOwnProperty.call(source, key)) { 18 | target[key] = source[key]; 19 | } 20 | } 21 | } 22 | 23 | return target; 24 | }; 25 | 26 | var defaults = { 27 | clamp: 'auto', 28 | truncationChar: '…', 29 | splitOnChars: ['.', '-', '–', '—', ' '], 30 | useNativeClamp: false 31 | }; 32 | 33 | export function setDefaults(options) { 34 | defaults = _extends({}, defaults, options); 35 | } 36 | 37 | function setup(el, clampValue) { 38 | tearDown(el); 39 | 40 | const resizeListener = () => { 41 | clampElement(el, clampValue); 42 | }; 43 | 44 | el.__VueClampy = { 45 | clampValue, 46 | resizeListener 47 | }; 48 | 49 | // Re-clamp on element resize 50 | // resizeDetector.listenTo(el, () => { 51 | // clampElement(el, clampValue); 52 | // }); 53 | 54 | // Also re-clamp on window resize 55 | window.addEventListener('resize', resizeListener); 56 | 57 | clampElement(el, clampValue); 58 | } 59 | 60 | function tearDown(el) { 61 | if (!el || !el.__VueClampy) return; 62 | // Remove all listeners 63 | // resizeDetector.removeAllListeners(el); 64 | window.removeEventListener('resize', el.__VueClampy.resizeListener); 65 | } 66 | 67 | function setInitialContent(el) { 68 | if (el.clampInitialContent === undefined) { 69 | el.clampInitialContent = el.innerHTML.trim(); 70 | } 71 | } 72 | 73 | function clampElement(el, clamp) { 74 | // We use element-resize-detector to trigger the ellipsis. 75 | // Element-resize-detector adds an inner div to monitor 76 | // it's scroll events. 77 | // The process of truncating the text for ellipsis removes this div, so we need to remove and readd it 78 | const scrollNode = el.querySelector('.erd_scroll_detection_container'); 79 | if (scrollNode) { 80 | el.removeChild(scrollNode); 81 | } 82 | 83 | setInitialContent(el); 84 | 85 | if (el.clampInitialContent !== undefined) { 86 | el.innerHTML = el.clampInitialContent; 87 | } 88 | 89 | defaults = _extends({}, defaults, { clamp: clamp ? clamp : 'auto' }); 90 | 91 | // Set the opacity to 0 to avoid content to flick when clamping. 92 | el.style.opacity = '0'; 93 | const result = clampy.clamp(el, defaults); 94 | 95 | // Set the opacity back to 1 now that the content is clamped. 96 | el.style.opacity = '1'; 97 | 98 | if (scrollNode) { 99 | el.appendChild(scrollNode); 100 | } 101 | } 102 | 103 | export default { 104 | inserted(el, binding, vnode) { 105 | clampValue = binding.value; 106 | setup(el, clampValue); 107 | }, 108 | 109 | update(el, binding, vnode) { 110 | clampValue = binding.value; 111 | setup(el, clampValue); 112 | }, 113 | 114 | unbind(el, binding, vnode) { 115 | tearDown(el); 116 | delete el.__VueClampy; 117 | } 118 | }; 119 | -------------------------------------------------------------------------------- /vue-clampy.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global['vue-clampy'] = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 8 | 9 | 10 | 11 | function unwrapExports (x) { 12 | return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 13 | } 14 | 15 | function createCommonjsModule(fn, module) { 16 | return module = { exports: {} }, fn(module, module.exports), module.exports; 17 | } 18 | 19 | var clampy_umd = createCommonjsModule(function (module, exports) { 20 | (function (global, factory) { 21 | factory(exports); 22 | }(commonjsGlobal, (function (exports) { 'use strict'; 23 | 24 | var ClampOptions = /** @class */ (function () { 25 | function ClampOptions(clamp, truncationChar, truncationHTML, splitOnChars) { 26 | this.clamp = clamp || "auto"; 27 | this.truncationChar = truncationChar || "…"; 28 | this.truncationHTML = truncationHTML; 29 | this.splitOnChars = splitOnChars || [".", "-", "–", "—", " "]; 30 | } 31 | return ClampOptions; 32 | }()); 33 | var ClampResponse = /** @class */ (function () { 34 | function ClampResponse(original, clamped) { 35 | this.original = original; 36 | this.clamped = clamped; 37 | } 38 | return ClampResponse; 39 | }()); 40 | /** 41 | * Clamps (ie. cuts off) an HTML element's content by adding ellipsis to it if the content inside is too long. 42 | * 43 | * @export 44 | * @param {HTMLElement} element The HTMLElement that should be clamped. 45 | * @param {ClampOptions} [options] The Clamp options 46 | * @returns {ClampResponse} The Clamp response 47 | */ 48 | function clamp(element, options) { 49 | var win = window; 50 | if (!options) { 51 | options = { 52 | clamp: "auto", 53 | truncationChar: "…", 54 | splitOnChars: [".", "-", "–", "—", " "] 55 | }; 56 | } 57 | var opt = { 58 | clamp: options.clamp || "auto", 59 | splitOnChars: options.splitOnChars || [".", "-", "–", "—", " "], 60 | truncationChar: options.truncationChar || "…", 61 | truncationHTML: options.truncationHTML 62 | }; 63 | var splitOnChars = opt.splitOnChars.slice(0); 64 | var splitChar = splitOnChars[0]; 65 | var chunks; 66 | var lastChunk; 67 | var sty = element.style; 68 | var originalText = element.innerHTML; 69 | var clampValue = opt.clamp; 70 | var isCSSValue = clampValue.indexOf && (clampValue.indexOf("px") > -1 || clampValue.indexOf("em") > -1); 71 | var truncationHTMLContainer; 72 | if (opt.truncationHTML) { 73 | truncationHTMLContainer = document.createElement("span"); 74 | truncationHTMLContainer.innerHTML = opt.truncationHTML; 75 | } 76 | // UTILITY FUNCTIONS __________________________________________________________ 77 | /** 78 | * Return the current style for an element. 79 | * @param {HTMLElement} elem The element to compute. 80 | * @param {string} prop The style property. 81 | * @returns {number} 82 | */ 83 | function computeStyle(elem, prop) { 84 | return win.getComputedStyle(elem).getPropertyValue(prop); 85 | } 86 | /** 87 | * Returns the maximum number of lines of text that should be rendered based 88 | * on the current height of the element and the line-height of the text. 89 | */ 90 | function getMaxLines(height) { 91 | var availHeight = height || element.clientHeight; 92 | var lineHeight = getLineHeight(element); 93 | return Math.max(Math.floor(availHeight / lineHeight), 0); 94 | } 95 | /** 96 | * Returns the maximum height a given element should have based on the line- 97 | * height of the text and the given clamp value. 98 | */ 99 | function getMaxHeight(clmp) { 100 | var lineHeight = getLineHeight(element); 101 | return lineHeight * clmp; 102 | } 103 | /** 104 | * Returns the line-height of an element as an integer. 105 | */ 106 | function getLineHeight(elem) { 107 | var lh = computeStyle(elem, "line-height"); 108 | if (lh === "normal") { 109 | // Normal line heights vary from browser to browser. The spec recommends 110 | // a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff. 111 | lh = parseFloat(parseFloat(computeStyle(elem, "font-size")).toFixed(0)) * 1.1; 112 | } 113 | return parseFloat(parseFloat(lh).toFixed(0)); 114 | } 115 | /** 116 | * Returns the height of an element as an integer (max of scroll/offset/client). 117 | * Note: inline elements return 0 for scrollHeight and clientHeight 118 | */ 119 | function getElemHeight(elem) { 120 | // The '- 4' is a hack to deal with the element height when the browser(especially IE) zoom level is not 100%. 121 | // It also doesn't impact clamping when the browser zoom level is 100%. 122 | return Math.max(elem.scrollHeight, elem.clientHeight) - 4; 123 | } 124 | /** 125 | * Gets an element's last child. That may be another node or a node's contents. 126 | */ 127 | function getLastChild(elem) { 128 | if (!elem.lastChild) { 129 | return; 130 | } 131 | // Current element has children, need to go deeper and get last child as a text node 132 | if (elem.lastChild.children && elem.lastChild.children.length > 0) { 133 | return getLastChild(Array.prototype.slice.call(elem.children).pop()); 134 | } 135 | // This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying 136 | else if (!elem.lastChild || 137 | !elem.lastChild.nodeValue || 138 | elem.lastChild.nodeValue === "" || 139 | elem.lastChild.nodeValue === opt.truncationChar) { 140 | if (!elem.lastChild.nodeValue) { 141 | // Check for void/empty element (such as
tag) or if it's the ellipsis and remove it. 142 | if ((elem.lastChild.firstChild === null || 143 | elem.lastChild.firstChild.nodeValue === opt.truncationChar) && 144 | elem.lastChild.parentNode) { 145 | elem.lastChild.parentNode.removeChild(elem.lastChild); 146 | // Check if the element has no more children and remove it if it's the case. 147 | // This can happen for instance with lists (i.e.