define('jira/ajs/shorten/shortener', ['jira/util/formatter', 'jira/ajs/control', 'jira/data/local-storage', 'jquery'], function (formatter, Control, localStorage, jQuery) { /** * Shorten long lists with an ellipsis * *

Use

* *
Markup:
* *
     * 
* a * b * c * d * e * f * g * h * i * j * k *
*
* *
JavaScript
* *
     * require(["jira/ajs/shorten/shortener"], function(Shortener) {
     *     // no options
     *     new Shortener("#mylist");
     *
     *     // with options
     *     new Shortener({
     *         element: "#myList",
     *         numRows: 2
     *     });
     * });
     * 
* * @class Shortener * @extends Control * */ return Control.extend({ _getDefaultOptions: function _getDefaultOptions() { return { items: "a, span", numRows: 1, shortenText: "hide", shortenOnInit: true, persist: true, expandButtonTooltip: formatter.I18n.getText("viewissue.shorten.view.more"), collapseButtonTooltip: formatter.I18n.getText("viewissue.shorten.hide") }; }, /** * Creates a shorten control * * @constructs * @param {Object} options * @param {String |jQuery} [options.items=a,span] - selector or jQuery collection specifying items * @param {Number} [options.numRows=1] - Number of rows to display when shortened * @param {String} [options.shortenText=hide] - Text to display in link at the end of the list when expanded * @param {Boolean} [options.shortenOnInit=true] - If true will shorten onload */ init: function init(options) { if (typeof options === "string") { options = { element: options }; } options = options || {}; this.options = jQuery.extend(this._getDefaultOptions(), options); this._timerId = 0; this.expanded = false; this.$container = jQuery(this.options.element); this._assignEvents("body", document.body); this._ready(); }, /** * Validate initialization * @return {Boolean} */ _isValid: function _isValid() { return !this.initialized && this.$container.is(":visible") && this.$container.children().length > 0; }, /** * Lazy initialization, so that we can init on dom ready if it is visible or when a activating a tab makes it visible * @private */ _ready: function _ready() { if (this._isValid()) { this.$items = this.$container.children(this.options.items); this.$expandButton = this._render("expandButton"); this.$collapseButton = this._render("collapseButton"); this._assignEvents("expand-button", ".shortener-expand"); this._assignEvents("collapse-button", ".shortener-collapse"); if (!jQuery.browser.msie || jQuery.browser.version >= "9") { // IE8 is excluded from reflowing on "resize" events. Rendering this thing // is very expensive in IE8 and "resize" events occur too frequently. this._assignEvents("resize-region", window); } if (this._isCollapsedOnInit()) { this.collapse(); } else { this.expand(); } this.initialized = true; } }, _renders: { /** * Creates the jQuery object representing an ellipsis. The ellipsis appended to the shortened list of items. It * contains text representing how many items have been hidden. When clicked it reveals the full list. * The ellipsis has a class of ellipsis and styling should be controlled in css. * * @method #_renders.expandButton * @private * @param {number} itemsHidden - number of items hidden * @return {jQuery} jQuery wrapped HTML element * */ "expandButton": function expandButton() { return jQuery("").attr("title", this.options.expandButtonTooltip).add("
"); }, /** * Creates the jQuery object representing the shorten tip. The shorten tip is appended to the expanded list of * items. When clicked it shortens the list to the user specified paramater numRows Ellipsis has * a calss of icon-hide and styling should be controlled in css. * * @method #_renders.shortenTip * @private * @param {string} removeText - number of items hidden * @return {jQuery} jQuery wrapped HTML element * */ "collapseButton": function collapseButton() { return jQuery("").append(jQuery("").text(this.options.collapseButtonTooltip)); } }, _events: { "expand-button": { "click": function click(event) { if (event.currentTarget === this.$expandButton[0]) { event.preventDefault(); this.expand(); this._saveState("expanded"); } } }, "collapse-button": { "click": function click(event) { if (event.currentTarget === this.$collapseButton[0]) { event.preventDefault(); this.collapse(); this._saveState("collapsed"); this.$container.scrollIntoView(); } } }, "resize-region": { "resize": function resize() { clearTimeout(this._timerId); if (!this.expanded) { var instance = this; this._timerId = setTimeout(function () { instance.collapse(); }, 400); } } }, "body": { // handling for the case where control is in a tab, and as a result hidden. tabSelect: function tabSelect() { this._ready(); } } }, /** * @private * @param {string} value */ _saveState: function _saveState(value) { try { localStorage.setItem("AJS.Shortener#" + this.$container.closest("[id]").attr("id"), value); } catch (QUOTA_EXCEEDED_ERR) { // ignore } }, /** * @private * @return {?string} */ _loadState: function _loadState() { return localStorage.getItem("AJS.Shortener#" + this.$container.closest("[id]").attr("id")); }, /** * Should list should be shortened on load. This is determined by cookie, if persist options is true, or "shortenOnInit" * option. Please not that if persist option is set to true, the list will not be shortened if use has expanded it previously * regardless of the shortenOnInit set to true. * * @private * @return boolean */ _isCollapsedOnInit: function _isCollapsedOnInit() { var shortenOnInit = this._loadState(); if (shortenOnInit !== null) { return shortenOnInit !== "expanded"; } return this.options.shortenOnInit; }, /** * Removes $expandButton and $collapseButton. * * @private */ _removeButtons: function _removeButtons() { this.$expandButton.remove(); this.$collapseButton.remove(); }, /** * Get the index within this.$items of the first element that flows over * the allowed number of lines, or (-1) if all items fit within the limit. * * Note: The first item in the list is never considered overflowing, even * when it contains several words that might wrap multiple lines, so this * function will never return 0. * * @private * @return {number} */ _getOverflowIndex: function _getOverflowIndex() { if (this.$items.length > 1) { var currentRow = 1; var prevItemPageX = -1; for (var i = 0; i < this.$items.length; i++) { var itemPageX = this.$items.eq(i).offset().left; if (itemPageX <= prevItemPageX) { // This item flows to a new line. currentRow++; if (currentRow > this.options.numRows) { // This item exceeds the allowed number of lines. return i; } } prevItemPageX = itemPageX; } } return -1; }, /** * Expands list to full height, adding a link to shorten */ expand: function expand() { this._removeButtons(); if (this._getOverflowIndex() > 0) { this.$collapseButton = this._render("collapseButton"); this.$container.append(this.$collapseButton); this.$container.css("height", "auto"); // Ensure IE8 renders the new layout. if (jQuery.browser.msie && jQuery.browser.version < "9") { jQuery('body').toggleClass('reflow'); } } this.expanded = true; }, /** * Contracts list to user specified number of rows, adding a link to shorten. */ collapse: function collapse() { this._removeButtons(); var i = this._getOverflowIndex(); if (i > 0) { // Isolate $container in render tree while we make adjustments. this.$container.css({ "position": "absolute", "visibility": "hidden", "width": this.$container[0].clientWidth + "px" }); var $expandButtonContent = this.$expandButton.first(); do { var remainingItemCount = this.$items.length - i; $expandButtonContent.text("(" + remainingItemCount + ")"); this.$expandButton.insertBefore(this.$items[i]); // Check that $expandButton fits on the same line as the previous item, // otherwise try again with the item before that. i--; var oi = this.$items.eq(i).offset(); var ob = this.$expandButton.offset(); if (oi.left < ob.left && ob.top < oi.top + 10) { // It fits! We assume $expandButton is on the same line as the previous // item if it's offsetTop is less than a line-height below the item's // offsetTop. Hard-coding 10px is a best approximation of line-height. break; } } while (i > 0); // Set the $container height required to clip the item immediately after $expandButton. var height = i < this.$items.length - 1 ? this.$items.eq(i + 1).offset().top - this.$container.offset().top + "px" : "auto"; this.$container.css({ "height": height, "position": "static", "visibility": "visible", "width": "auto" }); $expandButtonContent.attr("title", formatter.format(this.options.expandButtonTooltip, remainingItemCount)); // Ensure IE8 renders the new layout. // Otherwise, shortening a field in the "People" group will leave the things below it hanging. if (jQuery.browser.msie && jQuery.browser.version < "9") { jQuery('body').toggleClass('reflow'); } } else { // Make sure no items are being clipped. this.$container.css("height", "auto"); } this.expanded = false; } }); }); define('jira/jquery/plugins/shorten/shorten', ['jira/ajs/shorten/shortener', 'jquery'], function (Shortener, jQuery) { /** * * jQuery plugin to shorten long lists with an ellipsis. * * For full options see {@link Shortener} * * @note Delegates to {@link Shortener} * * @example * * // no options * jQuery("#my-container").shorten(); * * // options * jQuery("#my-container").shorten({ * numRows: 5 * }); * * @function external:"jQuery.fn".shorten * @param {Object} [options] * @param {HTMLElement | jQuery} options.element */ jQuery.fn.shorten = function (options) { var res = []; options = options || {}; this.each(function () { options.element = this; res.push(new Shortener(options)); }); return res; }; }); AJS.namespace('AJS.Shortener', null, require('jira/ajs/shorten/shortener')); // Make extension available in global scope immediately / synchronously. // TODO INC-71 - remove synchronous require (function () { require('jira/jquery/plugins/shorten/shorten'); })();