vue-truncate-collapsed
Version:
A simple component that truncates your text and adds a 'Read More/Show Less' clickable.
766 lines (701 loc) • 22.5 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})(typeof self !== 'undefined' ? self : this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
var _h2p = __webpack_require__(4);
/* harmony default export */ __webpack_exports__["a"] = ({
name: 'Truncate',
props: {
truncated: {
type: Boolean,
"default": true
},
collapsedTextClass: {
type: String,
"default": ''
},
text: {
type: String,
required: true
},
clamp: {
type: String,
"default": 'Read More'
},
length: {
type: Number,
"default": 100
},
less: {
type: String,
"default": 'Show Less'
},
type: {
type: String,
"default": 'text'
},
actionClass: {
type: String,
"default": ''
}
},
data: function data() {
return {
show: false
};
},
computed: {
isHTML: function isHTML() {
return this.type === 'html';
},
textClass: function textClass() {
return this.textLength > this.length && this.collapsedTextClass ? this.collapsedTextClass : '';
},
textLength: function textLength() {
if (this.isHTML) {
// We need the length of the text without the html being considered
// This ensures we provide the right calculation for when to show/hide the more link
var text = this.text.replace(/<[^>]*>/g, '');
return text.length;
}
return this.text.length;
},
showToggle: function showToggle() {
return this.textLength > this.length;
}
},
watch: {
truncated: function truncated(value) {
this.toggle(value);
}
},
created: function created() {
this.show = !this.truncated;
this.toggle(!this.truncated);
},
methods: {
truncate: function truncate(string) {
if (string) {
if (this.type === 'html') return _h2p(string, this.length);
return string.toString().substring(0, this.length);
}
return '';
},
toggle: function toggle(override) {
// use the override value if it is set as a boolean
var toggled = typeof override === 'boolean' ? override : !this.show;
this.show = toggled;
this.$emit('toggle', toggled);
},
h2p: function h2p(text) {
return _h2p(text);
}
}
});
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__truncate_vue__ = __webpack_require__(2);
var VueTruncate = {
install: function install(Vue) {
Vue.component('truncate', __WEBPACK_IMPORTED_MODULE_0__truncate_vue__["a" /* default */]);
}
};
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(VueTruncate);
}
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__babel_loader_node_modules_vue_loader_lib_selector_type_script_index_0_truncate_vue__ = __webpack_require__(0);
/* unused harmony namespace reexport */
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1__node_modules_vue_loader_lib_template_compiler_index_id_data_v_11b2e33f_hasScoped_false_buble_transforms_node_modules_vue_loader_lib_selector_type_template_index_0_truncate_vue__ = __webpack_require__(5);
var disposed = false
var normalizeComponent = __webpack_require__(3)
/* script */
/* template */
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = null
/* scopeId */
var __vue_scopeId__ = null
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
__WEBPACK_IMPORTED_MODULE_0__babel_loader_node_modules_vue_loader_lib_selector_type_script_index_0_truncate_vue__["a" /* default */],
__WEBPACK_IMPORTED_MODULE_1__node_modules_vue_loader_lib_template_compiler_index_id_data_v_11b2e33f_hasScoped_false_buble_transforms_node_modules_vue_loader_lib_selector_type_template_index_0_truncate_vue__["a" /* default */],
__vue_template_functional__,
__vue_styles__,
__vue_scopeId__,
__vue_module_identifier__
)
Component.options.__file = "src/truncate.vue"
/* hot reload */
if (false) {(function () {
var hotAPI = require("vue-hot-reload-api")
hotAPI.install(require("vue"), false)
if (!hotAPI.compatible) return
module.hot.accept()
if (!module.hot.data) {
hotAPI.createRecord("data-v-11b2e33f", Component.options)
} else {
hotAPI.reload("data-v-11b2e33f", Component.options)
}
module.hot.dispose(function (data) {
disposed = true
})
})()}
/* harmony default export */ __webpack_exports__["a"] = (Component.exports);
/***/ }),
/* 3 */
/***/ (function(module, exports) {
/* globals __VUE_SSR_CONTEXT__ */
// IMPORTANT: Do NOT use ES2015 features in this file.
// This module is a runtime utility for cleaner component module output and will
// be included in the final webpack user bundle.
module.exports = function normalizeComponent (
rawScriptExports,
compiledTemplate,
functionalTemplate,
injectStyles,
scopeId,
moduleIdentifier /* server only */
) {
var esModule
var scriptExports = rawScriptExports = rawScriptExports || {}
// ES6 modules interop
var type = typeof rawScriptExports.default
if (type === 'object' || type === 'function') {
esModule = rawScriptExports
scriptExports = rawScriptExports.default
}
// Vue.extend constructor export interop
var options = typeof scriptExports === 'function'
? scriptExports.options
: scriptExports
// render functions
if (compiledTemplate) {
options.render = compiledTemplate.render
options.staticRenderFns = compiledTemplate.staticRenderFns
options._compiled = true
}
// functional template
if (functionalTemplate) {
options.functional = true
}
// scopedId
if (scopeId) {
options._scopeId = scopeId
}
var hook
if (moduleIdentifier) { // server build
hook = function (context) {
// 2.3 injection
context =
context || // cached call
(this.$vnode && this.$vnode.ssrContext) || // stateful
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__
}
// inject component styles
if (injectStyles) {
injectStyles.call(this, context)
}
// register component module identifier for async chunk inferrence
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier)
}
}
// used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook
} else if (injectStyles) {
hook = injectStyles
}
if (hook) {
var functional = options.functional
var existing = functional
? options.render
: options.beforeCreate
if (!functional) {
// inject component registration as beforeCreate hook
options.beforeCreate = existing
? [].concat(existing, hook)
: [hook]
} else {
// for template-only hot-reload because in that case the render fn doesn't
// go through the normalizer
options._injectStyles = hook
// register for functioal component in vue file
options.render = function renderWithStyleInjection (h, context) {
hook.call(context)
return existing(h, context)
}
}
}
return {
esModule: esModule,
exports: scriptExports,
options: options
}
}
/***/ }),
/* 4 */
/***/ (function(module, exports) {
/**
* Truncate HTML string and keep tag safe.
*
* @method truncate
* @param {String} string string needs to be truncated
* @param {Number} maxLength length of truncated string
* @param {Object} options (optional)
* @param {Boolean} [options.keepImageTag] flag to specify if keep image tag, false by default
* @param {Boolean} [options.truncateLastWord] truncates last word, true by default
* @param {Number} [options.slop] tolerance when options.truncateLastWord is false before we give up and just truncate at the maxLength position, 10 by default (but not greater than maxLength)
* @param {Boolean|String} [options.ellipsis] omission symbol for truncated string, '...' by default
* @return {String} truncated string
*/
function truncate(string, maxLength, options) {
var EMPTY_OBJECT = {},
EMPTY_STRING = '',
DEFAULT_TRUNCATE_SYMBOL = '...',
DEFAULT_SLOP = 10 > maxLength ? maxLength : 10,
EXCLUDE_TAGS = ['img', 'br'], // non-closed tags
items = [], // stack for saving tags
total = 0, // record how many characters we traced so far
content = EMPTY_STRING, // truncated text storage
KEY_VALUE_REGEX = '([\\w|-]+\\s*(=\\s*"[^"]*")?\\s*)*',
IS_CLOSE_REGEX = '\\s*\\/?\\s*',
CLOSE_REGEX = '\\s*\\/\\s*',
SELF_CLOSE_REGEX = new RegExp('<\\/?\\w+\\s*' + KEY_VALUE_REGEX + CLOSE_REGEX + '>'),
HTML_TAG_REGEX = new RegExp('<\\/?\\w+\\s*' + KEY_VALUE_REGEX + IS_CLOSE_REGEX + '>'),
URL_REGEX = /(((ftp|https?):\/\/)[\-\w@:%_\+.~#?,&\/\/=]+)|((mailto:)?[_.\w\-]+@([\w][\w\-]+\.)+[a-zA-Z]{2,3})/g, // Simple regexp
IMAGE_TAG_REGEX = new RegExp('<img\\s*' + KEY_VALUE_REGEX + IS_CLOSE_REGEX + '>'),
WORD_BREAK_REGEX = new RegExp('\\W+', 'g'),
matches = true,
result,
index,
tail,
tag,
selfClose;
/**
* Remove image tag
*
* @private
* @method _removeImageTag
* @param {String} string not-yet-processed string
* @return {String} string without image tags
*/
function _removeImageTag(string) {
var match = IMAGE_TAG_REGEX.exec(string),
index,
len;
if (!match) {
return string;
}
index = match.index;
len = match[0].length;
return string.substring(0, index) + string.substring(index + len);
}
/**
* Dump all close tags and append to truncated content while reaching upperbound
*
* @private
* @method _dumpCloseTag
* @param {String[]} tags a list of tags which should be closed
* @return {String} well-formatted html
*/
function _dumpCloseTag(tags) {
var html = '';
tags.reverse().forEach(function (tag, index) {
// dump non-excluded tags only
if (-1 === EXCLUDE_TAGS.indexOf(tag)) {
html += '</' + tag + '>';
}
});
return html;
}
/**
* Process tag string to get pure tag name
*
* @private
* @method _getTag
* @param {String} string original html
* @return {String} tag name
*/
function _getTag(string) {
var tail = string.indexOf(' ');
// TODO:
// we have to figure out how to handle non-well-formatted HTML case
if (-1 === tail) {
tail = string.indexOf('>');
if (-1 === tail) {
throw new Error('HTML tag is not well-formed : ' + string);
}
}
return string.substring(1, tail);
}
/**
* Get the end position for String#substring()
*
* If options.truncateLastWord is FALSE, we try to the end position up to
* options.slop characters to avoid breaking in the middle of a word.
*
* @private
* @method _getEndPosition
* @param {String} string original html
* @param {Number} tailPos (optional) provided to avoid extending the slop into trailing HTML tag
* @return {Number} maxLength
*/
function _getEndPosition (string, tailPos) {
var defaultPos = maxLength - total,
position = defaultPos,
isShort = defaultPos < options.slop,
slopPos = isShort ? defaultPos : options.slop - 1,
substr,
startSlice = isShort ? 0 : defaultPos - options.slop,
endSlice = tailPos || (defaultPos + options.slop),
result;
if (!options.truncateLastWord) {
substr = string.slice(startSlice, endSlice);
if (tailPos && substr.length <= tailPos) {
position = substr.length;
}
else {
while ((result = WORD_BREAK_REGEX.exec(substr)) !== null) {
// a natural break position before the hard break position
if (result.index < slopPos) {
position = defaultPos - (slopPos - result.index);
// keep seeking closer to the hard break position
// unless a natural break is at position 0
if (result.index === 0 && defaultPos <= 1) break;
}
// a natural break position exactly at the hard break position
else if (result.index === slopPos) {
position = defaultPos;
break; // seek no more
}
// a natural break position after the hard break position
else {
position = defaultPos + (result.index - slopPos);
break; // seek no more
}
}
}
if (string.charAt(position - 1).match(/\s$/)) position--;
}
return position;
}
options = options || EMPTY_OBJECT;
options.ellipsis = (undefined !== options.ellipsis) ? options.ellipsis : DEFAULT_TRUNCATE_SYMBOL;
options.truncateLastWord = (undefined !== options.truncateLastWord) ? options.truncateLastWord : true;
options.slop = (undefined !== options.slop) ? options.slop : DEFAULT_SLOP;
while (matches) {
matches = HTML_TAG_REGEX.exec(string);
if (!matches) {
if (total >= maxLength) { break; }
matches = URL_REGEX.exec(string);
if (!matches || matches.index >= maxLength) {
content += string.substring(0, _getEndPosition(string));
break;
}
while (matches) {
result = matches[0];
index = matches.index;
content += string.substring(0, (index + result.length) - total);
string = string.substring(index + result.length);
matches = URL_REGEX.exec(string);
}
break;
}
result = matches[0];
index = matches.index;
if (total + index > maxLength) {
// exceed given `maxLength`, dump everything to clear stack
content += string.substring(0, _getEndPosition(string, index));
break;
} else {
total += index;
content += string.substring(0, index);
}
if ('/' === result[1]) {
// move out open tag
items.pop();
selfClose=null;
} else {
selfClose = SELF_CLOSE_REGEX.exec(result);
if (!selfClose) {
tag = _getTag(result);
items.push(tag);
}
}
if (selfClose) {
content += selfClose[0];
} else {
content += result;
}
string = string.substring(index + result.length);
}
if (string.length > maxLength - total && options.ellipsis) {
content += options.ellipsis;
}
content += _dumpCloseTag(items);
if (!options.keepImageTag) {
content = _removeImageTag(content);
}
return content;
}
module.exports = truncate;
/***/ }),
/* 5 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c("div", [
!_vm.show && !_vm.isHTML
? _c("div", [
_c("span", { class: _vm.textClass }, [
_vm._v("\n " + _vm._s(_vm.truncate(_vm.text)) + "\n ")
]),
_vm._v(" "),
_vm.showToggle
? _c(
"button",
{
class: _vm.actionClass,
attrs: { type: "button" },
on: {
click: function($event) {
$event.preventDefault()
$event.stopPropagation()
return _vm.toggle($event)
}
}
},
[_vm._v("\n " + _vm._s(_vm.clamp) + "\n ")]
)
: _vm._e()
])
: !_vm.show && _vm.isHTML
? _c("div", [
_c("span", {
class: _vm.textClass,
domProps: { innerHTML: _vm._s(_vm.truncate(_vm.text)) }
}),
_vm._v(" "),
_vm.showToggle
? _c(
"button",
{
class: _vm.actionClass,
attrs: { type: "button" },
on: {
click: function($event) {
$event.preventDefault()
$event.stopPropagation()
return _vm.toggle($event)
}
}
},
[_vm._v(_vm._s(_vm.clamp))]
)
: _vm._e()
])
: _vm._e(),
_vm._v(" "),
_vm.show && !_vm.isHTML
? _c("div", [
_c("span", [_vm._v(_vm._s(_vm.text))]),
_vm._v(" "),
_vm.showToggle
? _c(
"button",
{
class: _vm.actionClass,
attrs: { type: "button" },
on: {
click: function($event) {
$event.preventDefault()
$event.stopPropagation()
return _vm.toggle($event)
}
}
},
[_vm._v(_vm._s(_vm.less))]
)
: _vm._e()
])
: _vm.show && _vm.isHTML
? _c("div", [
_vm.text.length >= _vm.length
? _c("div", { domProps: { innerHTML: _vm._s(_vm.text) } })
: _vm._e(),
_vm._v(" "),
_vm.showToggle
? _c(
"button",
{
class: _vm.actionClass,
attrs: { type: "button" },
on: {
click: function($event) {
$event.preventDefault()
$event.stopPropagation()
return _vm.toggle($event)
}
}
},
[_vm._v(_vm._s(_vm.less))]
)
: _c("p", [
_vm._v("\n " + _vm._s(_vm.h2p(_vm.text)) + "\n ")
])
])
: _vm._e()
])
}
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
/* harmony default export */ __webpack_exports__["a"] = (esExports);
if (false) {
module.hot.accept()
if (module.hot.data) {
require("vue-hot-reload-api") .rerender("data-v-11b2e33f", esExports)
}
}
/***/ })
/******/ ]);
});