(function (Q, $, window, document, undefined) {
/**
* Q Tools
* @module Q-tools
*/
/**
* Adds a cool "popping" effect to a clickable element when you press it,
* which especially looks nice on touchscreens.
* I originally came up with this effect at Intermagix.
* @class Q clickable
* @constructor
* @param {Object} [options] options for function configuration
* @param {String} [options.className] any CSS classes to add to the container element
* @param {Object|null} [options.shadow] shadow effect configuration, or false for no shadow
* @param {String} [options.shadow.src="{{Q}}/img/shadow3d.png"] url of the shadow image
* @param {Number} [options.shadow.stretch=1.5] stretch
* @param {Number} [options.shadow.dip=0.25] dip
* @param {Number} [options.shadow.opacity=0.5] opacity
* @param {Object} [options.press] press
* @param {Number} [options.press.duration=100] duration
* @param {Number} [options.press.size=0.85] size
* @param {Number} [options.press.opacity=1] opacity
* @param {Q.Animation.ease} [option.press.ease=Q.Animation.ease.linear] ease
* @param {Object} [options.release] release
* @param {Number} [options.release.duration=75] duration
* @param {Number} [options.release.size=1.3] size
* @param {Number} [options.release.opacity=0.5] opacity
* @param {Q.Animation.ease} [options.release.ease=Q.Animation.ease.smooth] ease
* @param {Object} [options.snapback] snapback
* @param {Number} [options.snapback.duration=75] duration
* @param {Q.Animation.ease} [options.snapback.ease=Q.Animation.ease.smooth]
* @param {Object} [options.center] center
* @param {Number} [options.center.x=0.5] x
* @param {Number} [options.center.y=0.5] y
* @param {Boolean} [options.selectable=false]
* @param {Boolean} [options.triggers=null] A jquery selector or jquery of additional elements to trigger the clickable
* @param {Q.Event} [options.onPress] onPress occurs after the user begins a click or tap.
* @param {Q.Event} [options.onRelease] onRelease occurs after the user ends the click or tap. This event receives parameters (event, overElement)
* @param {Q.Event} [options.afterRelease] afterRelease occurs after the user ends the click or tap and the release animation completed. This event receives parameters (evt, overElement)
* @param {Number} [options.cancelDistance=15] cancelDistance
*
*/
Q.Tool.jQuery('Q/clickable',
function _Q_clickable(o) {
var $this = $(this);
var state = $this.state('Q/clickable');
$this.on('invoke.Q_clickable', function () {
$(this).trigger('mousedown');
setTimeout(function () {
$(this).trigger('release')
}, o.press.duration);
});
var originalTime = Date.now();
var timing = state.timing;
setTimeout(function _clickify() {
if (!$this.is(':visible')) {
if (!$this.closest('body').length) {
return;
}
if (timing.waitingPeriod
&& Date.now() - originalTime >= timing.waitingPeriod) {
return;
}
if (timing.waitingInterval) {
setTimeout(_clickify, timing.waitingInterval);
}
return;
}
state.oldStyle = $this.attr('style');
var display = $this.css('display');
var position = $this.css('position');
var p = $this.parent();
if (p.length && p[0].tagName.toUpperCase() === 'TD') {
p.css('position', 'relative');
}
if (!o.selectable) {
$this[0].preventSelections(true);
}
var $triggers;
if (o.triggers) {
$triggers = (typeof o.triggers === 'function')
? $(o.triggers.call($this, o))
: $(o.triggers);
}
var rect = $this[0].getBoundingClientRect();
var csw = Math.ceil(rect.width);
var csh = Math.ceil(rect.height);
// $this.css('height', $this.height()+'px');
var $container = $('<span class="Q_clickable_container" />').css({
'display': (display === 'inline' || display === 'inline-block') ? 'inline-block' : display,
'zoom': 1,
'position': position === 'static' ? 'relative' : position,
'left': position === 'static' ? 0 : $this.css('left'),
'right': position === 'static' ? 'auto' : $this.css('right'),
'top': position === 'static' ? 0 : $this.css('top'),
'margin': '0px',
'padding': '0px',
'border': '0px solid transparent',
'float': $this.css('float'),
// 'z-index': $this.css('z-index') + 1, //10000,
'width': csw+'px',
'height': csh+'px',
'max-width': $this.css('max-width'),
'max-height': $this.css('max-height'),
'overflow': 'visible',
'line-height': $this.css('line-height'),
'vertical-align': $this.css('vertical-align'),
'text-align': $this.css('text-align')
});
if (state.className) {
$container.addClass(state.className);
}
$this.hide(); // to get percentage values, if any, for margins & padding
Q.each(['left', 'right', 'top', 'bottom'], function (i, pos) {
$container.css('margin-'+pos, $this.css('margin-'+pos));
});
$this.show();
$this.css('margin', 0);
$container.insertAfter($this);
// $this.css('height', h);
// if (display === 'inline') {
// $container.html(' ');
// }
if (!o.allowCallout) {
$this.css('-webkit-touch-callout', 'none');
}
if (o.shadow && o.shadow.src) {
var shadow = $('<img />').addClass('Q_clickable_shadow')
.attr('src', Q.url(o.shadow.src));
shadow.css('display', 'none').appendTo($container).load(function () {
var $this = $(this);
var width = csw * o.shadow.stretch;
var height = Math.min($this.height() * width / $this.width(), csh/2);
var toSet = {
'position': 'absolute',
'left': (csw - width)/2+'px',
'top': csh - height * (1-o.shadow.dip)+'px',
'width': width+'px',
'height': height+'px',
'opacity': o.shadow.opacity,
'display': '',
'padding': '0px',
'background': 'none',
'border': '0px',
'outline': '0px'
};
var i, l, props = Object.keys(toSet);
$this.css(toSet);
});
}
var $stretcher = $('<div class="Q_clickable_stretcher" />').css({
'position': 'absolute',
'left': '0px',
'top': '0px',
'width': csw+'px',
'height': csh+'px',
'overflow': 'visible',
'padding': '0px',
'margin': '0px'
}).addClass('Q_clickable_sized')
.appendTo($container);
var triggers = $stretcher;
var width = csw;
var height = csh;
var left = parseInt($container.css('left'));
var top = parseInt($container.css('top'));
var tw = $this.outerWidth();
var th = $this.outerHeight();
$this.appendTo($stretcher).css({
position: 'absolute',
left: '0px',
top: '0px'
// width: csw,
// height: csh
});
var zindex;
var anim = null;
triggers = $stretcher;
if ($triggers && $triggers.length) {
if (!Q.info.isTouchscreen) {
$triggers.mouseenter(function () {
$container.addClass('Q_hover');
}).mouseleave(function () {
$container.removeClass('Q_hover');
});
}
triggers = triggers.add($triggers);
}
var _started = null;
triggers.on('dragstart', function () {
return false;
}).on(Q.Pointer.start, function (evt) {
// if (Q.info.isTouchscreen) {
// evt.preventDefault();
// }
if ($this.css('pointer-events') === 'none') return;
if (_started) return;
_started = this;
setTimeout(function () {
_started = null;
}, 0);
if (Q.Pointer.canceledClick
|| $('.Q_discouragePointerEvents', evt.target).length) {
return;
}
if (!o.selectable) {
triggers[0].preventSelections(true);
}
zindex = $this.css('z-index');
$container.css('z-index', 1000000).addClass('Q_pressed');
Q.handle(o.onPress, $this, [evt, triggers]);
state.animation = Q.Animation.play(function(x, y) {
scale(1 + y * (o.press.size-1));
$this.css('opacity', 1 + y * (o.press.opacity-1));
}, o.press.duration, o.press.ease);
//$this.bind('click.Q_clickable', function () {
// return false;
//});
var pos = null;
$container.parents().each(function () {
var $t = $(this);
$t.data('Q/clickable scrollLeft', $t.scrollLeft());
$t.data('Q/clickable scrollTop', $t.scrollTop());
$t.data('Q/clickable transform', $t.css('transform'));
});
Q.Pointer.onCancelClick.set(function (e, extraInfo) {
if (!extraInfo || !extraInfo.comingFromPointerMovement) {
return;
}
var jq = $(document.elementFromPoint(
extraInfo.toX,
extraInfo.toY
));
var scrolled = false;
$container.removeClass('Q_pressed')
.parents().each(function () {
var $t = $(this);
if ($t.data('Q/clickable scrollLeft') != $t.scrollLeft()
|| $t.data('Q/clickable scrollTop') != $t.scrollTop()
|| $t.data('Q/clickable transform') != $t.css('transform')) {
// there was some scrolling of parent elements
scrolled = true;
return false;
}
});
if (!scrolled) {
var overElement = (jq.closest(triggers).length > 0);
if (overElement) {
return false; // click doesn't have to be canceled
}
}
state.animation && state.animation.pause();
scale(1);
}, 'Q/clickable');
var _released = false;
$(window).add(triggers).on('release.Q_clickable', onRelease);
state.onEndedKey = Q.Pointer.onEnded.set(onRelease, state.onEndedKey);
if (state.preventDefault) {
evt.preventDefault();
}
if (state.stopPropagation) {
evt.stopPropagation();
}
function onRelease (evt) {
if (_released) return;
_released = true;
setTimeout(function () {
_released = false;
}, 0);
$container.removeClass('Q_pressed')
.parents().each(function () {
$(this).removeData(
['Q/clickable scrollTop',
'Q/clickable scrollTop',
'Q/clickable transform']
);
});
var jq;
if (!evt) {
jq = null;
} else if (evt.type === 'release') {
jq = $this;
} else {
var x = Q.Pointer.getX(evt);
var y = Q.Pointer.getY(evt);
jq = $(Q.Pointer.elementFromPoint(x, y));
}
Q.Pointer.onEnded.remove(state.onEndedKey);
var overElement = !Q.Pointer.canceledClick
&& jq && jq.closest(triggers).length > 0;
var factor = scale.factor || 1;
state.animation && state.animation.pause();
if (overElement) {
state.animation = Q.Animation.play(function(x, y) {
scale(factor + y * (o.release.size-factor));
$this.css('opacity', o.press.opacity + y * (o.release.opacity-o.press.opacity));
}, o.release.duration, o.release.ease);
var key = state.animation.onComplete.set(function () {
state.animation = Q.Animation.play(function(x, y) {
scale(o.release.size + y * (1-o.release.size));
$this.css('opacity', 1 + y * (1 - o.release.opacity));
}, o.snapback.duration, o.snapback.ease);
state.animation.onComplete.set(function () {
Q.handle(o.afterRelease, $this, [evt, overElement]);
$this.trigger('afterRelease', $this, evt, overElement);
$container.css('z-index', zindex);
// $this.unbind('click.Q_clickable');
// $this.trigger('click');
state.animation = null;
});
});
} else {
state.animation = Q.Animation.play(function(x, y) {
scale(factor + y * (1-factor));
$this.css('opacity', o.press.opacity + y * (1-o.press.opacity));
// if (x === 1) {
// $this.off('click.Q_clickable');
// }
}, o.release.duration, o.release.ease);
state.animation.onComplete.set(function () {
state.animation = null;
});
setTimeout(function () {
Q.handle(o.afterRelease, $this, [evt, overElement]);
$this.trigger('afterRelease', $this, evt, overElement);
$container.css('z-index', zindex);
state.animation = null;
}, o.release.duration);
}
if (!o.selectable) {
triggers[0].restoreSelections(true);
}
$(window).add(triggers)
.off([Q.Pointer.end, '.Q_clickable'])
.off('release.Q_clickable');
var ts = $this.state('Q/clickable');
if (ts) { // it may have been removed already
Q.handle(ts.onRelease, $this, [evt, overElement, triggers]);
}
};
function scale(factor) {
scale.factor = factor;
if (!Q.info.isIE(0, 8)) {
$stretcher.css({
'-moz-transform': 'scale('+factor+')',
'-webkit-transform': 'scale('+factor+')',
'-o-transform': 'scale('+factor+')',
'-ms-transform': 'scale('+factor+')',
'transform': 'scale('+factor+')'
});
} else if (!scale.started) {
scale.started = true;
$stretcher.css({
left: width * (o.center.x - factor/2) * factor +'px',
top: height * (o.center.y - factor/2) * factor +'px',
zoom: factor
});
scale.started = false;
}
}
}).on(Q.Pointer.click, function (evt) {
if (state.preventDefault) {
evt.preventDefault();
}
if (state.stopPropagation) {
evt.stopPropagation();
}
});
if (timing.renderingInterval) {
var csw2, csh2;
setTimeout(function _update() {
if (state.animation || Date.now() - originalTime >= timing.renderingPeriod) {
return;
}
$stretcher.removeClass('Q_clickable_sized');
var rect = $this[0].getBoundingClientRect();
var csw = Math.ceil(rect.width);
var csh = Math.ceil(rect.height);
if (csw2 != csw || csh2 != csh) {
if (!$this.is(':visible')) {
return;
}
$container.css({
'width': csw,
'height': csh
});
$stretcher.css({
'width': csw+'px',
'height': csh+'px'
});
}
csw2 = csw;
csh2 = csh;
$stretcher.addClass('Q_clickable_sized');
setTimeout(_update, timing.renderingInterval);
}, timing.renderingInterval);
}
}, timing.renderingDelay);
return this;
},
{ // default options
shadow: {
src: "{{Q}}/img/shadow3d.png",
stretch: 1.5,
dip: 0.25,
opacity: 0.5
},
press: {
duration: 100,
size: 0.85,
opacity: 1,
ease: Q.Animation.ease.linear
},
release: {
duration: 75,
size: 1.3,
opacity: 0.5,
ease: Q.Animation.ease.smooth
},
snapback: {
duration: 75,
ease: Q.Animation.ease.smooth
},
center: {
x: 0.5,
y: 0.5
},
timing: {
renderingPeriod: 0,
renderingInterval: 100,
waitingPeriod: 0,
waitingInterval: 100,
renderingDelay: 0
},
selectable: false,
allowCallout: false,
cancelDistance: 15,
preventDefault: false,
stopPropagation: false,
onPress: new Q.Event(),
onRelease: new Q.Event(),
afterRelease: new Q.Event()
},
{
remove: function () {
var $container = this.parent().parent('.Q_clickable_container');
var state = this.state('Q/clickable');
this.attr('style', state.oldStyle || "").insertAfter($container);
this[0].restoreSelections();
Q.Pointer.onEnded.remove(state.onEndedKey);
$container.remove();
}
}
);
})(Q, jQuery, window, document);