(function (Q, $) {
/**
* @module Q-tools
*/
/**
* Implements vertical drawers that work on most modern browsers,
* including ones on touchscreens.
* @class Q drawers
* @constructor
* @param {Object} [options] Override various options for this tool
* @param {Element} [options.container=null] Optional container element for handling scrolling
* @param {Object} [options.initial] Information for the initial animation
* @param {Number} [options.initial.index=1] The index of the drawer to show, either 0 or 1
* @param {Number} [options.initial.delay=0] Delay before starting initial animation
* @param {Number} [options.initial.duration=300] The duration of the initial animation
* @param {Function} [options.initial.ease=Q.Animation.linear] The easing function of the initial animation
* @param {Object} [options.transition] Information for the transition animation
* @param {Number} [options.transition.duration=300] The duration of the transition animation
* @param {Function} [options.transition.ease=Q.Animation.linear] The easing function of the transition animation
* @param {Function} [options.width] Override the function that computes the width of the drawers
* @param {Function} [options.height] Override the function that computes the height drawers tool
* @param {Array} [options.heights=[100,100]] Array of [height0, height1] for drawers that are pinned. Can contain numbers or functions returning numbers, or names of functions.
* @param {Array} [options.placeholders=['','']] Array of [html0, html1] for drawers that are pinned.
* @param {Array} [options.behind=[true,false]] Array of [boolean0, boolean1] to indicate which drawer is behind the others
* @param {Array} [options.bottom=[false,false]] Array of [boolean0, boolean1] to indicate whether to scroll to the bottom of a drawer after switching to it
* @param {Array} [options.triggers=['{{Q}}/img/drawers/up.png', '{{Q}}/img/drawers/down.png']] Array of [src0, src1] for img elements that act as triggers to swap drawers. Set array elements to false to avoid rendering a trigger.
* @param {Object} [options.trigger]] Options for the trigger elements
* @param {Number} [options.trigger.rightMargin=10]] How many pixels from the right side of the drawers
* @param {Number} [options.transition=300]] Number of milliseconds for fading in the trigger images
* @param {Boolean} [options.fullscreen=Q.info.isMobile && Q.info.isAndroid(1000)]] Whether the drawers should take up the whole screen
* @param {Number} [options.foregroundZIndex=50] The z-index of the drawer in the foreground
* @param {Number} [options.beforeSwap=new Q.Event()] Occurs right before drawer swap
* @param {Number} [options.onSwap=new Q.Event()] Occurs right after drawer swap
* @return {Q.Tool}
*/
Q.Tool.define("Q/drawers", function _Q_drawers(options) {
var tool = this;
var state = tool.state;
var $te = $(tool.element);
state.containerOriginal = state.container;
state.swapCount = 0;
Q.addStylesheet('{{Q}}/css/drawers.css', { slotName: 'Q' });
if (state.fullscreen || !state.container) {
state.container = $(tool.element).parents().eq(-3)[0];
}
var $scrolling = state.$scrolling =
$(state.container && !state.fullscreen ? state.container : window);
if ($te.css('position') == 'static') {
$te.css('position', 'relative');
}
if (!state.behind[0]) {
state.bottom[0] = true;
}
if (!state.behind[1]) {
state.bottom[1] = false;
}
state.$drawers = $(this.element).children('.Q_drawers_drawer');
state.currentIndex = 1 - state.initial.index;
state.canceledSwap = null;
var lastScrollingHeight;
setTimeout(function () {
_initialize();
var $column = $(tool.element).closest('.Q_columns_column');
if (!$column.length || $column.hasClass('Q_columns_opened')) {
return;
}
var columns = $column.closest('.Q_tool')[0].Q("Q/columns");
var key = columns.state.onOpen.set(function () {
if (state.fullscreen) {
_initialize();
} else {
_layout();
}
columns.state.onOpen.remove(key);
}, tool);
}, state.initial.delay || 0);
function _initialize() {
state.lastScrollingHeight = _h($scrolling[0].clientHeight || $scrolling.height(), tool);
tool.swap(_layout);
Q.onLayout(tool).set(_layout, tool);
}
$te.parents().each(function () {
var $this = $(this);
$this.data('Q/drawers originalBackground', this.style.background);
this.style.background = 'transparent';
if ($this.is(state.container)) return false;
});
if (Q.info.isMobile) {
this.managePinned();
}
// Accomodate mobile keyboard
if (Q.info.isMobile) {
var scrollTop = null;
state.$drawers.eq(0).on(Q.Pointer.focusin, tool, function () {
scrollTop = $scrolling.scrollTop();
setTimeout(function () {
state.$drawers.eq(1).hide();
state.$placeholder.hide();
state.$trigger.hide();
}, 100);
});
state.$drawers.eq(0).on(Q.Pointer.focusout, tool, function () {
state.$drawers.eq(1).show();
state.$placeholder.show();
state.$trigger.show();
setTimeout(function () {
$scrolling.scrollTop(scrollTop);
}, 0);
});
}
function _layout() {
// to do: fix for cases where element doesn't take up whole screen
if (Q.info.isMobile) {
var w = state.drawerWidth = $(window).width();
$(tool.element).width(w);
state.$drawers.each(function () {
var $this = $(this);
$this.width(w - $this.outerWidth(true) + $this.width());
});
state.$drawers.height();
}
var sh = _h($scrolling[0].clientHeight || $scrolling.height(), tool);
var sHeights = (state.heights instanceof Array)
? state.heights : Q.getObject(state.heights).apply(tool);
var $d0 = state.$drawers.eq(0);
var $d1 = state.$drawers.eq(1);
$d0.css('min-height', sh-sHeights[1]+'px');
$d1.css('min-height', sh-sHeights[0]+'px');
if (state.currentIndex == 0) {
var heightDiff = sh - lastScrollingHeight;
var offset = $d1.offset();
$d1.offset({
left: offset.left,
top: offset.top + heightDiff
});
}
state.lastScrollingHeight = _h($scrolling[0].clientHeight || $scrolling.height(), tool);
}
},
{
initial: {
delay: 0,
duration: 300,
ease: Q.Animation.linear,
index: 1
},
transition: {
duration: 300,
easing: Q.Animation.linear
},
container: null,
width: function () { return $(this.element).width() },
height: function () {
var sp = this.element.scrollingParent();
if (sp === document.documentElement) {
return _h(Q.Pointer.windowHeight(), this);
}
return sp.clientHeight;
},
currentIndex: null,
placeholders: ['', ''],
heights: [100, 100],
behind: [true, false],
bottom: [false, false],
triggers: ['{{Q}}/img/drawers/up.png', '{{Q}}/img/drawers/down.png'],
trigger: { rightMargin: 10, transition: 300 },
fullscreen: Q.info.useFullscreen,
foregroundZIndex: 50,
beforeSwap: new Q.Event(),
onSwap: new Q.Event()
},
{
swap: function (callback, animationStartCallback) {
var tool = this;
var state = tool.state;
if (state.locked) return false;
state.locked = true;
var otherIndex = state.currentIndex;
var index = state.currentIndex = (state.currentIndex + 1) % 2;
var $drawer = state.$drawers.eq(index);
var $otherDrawer = state.$drawers.eq(otherIndex);
var sWidth = (typeof state.width === 'number')
? state.width : Q.getObject(state.width).apply(tool);
var sHeight = (typeof state.height === 'number')
? state.height : Q.getObject(state.height).apply(tool);
var sHeights = (state.heights instanceof Array)
? state.heights : Q.getObject(state.heights).apply(tool);
var $scrolling = $(state.container && !state.fullscreen ? state.container : window);
var behind = state.behind[index];
var fromHeight = behind
? sHeights[index]
: sHeight - sHeights[index];
var toHeight = behind
? sHeight - sHeights[otherIndex]
: sHeights[otherIndex];
var eventName = Q.info.isTouchscreen
? 'touchend.Q_drawers'
: 'mouseup.Q_drawers';
var scrollEventName = 'scroll.Q_drawers';
var scrollingHeight;
// give things a chance to settle down
setTimeout(_setup1, 0);
function _setup1() {
var scrollTop;
var sHeights = (state.heights instanceof Array)
? state.heights : Q.getObject(state.heights).apply(tool);
state.lastScrollingHeight = scrollingHeight = _h(
$scrolling[0].clientHeight || $scrolling.height(),
tool
);
if (state.$pinnedElement) {
scrollTop = state.bottom[otherIndex]
? scrollingHeight - sHeights[otherIndex] - $otherDrawer.height()
: 0;
$scrolling.scrollTop(scrollTop);
}
$scrolling.off(scrollEventName);
$drawer.addClass('Q_drawers_current')
.removeClass('Q_drawers_notCurrent');
$otherDrawer.removeClass('Q_drawers_current')
.addClass('Q_drawers_notCurrent');
if ($(tool.element).css('position') == 'static') {
$(tool.element).css('position', 'relative');
}
// give that scrollTop a chance to take effect
setTimeout(_setup2, 0);
}
function _setup2() {
$drawer.add($otherDrawer).add(state.$placeholder).off(eventName);
function _onSwap() {
state.onSwap.handle.call(tool, state.currentIndex);
Q.handle(callback, tool);
};
state.beforeSwap.handle.call(tool, index);
if (state.$trigger) {
state.$trigger.remove();
}
if (behind) {
_animate([_pin, _addEvents, _onSwap]);
} else {
_pin([_animate, _addEvents, _onSwap]);
}
}
function _pin(callbacks) {
var ae = document.activeElement;
$otherDrawer.css('position', 'relative');
var p = state.drawerPosition;
var w = state.drawerWidth;
var h = state.drawerHeight;
state.drawerPosition = $otherDrawer.css('position');
state.drawerWidth = $otherDrawer.width();
state.drawerHeight = $otherDrawer.height();
state.drawerOffset = $otherDrawer.offset();
var $pe;
var sHeights = (state.heights instanceof Array)
? state.heights : Q.getObject(state.heights).apply(tool);
if ($pe = state.$pinnedElement) {
state.$placeholder.before($pe).remove();
$pe.css({
position: p,
left: 0,
top: 0
});
} else if (!index) {
state.drawerOffset = $scrolling.offset()
|| {left: 0, top: 0};
state.drawerOffset.top += state.bottom[1]
? 0
: scrollingHeight - sHeights[1];
}
var scrollHeight = ($scrolling[0] === window)
? document.documentElement.scrollHeight
: $scrolling[0].scrollHeight;
$scrolling.scrollTop(
state.bottom[index] ? _h(scrollHeight, tool) : 0
);
if ($pe && index) {
state.drawerOffset = $otherDrawer.offset();
}
state.$placeholder = $('<div class="Q_drawers_placeholder" />')
.html(state.placeholders[otherIndex])
.css({
background: 'transparent',
height: (index ? fromHeight : sHeights[1]) + 'px',
cursor: 'pointer'
}).insertAfter($otherDrawer);
state.$placeholder.find('*').css('pointer-events', 'none');
var jqAction = 'insert'+(state.behind[otherIndex]?'Before':'After');
$otherDrawer[jqAction](state.container).css({
position: state.fullscreen ? 'fixed' : 'absolute',
width: sWidth,
zIndex: $(state.container).css('zIndex')
}).offset(state.drawerOffset)
.activate(); // Q.find missed it outside the tool's element
if (state.behind[index]) {
$otherDrawer.css({cursor: 'pointer'});
}
if (state.fullscreen && state.behind[index]) {
$otherDrawer.css({zIndex: state.foregroundZIndex});
}
state.$pinnedElement = $otherDrawer;
if (Q.info.isMobile) {
tool.managePinned();
}
// TODO: adjust height, do not rely on parent of container having
// overflow: hidden
if (!$(ae).closest(state.$otherDrawer).length) {
ae.focus();
}
callbacks[0](callbacks.slice(1));
}
function _animate(callbacks) {
Q.handle(animationStartCallback);
var o = state[state.swapCount ? 'transition' : 'initial'];
if (!state.$placeholder) {
return _continue();
}
if (!o.duration) {
state.$placeholder.height(toHeight);
return _continue();
}
Q.Animation.play(function (x, y) {
state.$placeholder.height(fromHeight + (toHeight-fromHeight)*y);
}, o.duration, o.ease)
.onComplete.set(function () {
this.onComplete.remove("Q/drawers");
_continue();
}, "Q/drawers");
function _continue() {
setTimeout(function () {
callbacks[0](callbacks.slice(1));
}, 0);
}
}
function _addEvents(callbacks) {
var o = state[state.swapCount ? 'transition' : 'initial'];
var $jq = $(behind ? state.$pinnedElement : state.$placeholder);
$jq.off(eventName).on(eventName, function (evt) {
var product = Q.Pointer.movement && Q.Pointer.movement.movingAverageVelocity
? Q.Pointer.movement.movingAverageVelocity.y * (state.currentIndex-0.5)
: 0;
if (!$(evt.target).closest('.Q_discourageDrawerSwap').length
&& product >= 0) {
if (Q.Pointer.which(evt) < 2) {
// don't do it right away, so that other event handlers
// can still access the old state.currentIndex
setTimeout(function () {
if (!state.canceledSwap) {
tool.swap();
}
state.canceledSwap = null;
}, 0);
}
}
});
if (!behind) {
if (Q.info.isTouchscreen) {
$scrolling.off('touchstart.Q_columns')
.off('touchend.Q_columns')
.on('touchstart.Q_columns', function (event) {
state.touchCount = Q.Pointer.touchCount(event);
}).on('touchend.Q_columns', function (event) {
state.touchCount = 0;
});
}
}
state.locked = false;
++state.swapCount;
if (Q.info.isTouchscreen && !(Q.info.isAndroidStock)) {
_addTouchEvents();
}
var src = state.triggers[state.currentIndex];
if (state.$trigger) {
state.$trigger.remove();
}
if (src) {
state.$trigger = $('<img />').attr({
'src': Q.url(src),
'class': 'Q_drawers_trigger ' + 'Q_drawers_trigger_' + state.currentIndex,
'alt': state.currentIndex ? 'reveal bottom drawer' : 'reveal top drawer'
}).insertAfter(state.$drawers[1])
.css({'opacity': 0})
.animate({'opacity': 1}, state.trigger.transition)
.on(Q.Pointer.start, function (evt) {
if (Q.Pointer.which(evt) < 2) {
state.$trigger.hide();
tool.swap();
}
});
var $drawer = tool.state.$drawers.eq(1);
if ($drawer.is(':visible')) {
var left = $drawer.offset().left
- $drawer.offsetParent().offset().left
+ $drawer.outerWidth()
- state.$trigger.outerWidth()
- state.trigger.rightMargin;
var top = $drawer.offset().top
- $drawer.offsetParent().offset().top
- state.$trigger.height() / 2;
state.$trigger.css({
left: left + 'px',
top: top + 'px',
position: state.fullscreen && state.currentIndex == 0
? 'fixed'
: 'absolute'
});
} else {
state.$trigger.hide();
}
}
Q.handle(callbacks[0], tool);
}
function _addTouchEvents() {
var y1, y2;
var anim = null;
var notThisOne = false;
var canShowTrigger = true;
state.$placeholder.on('touchmove', function (e) {
e.preventDefault();
});
state.$drawers.eq(state.currentIndex)
.on('touchstart', true, function (e) {
if (anim) anim.pause();
notThisOne = false;
if (state.currentIndex == 0
|| state.$scrolling.scrollTop() > 0
|| $(e.target).closest('.Q_discourageDrawerSwap').length) {
notThisOne = true;
return;
}
state.$trigger.hide();
canShowTrigger = false;
y1 = Q.Pointer.getY(e);
e.preventDefault();
}).on('touchmove', true, function (e) {
if (notThisOne) return;
y2 = Q.Pointer.getY(e);
if (y1 - y2 > 0) {
state.$scrolling.scrollTop(y1-y2);
state.$drawers.eq(1).css('margin-top', 0);
} else {
state.$drawers.eq(1).css('margin-top', y2-y1);
}
canShowTrigger = false;
}).on('touchend', true, function (e) {
if (notThisOne) return;
if (y2 - y1 > 0) {
tool.swap(null, function () {
var $d = state.$drawers.eq(1);
var mt = parseInt($d.css('margin-top'));
Q.Animation.play(function (x, y) {
$d.css('margin-top', mt*(1-y)+'px');
}, state.transition.duration);
});
} else {
anim = Q.Animation.play(function (x, y) {
if (!Q.Pointer.movement
|| !Q.Pointer.movement.movingAverageVelocity) {
return;
}
var v = Q.Pointer.movement.movingAverageVelocity.y;
var t = state.$scrolling.scrollTop();
var dampening = 1-y;
state.$scrolling.scrollTop(
t-v*this.sinceLastFrame*dampening
);
}, 3000, Q.Animation.ease.power(3.5));
}
y1 = y2 = undefined;
canShowTrigger = true;
});
state.$interval = setInterval(function () {
if (!state.$drawers.eq(1).is(':visible')) {
state.$trigger.hide();
} else if (canShowTrigger && state.$scrolling.scrollTop() === 0) {
var $drawer = tool.state.$drawers.eq(1);
var left = $drawer.offset().left
- $drawer.offsetParent().offset().left
+ $drawer.outerWidth()
- state.$trigger.outerWidth()
- state.trigger.rightMargin;
var top = $drawer.offset().top
- $drawer.offsetParent().offset().top
- state.$trigger.height() / 2;
state.$trigger.show().css({
left: left + 'px',
top: top + 'px'
});
}
}, 300);
}
},
Q: {
beforeRemove: {"Q/drawers": function () {
var state = this.state;
$(this.element).parents().each(function () {
var $this = $(this);
var b = $this.data('Q/drawers originalBackground');
if (b != null) {
this.style.background = b;
$this.removeData('Q/drawers originalBackground');
}
if ($this.is(state.container)) return false;
});
var $pinnedElement = state.$pinnedElement;
if (!$pinnedElement) return;
Q.removeElement($pinnedElement[0], true);
var $scrolling = state.fullscreen ? $(window) : $(state.container);
$scrolling.off(state.scrollEventName)
.off('touchstart.Q_drawers')
.off('touchend.Q_drawers');
if (state.$trigger) {
state.$trigger.remove();
}
clearInterval(state.$interval);
}}
},
managePinned: function () {
var columnIndex;
var tool = this;
var state = tool.state;
$(this.element).parents().each(function () {
var $this = $(this);
if ($this.hasClass('Q_columns_column')) {
columnIndex = $this.attr('data-index');
}
var columns = this.Q("Q/columns");
if (columns) {
if (columns.state.currentIndex != columnIndex
&& state.$pinnedElement
&& state.behind[state.currentIndex]) {
state.$hiddenElements = state.$pinnedElement
.add(state.$trigger).hide();
}
columns.state.beforeOpen.set(function (options, index) {
if (index !== columnIndex
&& state.$pinnedElement
&& state.behind[state.currentIndex]) {
state.$hiddenElements = state.$pinnedElement
.add(state.$trigger).hide();
}
}, tool);
columns.state.beforeClose.set(function (index, indexAfterClose) {
if (indexAfterClose === columnIndex && state.$hiddenElements) {
state.$hiddenElements.show();
state.$hiddenElements = null;
}
}, tool);
return false;
}
});
}
}
);
function _h(scrollingHeight, tool) {
if (!tool.state.fullscreen) {
return scrollingHeight;
}
return scrollingHeight - $(tool.element).position().top;
}
})(Q, jQuery);