avvo-styleguide
Version:
Avvo styleguide
296 lines (215 loc) • 7.36 kB
JavaScript
/* ========================================================================
* Avvo UI - truncate.js
* ======================================================================== */
import 'dotdotdot/src/js/jquery.dotdotdot.js'
// Advanced cross-browser ellipsis for multi-line content.
// http://dotdotdot.frebsite.nl/
const $ = global.jQuery
const $window = $(window)
const $document = $(document)
let windowResizeTimeout
let windowWidth = $window.width()
// TRUNCATE PUBLIC CLASS DEFINITION
// ================================
const Truncate = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Truncate.DEFAULTS, options)
this.$toggler = $(Truncate.TOGGLE_SELECTOR).filter(`[data-target="#${element.id}"]`)
this.$toggler.attr('aria-controls', element.id)
this.options.expandLabel = this.options.expandLabel || this.$toggler.html()
this.transitioning = null
this.render()
}
Truncate.VERSION = '1.1.0'
Truncate.TRUNCATE_SELECTOR = '[data-truncate-lines]'
Truncate.TOGGLE_SELECTOR = '[data-toggle="truncate"]'
Truncate.TRANSITION_DURATION = 350
Truncate.WINDOW_RESIZE_DELAY = 100
Truncate.DEFAULTS = {
toleranceLines: 2,
truncateLines: 3,
truncateLabel: 'Less',
expandLabel: undefined,
}
Truncate.prototype.render = function () {
this.calculateHeights()
if (this.needsTruncation) {
this.$toggler.show()
if (this.$element.hasClass('is-truncated')) {
this.truncate({ animate: false })
} else {
this.expand({ animate: false })
}
} else {
this.$toggler.hide()
this.$element.css('height', '') // Remove inline styles
}
}
Truncate.prototype.calculateHeights = function () {
const lineHeight = calculateLineHeight(this.$element)
// Reset height…
if (this.$element.hasClass('is-truncated')) {
this.$element.trigger('destroy.dot')
}
this.expandedHeight = this.$element[0].scrollHeight
this.truncatedHeight = this.options.truncateLines * lineHeight // educated guess
this.needsTruncation = this.truncatedHeight < (this.expandedHeight - (this.options.toleranceLines * lineHeight))
}
Truncate.prototype.truncate = function (options) {
if (this.transitioning) {
return
}
options = $.extend({ animate: true }, options)
const startEvent = $.Event('truncate.ui.truncate')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) {
return
}
if (options.animate) {
// Hardcoding the element's height ensures there's a value to animate
this.$element.height(this.$element.height())
forceRedraw(this.$element[0])
this.$element.addClass('truncating')
}
this.$element.attr('aria-expanded', false)
this.$toggler
.attr('aria-expanded', false)
.trigger('blur')
this.transitioning = true
const complete = function () {
this.$element
.addClass('is-truncated')
.dotdotdot()
.height('')
.removeClass('truncating')
this.$toggler
.html(this.options.expandLabel)
this.transitioning = false
this.$element.trigger('truncated.ui.truncate')
}
this.$element.height(this.truncatedHeight)
if (!$.support.transition) {
return complete.call(this)
}
this.$element
.one('uiTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(options.animate ? Truncate.TRANSITION_DURATION : 0)
}
Truncate.prototype.expand = function (options) {
if (this.transitioning) {
return
}
options = $.extend({ animate: true }, options)
const startEvent = $.Event('expand.ui.truncate')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) {
return
}
if (options.animate) {
// Update truncated height with the exact height we have currently
// overflow:auto and outerHeight(true) ensures we factor in margins
this.truncatedHeight = this.$element.css('overflow', 'auto').outerHeight(true)
this.$element.height(this.truncatedHeight)
forceRedraw(this.$element[0])
this.$element.addClass('truncating')
}
this.$element
.removeClass('is-truncated')
.trigger('destroy.dot')
this.$toggler
.trigger('blur')
this.transitioning = true
const complete = function () {
this.$element
.height('')
.removeClass('truncating')
.attr('aria-expanded', true)
this.$toggler
.html(this.options.truncateLabel)
.attr('aria-expanded', true)
this.transitioning = false
this.$element.trigger('expanded.ui.truncate')
}
this.$element.height(this.expandedHeight)
if (!$.support.transition) {
return complete.call(this)
}
this.$element
.one('uiTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(options.animate ? Truncate.TRANSITION_DURATION : 0)
}
Truncate.prototype.toggle = function () {
const action = this.$element.hasClass('is-truncated') ? 'expand' : 'truncate'
this[action]()
}
// Normalizes browser differences in reporting line-height. Some always report it in pixels,
// others report back what was set in the stylesheet (e.g. a ratio like 1.5)
//
// @return int - The pixel line-height
function calculateLineHeight($el) {
const rawLineHeight = $el.css('line-height')
let lineHeight = parseFloat(rawLineHeight)
const isRatio = /^\d*\.?\d*$/.test(rawLineHeight)
if (isRatio) {
lineHeight *= parseFloat($el.css('font-size'))
}
return Math.ceil(lineHeight)
}
function forceRedraw(el) {
// Reading "offsetHeight" forces the browser to redraw the element.
return el.offsetHeight
}
// TRUNCATE PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function () {
const $this = $(this)
let data = $this.data('ui.truncate')
const options = $.extend({}, Truncate.DEFAULTS, $this.data(), typeof option === 'object' && option)
// Handle special case where [data-truncate-lines] was not explicitly set
if (options.truncateLines === '') {
options.truncateLines = Truncate.DEFAULTS.truncateLines
}
// instantiate only once
if (!data) {
$this.data('ui.truncate', (data = new Truncate(this, options)))
}
// calling functions, e.g. $('.foo').truncate('toggle');
if (typeof option === 'string') {
data[option]()
}
})
}
export function init() {
const old = $.fn.truncate
$.fn.truncate = Plugin
$.fn.truncate.Constructor = Truncate
// TRUNCATE NO CONFLICT
// ====================
$.fn.truncate.noConflict = function () {
$.fn.truncate = old
return this
}
// TRUNCATE DATA-API
// =================
// Truncate elements on load and on explicit init
$window.on('load.ui.truncate.data-api, init.ui.truncate.data-api', () => {
Plugin.call($(Truncate.TRUNCATE_SELECTOR))
})
// Truncate togglers
$document.on('click.ui.truncate.data-api', Truncate.TOGGLE_SELECTOR, function (event) {
event.preventDefault()
const $toggler = $(this)
const $target = $($toggler.data('target'))
Plugin.call($target, 'toggle')
})
$window.on('resize.ui.truncate.data-api', () => {
clearTimeout(windowResizeTimeout)
windowResizeTimeout = setTimeout(() => {
if ($window.width() !== windowWidth) {
windowWidth = $window.width()
Plugin.call($(Truncate.TRUNCATE_SELECTOR), 'render')
}
}, Truncate.WINDOW_RESIZE_DELAY)
})
}