447 lines
17 KiB
JavaScript
447 lines
17 KiB
JavaScript
/*!
|
|
|
|
Copyright (c) 2011 Peter van der Spek
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
(function($) {
|
|
|
|
/**
|
|
* Hash containing mapping of selectors to settings hashes for target selectors that should be live updated.
|
|
*
|
|
* @type {Object.<string, Object>}
|
|
* @private
|
|
*/
|
|
var liveUpdatingTargetSelectors = {};
|
|
|
|
/**
|
|
* Interval ID for live updater. Contains interval ID when the live updater interval is active, or is undefined
|
|
* otherwise.
|
|
*
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
var liveUpdaterIntervalId;
|
|
|
|
/**
|
|
* Boolean indicating whether the live updater is running.
|
|
*
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
var liveUpdaterRunning = false;
|
|
|
|
/**
|
|
* Set of default settings.
|
|
*
|
|
* @type {Object.<string, string>}
|
|
* @private
|
|
*/
|
|
var defaultSettings = {
|
|
ellipsis: '...',
|
|
setTitle: 'never',
|
|
live: false
|
|
};
|
|
|
|
/**
|
|
* Perform ellipsis on selected elements.
|
|
*
|
|
* @param {string} selector the inner selector of elements that ellipsis may work on. Inner elements not referred to by this
|
|
* selector are left untouched.
|
|
* @param {Object.<string, string>=} options optional options to override default settings.
|
|
* @return {jQuery} the current jQuery object for chaining purposes.
|
|
* @this {jQuery} the current jQuery object.
|
|
*/
|
|
$.fn.ellipsis = function(selector, options) {
|
|
var subjectElements, settings;
|
|
|
|
subjectElements = $(this);
|
|
|
|
// Check for options argument only.
|
|
if (typeof selector !== 'string') {
|
|
options = selector;
|
|
selector = undefined;
|
|
}
|
|
|
|
// Create the settings from the given options and the default settings.
|
|
settings = $.extend({}, defaultSettings, options);
|
|
|
|
// If selector is not set, work on immediate children (default behaviour).
|
|
settings.selector = selector;
|
|
|
|
// Do ellipsis on each subject element.
|
|
subjectElements.each(function() {
|
|
var elem = $(this);
|
|
|
|
// Do ellipsis on subject element.
|
|
ellipsisOnElement(elem, settings);
|
|
});
|
|
|
|
// If live option is enabled, add subject elements to live updater. Otherwise remove from live updater.
|
|
if (settings.live) {
|
|
addToLiveUpdater(subjectElements.selector, settings);
|
|
|
|
} else {
|
|
removeFromLiveUpdater(subjectElements.selector);
|
|
}
|
|
|
|
// Return jQuery object for chaining.
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Perform ellipsis on the given container.
|
|
*
|
|
* @param {jQuery} containerElement jQuery object containing one DOM element to perform ellipsis on.
|
|
* @param {Object.<string, string>} settings the settings for this ellipsis operation.
|
|
* @private
|
|
*/
|
|
function ellipsisOnElement(containerElement, settings) {
|
|
var containerData = containerElement.data('jqae');
|
|
if (!containerData) containerData = {};
|
|
|
|
// Check if wrapper div was already created and bound to the container element.
|
|
var wrapperElement = containerData.wrapperElement;
|
|
|
|
// If not, create wrapper element.
|
|
if (!wrapperElement) {
|
|
wrapperElement = containerElement.wrapInner('<div/>').find('>div');
|
|
|
|
// Wrapper div should not add extra size.
|
|
wrapperElement.css({
|
|
margin: 0,
|
|
padding: 0,
|
|
border: 0
|
|
});
|
|
}
|
|
|
|
// Check if the original wrapper element content was already bound to the wrapper element.
|
|
var wrapperElementData = wrapperElement.data('jqae');
|
|
if (!wrapperElementData) wrapperElementData = {};
|
|
|
|
var wrapperOriginalContent = wrapperElementData.originalContent;
|
|
|
|
// If so, clone the original content, re-bind the original wrapper content to the clone, and replace the
|
|
// wrapper with the clone.
|
|
if (wrapperOriginalContent) {
|
|
wrapperElement = wrapperElementData.originalContent.clone(true)
|
|
.data('jqae', {originalContent: wrapperOriginalContent}).replaceAll(wrapperElement);
|
|
|
|
} else {
|
|
// Otherwise, clone the current wrapper element and bind it as original content to the wrapper element.
|
|
|
|
wrapperElement.data('jqae', {originalContent: wrapperElement.clone(true)});
|
|
}
|
|
|
|
// Bind the wrapper element and current container width and height to the container element. Current container
|
|
// width and height are stored to detect changes to the container size.
|
|
containerElement.data('jqae', {
|
|
wrapperElement: wrapperElement,
|
|
containerWidth: containerElement.width(),
|
|
containerHeight: containerElement.height()
|
|
});
|
|
|
|
// Calculate with current container element height.
|
|
var containerElementHeight = containerElement.height();
|
|
|
|
// Calculate wrapper offset.
|
|
var wrapperOffset = (parseInt(containerElement.css('padding-top'), 10) || 0) + (parseInt(containerElement.css('border-top-width'), 10) || 0) - (wrapperElement.offset().top - containerElement.offset().top);
|
|
|
|
// Normally the ellipsis characters are applied to the last non-empty text-node in the selected element. If the
|
|
// selected element becomes empty during ellipsis iteration, the ellipsis characters cannot be applied to that
|
|
// selected element, and must be deferred to the previous selected element. This parameter keeps track of that.
|
|
var deferAppendEllipsis = false;
|
|
|
|
// Loop through all selected elements in reverse order.
|
|
var selectedElements = wrapperElement;
|
|
if (settings.selector) selectedElements = $(wrapperElement.find(settings.selector).get().reverse());
|
|
|
|
selectedElements.each(function() {
|
|
var selectedElement = $(this),
|
|
originalText = selectedElement.text(),
|
|
ellipsisApplied = false;
|
|
|
|
// Check if we can safely remove the selected element. This saves a lot of unnecessary iterations.
|
|
if (wrapperElement.innerHeight() - selectedElement.innerHeight() > containerElementHeight + wrapperOffset) {
|
|
selectedElement.remove();
|
|
|
|
} else {
|
|
// Reverse recursively remove empty elements, until the element that contains a non-empty text-node.
|
|
removeLastEmptyElements(selectedElement);
|
|
|
|
// If the selected element has not become empty, start ellipsis iterations on the selected element.
|
|
if (selectedElement.contents().length) {
|
|
|
|
// If a deffered ellipsis is still pending, apply it now to the last text-node.
|
|
if (deferAppendEllipsis) {
|
|
getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;
|
|
deferAppendEllipsis = false;
|
|
}
|
|
|
|
// Iterate until wrapper element height is less than or equal to the original container element
|
|
// height plus possible wrapperOffset.
|
|
while (wrapperElement.innerHeight() > containerElementHeight + wrapperOffset) {
|
|
// Apply ellipsis on last text node, by removing one word.
|
|
ellipsisApplied = ellipsisOnLastTextNode(selectedElement);
|
|
|
|
// If ellipsis was succesfully applied, remove any remaining empty last elements and append the
|
|
// ellipsis characters.
|
|
if (ellipsisApplied) {
|
|
removeLastEmptyElements(selectedElement);
|
|
|
|
// If the selected element is not empty, append the ellipsis characters.
|
|
if (selectedElement.contents().length) {
|
|
getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;
|
|
|
|
} else {
|
|
// If the selected element has become empty, defer the appending of the ellipsis characters
|
|
// to the previous selected element.
|
|
deferAppendEllipsis = true;
|
|
selectedElement.remove();
|
|
break;
|
|
}
|
|
|
|
} else {
|
|
// If ellipsis could not be applied, defer the appending of the ellipsis characters to the
|
|
// previous selected element.
|
|
deferAppendEllipsis = true;
|
|
selectedElement.remove();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the "setTitle" property is set to "onEllipsis" and the ellipsis has been applied, or if the
|
|
// property is set to "always", the add the "title" attribute with the original text. Else remove the
|
|
// "title" attribute. When the "setTitle" property is set to "never" we do not touch the "title"
|
|
// attribute.
|
|
if (((settings.setTitle == 'onEllipsis') && ellipsisApplied) || (settings.setTitle == 'always')) {
|
|
selectedElement.attr('title', originalText);
|
|
|
|
} else if (settings.setTitle != 'never') {
|
|
selectedElement.removeAttr('title');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Performs ellipsis on the last text node of the given element. Ellipsis is done by removing a full word.
|
|
*
|
|
* @param {jQuery} element jQuery object containing a single DOM element.
|
|
* @return {boolean} true when ellipsis has been done, false otherwise.
|
|
* @private
|
|
*/
|
|
function ellipsisOnLastTextNode(element) {
|
|
var lastTextNode = getLastTextNode(element);
|
|
|
|
// If the last text node is found, do ellipsis on that node.
|
|
if (lastTextNode.length) {
|
|
var text = lastTextNode.get(0).nodeValue;
|
|
|
|
// Find last space character, and remove text from there. If no space is found the full remaining text is
|
|
// removed.
|
|
var pos = text.lastIndexOf(' ');
|
|
if (pos > -1) {
|
|
text = $.trim(text.substring(0, pos));
|
|
lastTextNode.get(0).nodeValue = text;
|
|
|
|
} else {
|
|
lastTextNode.get(0).nodeValue = '';
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get last text node of the given element.
|
|
*
|
|
* @param {jQuery} element jQuery object containing a single element.
|
|
* @return {jQuery} jQuery object containing a single text node.
|
|
* @private
|
|
*/
|
|
function getLastTextNode(element) {
|
|
if (element.contents().length) {
|
|
|
|
// Get last child node.
|
|
var contents = element.contents();
|
|
var lastNode = contents.eq(contents.length - 1);
|
|
|
|
// If last node is a text node, return it.
|
|
if (lastNode.filter(textNodeFilter).length) {
|
|
return lastNode;
|
|
|
|
} else {
|
|
// Else it is an element node, and we recurse into it.
|
|
|
|
return getLastTextNode(lastNode);
|
|
}
|
|
|
|
} else {
|
|
// If there is no last child node, we append an empty text node and return that. Normally this should not
|
|
// happen, as we test for emptiness before calling getLastTextNode.
|
|
|
|
element.append('');
|
|
var contents = element.contents();
|
|
return contents.eq(contents.length - 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove last empty elements. This is done recursively until the last element contains a non-empty text node.
|
|
*
|
|
* @param {jQuery} element jQuery object containing a single element.
|
|
* @return {boolean} true when elements have been removed, false otherwise.
|
|
* @private
|
|
*/
|
|
function removeLastEmptyElements(element) {
|
|
if (element.contents().length) {
|
|
|
|
// Get last child node.
|
|
var contents = element.contents();
|
|
var lastNode = contents.eq(contents.length - 1);
|
|
|
|
// If last child node is a text node, check for emptiness.
|
|
if (lastNode.filter(textNodeFilter).length) {
|
|
var text = lastNode.get(0).nodeValue;
|
|
text = $.trim(text);
|
|
|
|
if (text == '') {
|
|
// If empty, remove the text node.
|
|
lastNode.remove();
|
|
|
|
return true;
|
|
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
} else {
|
|
// If the last child node is an element node, remove the last empty child nodes on that node.
|
|
while (removeLastEmptyElements(lastNode)) {
|
|
}
|
|
|
|
// If the last child node contains no more child nodes, remove the last child node.
|
|
if (lastNode.contents().length) {
|
|
return false;
|
|
|
|
} else {
|
|
lastNode.remove();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Filter for testing on text nodes.
|
|
*
|
|
* @return {boolean} true when this node is a text node, false otherwise.
|
|
* @this {Node}
|
|
* @private
|
|
*/
|
|
function textNodeFilter() {
|
|
return this.nodeType === 3;
|
|
}
|
|
|
|
/**
|
|
* Add target selector to hash of target selectors. If this is the first target selector added, start the live
|
|
* updater.
|
|
*
|
|
* @param {string} targetSelector the target selector to run the live updater for.
|
|
* @param {Object.<string, string>} settings the settings to apply on this target selector.
|
|
* @private
|
|
*/
|
|
function addToLiveUpdater(targetSelector, settings) {
|
|
// Store target selector with its settings.
|
|
liveUpdatingTargetSelectors[targetSelector] = settings;
|
|
|
|
// If the live updater has not yet been started, start it now.
|
|
if (!liveUpdaterIntervalId) {
|
|
liveUpdaterIntervalId = window.setInterval(function() {
|
|
doLiveUpdater();
|
|
}, 200);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the target selector from the hash of target selectors. It this is the last remaining target selector
|
|
* being removed, stop the live updater.
|
|
*
|
|
* @param {string} targetSelector the target selector to stop running the live updater for.
|
|
* @private
|
|
*/
|
|
function removeFromLiveUpdater(targetSelector) {
|
|
// If the hash contains the target selector, remove it.
|
|
if (liveUpdatingTargetSelectors[targetSelector]) {
|
|
delete liveUpdatingTargetSelectors[targetSelector];
|
|
|
|
// If no more target selectors are in the hash, stop the live updater.
|
|
if (!liveUpdatingTargetSelectors.length) {
|
|
if (liveUpdaterIntervalId) {
|
|
window.clearInterval(liveUpdaterIntervalId);
|
|
liveUpdaterIntervalId = undefined;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Run the live updater. The live updater is periodically run to check if its monitored target selectors require
|
|
* re-applying of the ellipsis.
|
|
*
|
|
* @private
|
|
*/
|
|
function doLiveUpdater() {
|
|
// If the live updater is already running, skip this time. We only want one instance running at a time.
|
|
if (!liveUpdaterRunning) {
|
|
liveUpdaterRunning = true;
|
|
|
|
// Loop through target selectors.
|
|
for (var targetSelector in liveUpdatingTargetSelectors) {
|
|
$(targetSelector).each(function() {
|
|
var containerElement, containerData;
|
|
|
|
containerElement = $(this);
|
|
containerData = containerElement.data('jqae');
|
|
|
|
// If container element dimensions have changed, or the container element is new, run ellipsis on
|
|
// that container element.
|
|
if ((containerData.containerWidth != containerElement.width()) ||
|
|
(containerData.containerHeight != containerElement.height())) {
|
|
ellipsisOnElement(containerElement, liveUpdatingTargetSelectors[targetSelector]);
|
|
}
|
|
});
|
|
}
|
|
|
|
liveUpdaterRunning = false;
|
|
}
|
|
};
|
|
|
|
})(jQuery); |