diff options
Diffstat (limited to 'addons/website/static/src/js/content')
| -rw-r--r-- | addons/website/static/src/js/content/share.js | 87 | ||||
| -rw-r--r-- | addons/website/static/src/js/content/snippets.animation.js | 593 | ||||
| -rw-r--r-- | addons/website/static/src/js/content/zoomodoo.js | 292 |
3 files changed, 972 insertions, 0 deletions
diff --git a/addons/website/static/src/js/content/share.js b/addons/website/static/src/js/content/share.js new file mode 100644 index 00000000000..4de4dd65117 --- /dev/null +++ b/addons/website/static/src/js/content/share.js @@ -0,0 +1,87 @@ +odoo.define('website.share', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); + +var _t = core._t; +var qweb = core.qweb; + +ajax.loadXML('/website/static/src/xml/website.share.xml', qweb); + +var SocialShare = Widget.extend({ + template: 'website.social_hover', + /** + :param element: element to bind popover + :param social_list: list of social media strings available from + ['facebook','twitter', 'linkedin', 'google-plus'] + :param hashtags: string of hashtags for twitter + */ + init: function (parent) { + this._super.apply(this, arguments); + this.element = parent; + if (parent.data('social')) { + this.social_list = (parent.data('social')).split(); + } else { + this.social_list = ['facebook','twitter', 'linkedin', 'google-plus']; + } + this.hashtags = parent.data('hashtags') || ''; + this.renderElement(); + this.bind_events(); + }, + bind_events: function () { + $('.oe_social_facebook').click($.proxy(this.renderSocial, this, 'facebook')); + $('.oe_social_twitter').click($.proxy(this.renderSocial, this, 'twitter')); + $('.oe_social_linkedin').click($.proxy(this.renderSocial, this, 'linkedin')); + $('.oe_social_google-plus').click($.proxy(this.renderSocial, this, 'google-plus')); + }, + renderElement: function () { + this.$el.append( + qweb.render('website.social_hover', {medias: this.social_list})); + //we need to re-render the element on each hover; popover has the nasty habit of not hiding but completely removing its + // code from the page + //so the binding is lost if we simply trigger on hover. + this.element.popover({ + 'content': this.$el.html(), + 'placement': 'bottom', + 'container': this.element, + 'html': true, + 'trigger': 'manual', + 'animation': false, + }).popover("show").on("mouseleave", function () { + var self = this; + setTimeout(function () { + if (! $(".popover:hover").length) { + $(self).popover("destroy"); + } + }, 200); + }); + }, + renderSocial: function (social) { + var url = document.URL.split(/[?#]/)[0]; // get current url without query string + var title = document.title.split(" | ")[0]; // get the page title without the company name + var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag) + var social_network = { + 'facebook':'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url), + 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url), + 'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title), + 'google-plus': 'https://plus.google.com/share?url=' + encodeURIComponent(url) + }; + if (! _.contains(_.keys(social_network), social)) return; + var window_height = 500, window_width = 500; + window.open(social_network[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + window_height + ',width=' + window_width); + }, +}); + +// Initialize all social_share links when ready +base.ready().done(function () { + $('.oe_social_share').mouseenter(function () { + new SocialShare($(this)); + }); +}); + +return SocialShare; + +}); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js new file mode 100644 index 00000000000..234a43463dd --- /dev/null +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -0,0 +1,593 @@ +odoo.define('website.snippets.animation', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var Class = require('web.Class'); +var core = require('web.core'); +var base = require('web_editor.base'); +var animation = require('web_editor.snippets.animation'); + +var qweb = core.qweb; + +// Initialize fallbacks for the use of requestAnimationFrame, cancelAnimationFrame and performance.now() +window.requestAnimationFrame = window.requestAnimationFrame + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || window.msRequestAnimationFrame + || window.oRequestAnimationFrame + || function (callback) { window.setTimeout(callback, 10); }; + +window.cancelAnimationFrame = window.cancelAnimationFrame + || window.webkitCancelAnimationFrame + || window.mozCancelAnimationFrame + || window.msCancelAnimationFrame + || window.oCancelAnimationFrame + || function (id) { window.clearTimeout(id); }; + +if (!window.performance || !window.performance.now) window.performance = {now: function () { return Date.now(); }}; + +/** + * The AnimationComponent class in in charge of handling one animation loop using the requestAnimationFrame + * feature. This is used by the Animation class below and should not be called directly by an end developer. + * The component contains a simple API, it can be started, stopped, played and paused. + */ +var AnimationComponent = Class.extend({ + init: function (parent, updateCallback, startEvents, $startTarget, options) { + this.parent = parent; // The Animation class instance which is using this component + + options = options || {}; + this._minFrameTime = 1000 / (options.maxFPS || 100); + + // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks + this._updateCallback = updateCallback || function () {}; + this.startEvents = startEvents || "scroll"; + this.$startTarget = $($startTarget || window); + this._getStateCallback = options.getStateCallback + || ((this.startEvents === "scroll" && this.$startTarget[0] === window) ? function () { return window.pageYOffset; } : false) + || ((this.startEvents === "resize" && this.$startTarget[0] === window) ? function () { return {width: window.innerWidth, height: window.innerHeight}; } : false) + || function () { return undefined; }; + this.endEvents = options.endEvents || false; + this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget; + + // Add a namespace to events using the generated uid + this._uid = "_animationComponent" + _.uniqueId(); + this.startEvents = _processEvents(this.startEvents, this._uid); + if (this.endEvents) { + this.endEvents = _processEvents(this.endEvents, this._uid); + } + + function _processEvents(events, namespace) { + events = events.split(" "); + return _.each(events, function (e, index) { + events[index] += ("." + namespace); + }).join(" "); + } + }, + /** + * The start method initializes when the animation must be played and paused and initializes the + * animation first frame. + */ + start: function () { + // Initialize the animation first frame + this._paused = false; + this._rafID = window.requestAnimationFrame((function (t) { + this._update(t); + this._paused = true; + }).bind(this)); + + // Initialize the animation play/pause events + if (this.endEvents) { + /** + * If there are endEvents, the animation should begin playing when the startEvents are triggered + * on the startTarget and pause when the endEvents are triggered on the endTarget. + */ + this.$startTarget.on(this.startEvents, (function (e) { + if (this._paused) { + _.defer(this.play.bind(this, e)); + } + }).bind(this)); + this.$endTarget.on(this.endEvents, (function () { + if (!this._paused) { + _.defer(this.pause.bind(this)); + } + }).bind(this)); + } else { + /** + * Else, if there is no endEvent, the animation should begin playing when the startEvents are + * *continuously* triggered on the startTarget or fully played once. To achieve this, the animation + * begins playing and is scheduled to pause after 2 seconds. If the startEvents are triggered during + * that time, the schedule is delayed for another 2 seconds. This allows to describe an "effect" + * animation (which lasts less than 2 seconds) or an animation which must be playing *during* an event + * (scroll, mousemove, resize, repeated clicks, ...). + */ + var pauseTimer = null; + this.$startTarget.on(this.startEvents, _.throttle((function (e) { + this.play(e); + + clearTimeout(pauseTimer); + pauseTimer = _.delay((function () { + this.pause(); + pauseTimer = null; + }).bind(this), 2000); + }).bind(this), 250, {trailing: false})); + } + }, + /** + * The stop method pauses the animation and destroys the attached events which trigger the + * animation to be played or paused. + */ + stop: function () { + this.$startTarget.off(this.startEvents); + if (this.endEvents) { + this.$endTarget.off(this.endEvents); + } + this.pause(); + }, + /** + * The play method forces the requestAnimationFrame loop to start. + * @param e -> the event which triggered the animation to play + */ + play: function (e) { + this._newEvent = e; + if (!this._paused) return; + this._paused = false; + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + this._lastUpdateTimestamp = undefined; + }, + /** + * The pause method forces the requestAnimationFrame loop to stop. + */ + pause: function () { + if (this._paused) return; + this._paused = true; + window.cancelAnimationFrame(this._rafID); + this._lastUpdateTimestamp = undefined; + }, + /** + * The private _update method is the callback which is repeatedly called by the requestAnimationFrame + * loop. It controls the max fps at which the animation is running and initializes the values that + * the update callback needs to describe the animation (state, elapsedTime, triggered event). + * @param timestamp -> the DOMHighResTimeStamp timestamp at which the function is called + */ + _update: function (timestamp) { + if (this._paused) return; + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + + // Check the elapsed time since the last update callback call. + // Consider it 0 if there is no info of last timestamp and leave this _update call if it was + // called too soon (would overflow the set max FPS). + var elapsedTime = 0; + if (this._lastUpdateTimestamp) { + elapsedTime = timestamp - this._lastUpdateTimestamp; + if (elapsedTime < this._minFrameTime) return; + } + + // Check the new animation state thanks to the get state callback and store its new value. + // If the state is the same as the previous one, leave this _update call, except if there + // is an event which triggered the "play" method again. + var animationState = this._getStateCallback.call(this.parent, elapsedTime, this._newEvent); + if (!this._newEvent && animationState !== undefined && _.isEqual(animationState, this._animationLastState)) return; + this._animationLastState = animationState; + + // Call the update callback with frame parameters + this._updateCallback.call(this.parent, this._animationLastState, elapsedTime, this._newEvent); + this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called + this._newEvent = undefined; // Forget the event which triggered the last "play" call + }, +}); + +/** + * Extend the Animation class to be able to register AnimationComponent's automatically according to + * the methods it defines. Also allows to register them in more conventional ways by extending the + * _prepareComponents method and calling the _addComponent method. + */ +animation.Class.include({ + maxFPS: 100, // The max FPS at which all the automatic animation components will be running by default + + /** + * Extend the start method to prepare animation components and start them. + */ + start: function () { + this._super.apply(this, arguments); + + this._prepareComponents(); + _.each(this._animationComponents, function (component) { + component.start(); + }); + }, + /** + * Extend the start method to stop animation components and destroy them. + */ + stop: function () { + this._super.apply(this, arguments); + + _.each(this._animationComponents, function (component) { + component.stop(); + }); + this._animationComponents = []; + }, + /** + * The private _prepareComponents method is the place to register AnimationComponent instances. + * This can be done by extending this method and calling the _addComponent method in it or by + * defining particulary-named methods in the specialized Animation class. + * + * The automatic creation of components according to method names is working like this: + * - Search for methods which match the pattern: on_startEvent[_startTarget[_off_endEvent[_endTarget]]] where: + * - startEvent: the event name which triggers the animation to begin playing + * - startTarget (optional): the selector (see description below) to find the target where to listen + * for startEvent (if no selector, the window target will be used) + * - endEvent (optional): the event name which triggers the end of the animation (if none is defined, the + * animation will stop after a while, @see AnimationComponent.start) + * - endTarget (optional): the selector (see description below) to find the target where to listen + * for endEvent (if no selector, the startTarget will be used) + * - For all of these methods, register the appropriate animation component with the method implementation used + * as the update callback. The method receives 3 arguments: the animation state, the elapsedTime since last + * update and the event which triggered the animation (undefined if just a new update call without trigger). + * The animation state is undefined by default, the scroll offset for the particular "on_scroll" method and + * and object with width and height for the particular "on_resize" method. There is the possibility to define + * the getState callback of the animation component with this method. For the component created by the definition + * of the "on_abc_def", define the "get_state_for_on_abc_def" (so "get_state_for_" followed by the method name). + * This allows to improve performance even further in some cases. + * + * Selectors mentioned above can be: + * - the "selector" string: this tells the system to use the Animation instance $target element as target for the + * animation component + * - an underscore-separated list of classnames: for example "leftPanel_playBtn", the system will search inside + * the Animation instance $target element for a DOM which matches the selector ".leftPanel .playBtn" + */ + _prepareComponents: function () { + this._animationComponents = []; + + var self = this; + _.each(this.__proto__, function (callback, key) { + if (!_.isFunction(callback)) return; + var m = key.match(/^on_([^_]+)(?:_(.+?))?(?:_off_([^_]+)(?:_(.+))?)?$/); // match the pattern described above + if (!m) return; + + self._addComponent(callback, m[1], _target_from_ugly_selector(m[2]), { + getStateCallback: self["get_state_for_" + key] || undefined, + endEvents: m[3] || undefined, + $endTarget: _target_from_ugly_selector(m[4]), + maxFPS: self.maxFPS, + }); + + // Return the DOM element matching the selector in the form described above. + function _target_from_ugly_selector(selector) { + if (selector) { + if (selector === "selector") { + return self.$target; + } else { + return self.$(_.map(selector.split("_"), function (v) { + return "." + v; + }).join(" ")); + } + } + return undefined; + } + }); + }, + /** + * The private _addComponent method registers a new AnimationComponent according to given parameters + * @see AnimationComponent.init + */ + _addComponent: function (updateCallback, startEvents, $startTarget, options) { + this._animationComponents.push(new AnimationComponent(this, updateCallback, startEvents, $startTarget, options)); + }, +}); + +function load_called_template () { + var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get()); + if (ids_or_xml_ids.length) { + ajax.jsonRpc('/website/multi_render', 'call', { + 'ids_or_xml_ids': ids_or_xml_ids + }).then(function (data) { + for (var k in data) { + var $data = $(data[k]).addClass('o_block_'+k); + $("[data-oe-call='"+k+"']").each(function () { + $(this).replaceWith($data.clone()); + }); + } + }); + } +} + +base.ready().then(function () { + load_called_template(); + if ($(".o_gallery:not(.oe_slideshow)").size()) { + // load gallery modal template + ajax.loadXML('/website/static/src/xml/website.gallery.xml', qweb); + } +}); + +animation.registry.slider = animation.Class.extend({ + selector: ".carousel", + start: function () { + this.$target.carousel(); + return this._super.apply(this, arguments); + }, + stop: function () { + this.$target.carousel('pause'); + this.$target.removeData("bs.carousel"); + return this._super.apply(this, arguments); + }, +}); + +animation.registry.parallax = animation.Class.extend({ + selector: ".parallax", + start: function () { + this._super.apply(this, arguments); + this._rebuild(); + $(window).on("resize.animation_parallax", _.debounce(this._rebuild.bind(this), 500)); + }, + stop: function () { + this._super.apply(this, arguments); + $(window).off(".animation_parallax"); + }, + _rebuild: function () { + // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax) + if (!this.$bg || !this.$bg.length) { + this.$bg = this.$("> .s_parallax_bg"); + if (!this.$bg.length) { + this.$bg = $('<span/>', { + class: 's_parallax_bg' + (this.$target.hasClass('oe_custom_bg') ? ' oe_custom_bg' : ''), + }).prependTo(this.$target); + } + } + var urlTarget = this.$target.css("background-image"); + if (urlTarget !== "none") { + this.$bg.css("background-image", urlTarget); + } + this.$target.css("background-image", "none"); + + // Get parallax speed + this.speed = parseFloat(this.$target.attr("data-scroll-background-ratio") || 0); + + // Reset offset if parallax effect will not be performed and leave + this.$target.toggleClass("s_parallax_is_fixed", this.speed === 1); + if (this.speed === 0 || this.speed === 1) { + this.$bg.css({ + transform: "", + top: "", + bottom: "" + }); + return; + } + + // Initialize parallax data according to snippet and viewport dimensions + this.viewport = document.body.clientHeight - $('#wrapwrap').position().top; + this.visible_area = [this.$target.offset().top]; + this.visible_area.push(this.visible_area[0] + this.$target.innerHeight() + this.viewport); + this.ratio = this.speed * (this.viewport / 10); + + // Provide a "safe-area" to limit parallax + this.$bg.css({ + top: -this.ratio, + bottom: -this.ratio + }); + }, + on_scroll: function (scrollOffset) { + if (this.speed === 0 || this.speed === 1) return; + + // Perform translate if the element is visible only + var vpEndOffset = scrollOffset + this.viewport; + if (vpEndOffset >= this.visible_area[0] && vpEndOffset <= this.visible_area[1]) { + this.$bg.css("transform", "translateY(" + _getNormalizedPosition.call(this, vpEndOffset) + "px)"); + } + + function _getNormalizedPosition(pos) { + // Normalize scroll in a 1 to 0 range + var r = (pos - this.visible_area[1]) / (this.visible_area[0] - this.visible_area[1]); + // Normalize accordingly to current options + return Math.round(this.ratio * (2 * r - 1)); + } + } +}); + +animation.registry.share = animation.Class.extend({ + selector: ".oe_share", + start: function () { + var url_regex = /(\?(?:|.*&)(?:u|url|body)=)(.*?)(&|#|$)/; + var title_regex = /(\?(?:|.*&)(?:title|text|subject)=)(.*?)(&|#|$)/; + var url = encodeURIComponent(window.location.href); + var title = encodeURIComponent($("title").text()); + this.$("a").each(function () { + var $a = $(this); + $a.attr("href", function(i, href) { + return href.replace(url_regex, function (match, a, b, c) { + return a + url + c; + }).replace(title_regex, function (match, a, b, c) { + return a + title + c; + }); + }); + if ($a.attr("target") && $a.attr("target").match(/_blank/i) && !$a.closest('.o_editable').length) { + $a.on('click', function () { + window.open(this.href,'','menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600'); + return false; + }); + } + }); + return this._super.apply(this, arguments); + } +}); + +animation.registry.media_video = animation.Class.extend({ + selector: ".media_iframe_video", + start: function () { + if (!this.$target.has('> iframe').length) { + var editor = '<div class="css_editable_mode_display"> </div>'; + var size = '<div class="media_iframe_video_size"> </div>'; + this.$target.html(editor+size); + } + this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>'); + return this._super.apply(this, arguments); + }, +}); + +animation.registry.ul = animation.Class.extend({ + selector: "ul.o_ul_folded, ol.o_ul_folded", + start: function (editable_mode) { + this.$('.o_ul_toggle_self').off('click').on('click', function (event) { + $(this).toggleClass('o_open'); + $(this).closest('li').find('ul,ol').toggleClass('o_close'); + event.preventDefault(); + }); + this.$('.o_ul_toggle_next').off('click').on('click', function (event) { + $(this).toggleClass('o_open'); + $(this).closest('li').next().toggleClass('o_close'); + event.preventDefault(); + }); + return this._super.apply(this, arguments); + }, +}); + +/** + * This is a fix for apple device (<= IPhone 4, IPad 2) + * Standard bootstrap requires data-toggle='collapse' element to be <a/> tags. Unfortunatly one snippet uses a + * <div/> tag instead. The fix forces an empty click handler on these div, which allows standard bootstrap to work. + * + * This should be removed in a future odoo snippets refactoring. + */ +animation.registry._fix_apple_collapse = animation.Class.extend({ + selector: ".s_faq_collapse [data-toggle='collapse']", + start: function () { + this.$target.off("click._fix_apple_collapse").on("click._fix_apple_collapse", function () {}); + return this._super.apply(this, arguments); + }, +}); + +/* ------------------------------------------------------------------------- +Gallery Animation + +This ads a Modal window containing a slider when an image is clicked +inside a gallery +-------------------------------------------------------------------------*/ +animation.registry.gallery = animation.Class.extend({ + selector: ".o_gallery:not(.o_slideshow)", + start: function () { + this.$el.on("click", "img", this.click_handler); + return this._super.apply(this, arguments); + }, + click_handler : function (event) { + var $cur = $(event.currentTarget); + var edition_mode = ($cur.closest("[contenteditable='true']").size() !== 0); + + // show it only if not in edition mode + if (!edition_mode) { + var urls = [], + idx = undefined, + milliseconds = undefined, + params = undefined, + $images = $cur.closest(".o_gallery").find("img"), + size = 0.8, + dimensions = { + min_width : Math.round( window.innerWidth * size*0.9), + min_height : Math.round( window.innerHeight * size), + max_width : Math.round( window.innerWidth * size*0.9), + max_height : Math.round( window.innerHeight * size), + width : Math.round( window.innerWidth * size*0.9), + height : Math.round( window.innerHeight * size) + }; + + $images.each(function () { + urls.push($(this).attr("src")); + }); + var $img = ($cur.is("img") === true) ? $cur : $cur.closest("img"); + idx = urls.indexOf($img.attr("src")); + + milliseconds = $cur.closest(".o_gallery").data("interval") || false; + params = { + srcs : urls, + index: idx, + dim : dimensions, + interval : milliseconds, + id: _.uniqueId("slideshow_") + }; + var $modal = $(qweb.render('website.gallery.slideshow.lightbox', params)); + $modal.modal({ + keyboard : true, + backdrop : true + }); + $modal.on('hidden.bs.modal', function () { + $(this).hide(); + $(this).siblings().filter(".modal-backdrop").remove(); // bootstrap leaves a modal-backdrop + $(this).remove(); + + }); + $modal.find(".modal-content, .modal-body.o_slideshow").css("height", "100%"); + $modal.appendTo(document.body); + + this.carousel = new animation.registry.gallery_slider($modal.find(".carousel").carousel()); + } + } // click_handler +}); + +animation.registry.gallery_slider = animation.Class.extend({ + selector: ".o_slideshow", + start: function (editable_mode) { + var self = this; + this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel"); + this.$indicator = this.$carousel.find('.carousel-indicators'); + this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden + this.$next = this.$indicator.find('li.fa:last').css('visibility', ''); + var $lis = this.$indicator.find('li:not(.fa)'); + var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space + var realNbPerPage = nbPerPage || 1; + var nbPages = Math.ceil($lis.length / realNbPerPage); + + var index; + var page; + update(); + + function hide() { + $lis.each(function (i) { + $(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage)); + }); + if (editable_mode) { // do not remove DOM in edit mode + return; + } + if (page <= 0) { + self.$prev.detach(); + } else { + self.$prev.prependTo(self.$indicator); + } + if (page >= nbPages - 1) { + self.$next.detach(); + } else { + self.$next.appendTo(self.$indicator); + } + } + + function update() { + index = $lis.index($lis.filter('.active')) || 0; + page = Math.floor(index / realNbPerPage); + hide(); + } + + this.$carousel.on('slide.bs.carousel.gallery_slider', function () { + setTimeout(function () { + var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next'); + var index = $item.index(); + $lis.removeClass("active") + .filter('[data-slide-to="'+index+'"]') + .addClass("active"); + }, 0); + }); + this.$indicator.on('click.gallery_slider', '> li.fa', function () { + page += ($(this).hasClass('o_indicators_left') ? -1 : 1); + page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary + self.$carousel.carousel(page * realNbPerPage); + hide(); + }); + this.$carousel.on('slid.bs.carousel.gallery_slider', update); + }, + stop: function () { + this._super.apply(this, arguments); + + this.$prev.prependTo(this.$indicator); + this.$next.appendTo(this.$indicator); + this.$carousel.off('.gallery_slider'); + this.$indicator.off('.gallery_slider'); + }, +}); + +}); diff --git a/addons/website/static/src/js/content/zoomodoo.js b/addons/website/static/src/js/content/zoomodoo.js new file mode 100644 index 00000000000..0e152d0e6de --- /dev/null +++ b/addons/website/static/src/js/content/zoomodoo.js @@ -0,0 +1,292 @@ +(function ($) { + /** + This code has been more that widely inspired by easyZoom library. + + Copyright 2013 Matt Hinchliffe + + 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. + **/ + + 'use strict'; + + var dw, dh, rw, rh, lx, ly; + + var defaults = { + + // Attribute to retrieve the zoom image URL from. + linkTag: 'a', + linkAttribute: 'data-zoom-image', + + // event to trigger zoom + event: 'click', //or mousenter + + // Prevent clicks on the zoom image link. + preventClicks: true, + + // disable on mobile + disabledOnMobile: true, + + // Callback function to execute before the flyout is displayed. + beforeShow: $.noop, + + // Callback function to execute before the flyout is removed. + beforeHide: $.noop, + + // Callback function to execute when the flyout is displayed. + onShow: $.noop, + + // Callback function to execute when the flyout is removed. + onHide: $.noop, + + // Callback function to execute when the cursor is moved while over the image. + onMove: $.noop + + }; + + /** + * ZoomOdoo + * @constructor + * @param {Object} target + * @param {Object} options (Optional) + */ + function ZoomOdoo(target, options) { + this.$target = $(target); + this.opts = $.extend({}, defaults, options, this.$target.data()); + + if (this.isOpen === undefined) { + this._init(); + } + } + + /** + * Init + * @private + */ + ZoomOdoo.prototype._init = function() { + if (window.outerWidth > 467 || !this.opts.disabledOnMobile) { + this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target; + this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target; + this.$flyout = $('<div class="zoomodoo-flyout" />'); + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { + $attach = this.$target.parents(this.opts.attach); + } + $attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this)); + $attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this)); + this.$target.parent().on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this)); + + if (this.opts.preventClicks) { + this.$target.on('click.zoomodoo', function(e) { e.preventDefault(); }); + } + } + }; + + /** + * Show + * @param {MouseEvent|TouchEvent} e + * @param {Boolean} testMouseOver (Optional) + */ + ZoomOdoo.prototype.show = function(e, testMouseOver) { + var w1, h1, w2, h2; + var self = this; + + if (this.opts.beforeShow.call(this) === false) return; + + if (!this.isReady) { + return this._loadImage(this.$link.attr(this.opts.linkAttribute), function() { + if (self.isMouseOver || !testMouseOver) { + self.show(e); + } + }); + } + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { + $attach = this.$target.parents(this.opts.attach); + } + $attach.parent().append(this.$flyout); + + w1 = this.$target.width(); + h1 = this.$target.height(); + + w2 = this.$flyout.width(); + h2 = this.$flyout.height(); + + dw = this.$zoom.width() - w2; + dh = this.$zoom.height() - h2; + + // For the case where the zoom image is actually smaller than + // the flyout. + if (dw < 0) dw = 0; + if (dh < 0) dh = 0; + + rw = dw / w1; + rh = dh / h1; + + this.isOpen = true; + + this.opts.onShow.call(this); + + if (e) { + this._move(e); + } + }; + + /** + * On enter + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onEnter = function(e) { + var touches = e.originalEvent.touches; + + this.isMouseOver = true; + if (!touches || touches.length === 1) { + e.preventDefault(); + this.show(e, true); + } + }; + + /** + * On move + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onMove = function(e) { + if (!this.isOpen) return; + + e.preventDefault(); + this._move(e); + }; + + /** + * On leave + * @private + */ + ZoomOdoo.prototype._onLeave = function() { + this.isMouseOver = false; + if (this.isOpen) { + this.hide(); + } + }; + + /** + * On load + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onLoad = function(e) { + // IE may fire a load event even on error so test the image dimensions + if (!e.currentTarget.width) return; + + this.isReady = true; + + this.$flyout.html(this.$zoom); + + if (e.data.call) { + e.data(); + } + }; + + + + /** + * Load image + * @private + * @param {String} href + * @param {Function} callback + */ + ZoomOdoo.prototype._loadImage = function(href, callback) { + var zoom = new Image(); + + this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this)); + + zoom.style.position = 'absolute'; + zoom.src = href; + }; + + /** + * Move + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._move = function(e) { + if (e.type.indexOf('touch') === 0) { + var touchlist = e.touches || e.originalEvent.touches; + lx = touchlist[0].pageX; + ly = touchlist[0].pageY; + } else { + lx = e.pageX || lx; + ly = e.pageY || ly; + } + + var offset = this.$target.offset(); + var pt = ly - offset.top; + var pl = lx - offset.left; + var xt = Math.ceil(pt * rh); + var xl = Math.ceil(pl * rw); + + // Close if outside + if (xl < 0 || xt < 0 || xl > dw || xt > dh) { + this.hide(); + } else { + var top = xt * -1; + var left = xl * -1; + + this.$zoom.css({ + top: top, + left: left + }); + + this.opts.onMove.call(this, top, left); + } + + }; + + /** + * Hide + */ + ZoomOdoo.prototype.hide = function() { + if (!this.isOpen) return; + if (this.opts.beforeHide.call(this) === false) return; + + this.$flyout.detach(); + this.isOpen = false; + + this.opts.onHide.call(this); + }; + + + // jQuery plugin wrapper + $.fn.zoomOdoo = function(options) { + return this.each(function() { + var api = $.data(this, 'zoomOdoo'); + + if (!api) { + $.data(this, 'zoomOdoo', new ZoomOdoo(this, options)); + } else if (api.isOpen === undefined) { + api._init(); + } + }); + }; + + // AMD and CommonJS module compatibility + if (typeof define === 'function' && define.amd){ + define(function() { + return ZoomOdoo; + }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = ZoomOdoo; + } + +})(jQuery); |
