// http://ryanmorr.com/using-mutation-observers-to-watch-for-element-availability/
// https://github.com/ryanmorr/ready
(function(win) {
'use strict';
var listeners = [],
doc = win.document,
MutationObserver = win.MutationObserver || win.WebKitMutationObserver,
observer;
/*
* Checks a selector for new matching
* elements and invokes the callback
* if one is found
*
* @param {String} selector
* @param {Function} fn
* @api private
*/
function checkSelector(selector, fn) {
var elements = doc.querySelectorAll(selector), i = 0, len = elements.length, element;
for (; i < len; i++) {
element = elements[i];
// Make sure the callback isn't invoked with the
// same element more than once
if (!element.ready) {
element.ready = true;
// Invoke the callback with the element
fn.call(element, element);
}
}
}
/*
* Check all selectors for new elements
* following a change in the DOM
*
* @api private
*/
function checkListeners() {
for (var i = 0, len = listeners.length, listener; i < len; i++) {
listener = listeners[i];
checkSelector(listener.selector, listener.fn);
}
}
/*
* Add a selector to watch for when a matching
* element becomes available in the DOM
*
* @param {String} selector
* @param {Function} fn
* @api public
*/
function ready(selector, fn) {
// Store the selector and callback to be monitored
listeners.push({
selector: selector,
fn: fn
});
if (!observer) {
// Watch for changes in the document
observer = new MutationObserver(checkListeners);
observer.observe(doc.documentElement, {
childList: true,
subtree: true
});
}
// Check if the element is currently in the DOM
checkSelector(selector, fn);
}
// Expose `ready`
if (typeof module !== 'undefined' && module.exports) {
module.exports = ready;
} else if(typeof define === 'function' && define.amd) {
define(function(){ return ready; });
} else {
win['ready'] = ready;
}
})(this);
/*
usage:
<div class="ra-mo-expandable-max-height">
<div class="content" style="max-height: 150px; overflow: hidden;">
... long content ...
</div>
</div>
*/
ready('.ra-mo-expandable-max-height', function(el) {
// For good measure, make sure we only process a given node once:
if(el.raMoExpandableMaxHeightInitialized) { return; }
el.raMoExpandableMaxHeightInitialized = true;
// Do nothing if we contain another ra-mo-expandable-max-height element:
// Not yet decided if this should be done. What would really need to happen in this case is
// to somehow figure out what the max-height of the children are, and then turn them
// off or us off depending on which is smaller. For now, with this commented out, nested
// layers display when the parent is smaller than the child, while may not be ideal, is at
// least arguably the most expected behaviour
//if(el.getElementsByClassName('ra-mo-expandable-max-height')[0]) { return; }
var CssStyle = window.getComputedStyle(el,null); // live object representing current styles
var hasMaxHeight = function() {
var prop = CssStyle.getPropertyValue('max-height');
return prop && prop != '' && prop != 'none' ? true : false;
}
var resetMH;
if(!hasMaxHeight()) {
// Set max-height via special 'mh-PIXELS' class name (e.g. 'mh-235')
for (var i = 0; i < el.classList.length; i++) {
var parts = el.classList[i].split('-');
if(parts.length == 2 && parts[0] == 'mh' && parseInt(parts[1])) {
resetMH = el.style['max-height'];
el.style['max-height'] = parts[1]+'px';
break;
}
}
}
var getRealHeights = function() {
var heights;
if(el.offsetParent) {
heights = [el.clientHeight,el.scrollHeight];
}
else {
// If we're here, el.offsetParent is not defined...
// this means we're not currently rendered/visible, so we can't find out our dimensions. To
// get the browser to tell us what they really are, we have to create a temp hidden element,
// move our element to it, then retrieve the dimensions, then move it back to the original
// parent element and then clean-up/remove the temp element.
var origParent = el.parentElement;
var origSibling = el.nextSibling;
var hidEl = document.createElement('div');
hidEl.style.opacity = 0;
hidEl.style.position = 'absolute';
document.body.appendChild(hidEl);
hidEl.appendChild(el);
heights = [el.clientHeight,el.scrollHeight];
if(origSibling) {
origParent.insertBefore(el,origSibling);
}
else {
origParent.appendChild(el);
}
document.body.removeChild(hidEl);
}
return heights;
}
var getPctShown = function() {
var heights = getRealHeights() || [];
if(!heights[1]) { return 100; }
var pct = Math.floor((heights[0]/heights[1])*10000);
if(! pct) { return 100; }
return pct < 500 ? pct/100 : Math.floor(pct/100);
}
// Do nothing if nothing is hidden:
if(getPctShown() == 100) {
// if we already changed the max-height due to special class we need to set it back
if(typeof resetMH != undefined) { el.style['max-height'] = resetMH; }
return;
}
// This only works if there is a max-height:
if(!hasMaxHeight()) { return; }
var origMH = el.style['max-height'];
var wrapEl = document.createElement('div');
el.parentNode.insertBefore(wrapEl,el);
wrapEl.appendChild(el);
var toggleWrap = document.createElement('div');
toggleWrap.style['padding-top'] = '5px';
toggleWrap.style['text-align'] = 'center';
var toggle = document.createElement('a');
toggle.style['color'] = '#0088cc';
toggleWrap.appendChild(toggle);
var updateToggle = function() {
if(toggle.raMoToggleExpanded) {
el.style['max-height'] = 'none';
toggle.innerHTML = '[ show less ]';
}
else {
el.style['max-height'] = origMH;
var pct = getPctShown();
toggle.innerHTML = pct + '% [ show more ]';
}
}
var toggleFn = function(e) {
e.stopPropagation();
if(toggle.raMoToggleExpanded) {
toggle.raMoToggleExpanded = false;
}
else {
toggle.raMoToggleExpanded = true;
}
updateToggle();
}
toggle.onclick = toggleFn;
updateToggle(); //init
wrapEl.appendChild(toggleWrap);
});