<commit_before>import Ember from 'ember';
import DS from 'ember-data';
import AjaxService from 'ember-ajax/services/ajax';

const { assert, inject } = Ember;
const { Model } = DS;

/**
 * Service for making custom requests to a backend API.
 * This service extends ember-ajax but uses ember-data for building the URL.
 *
 * @namespace ember-api-requests
 * @class ApiService
 * @extends AjaxService
 */
export default AjaxService.extend({
    store: inject.service(),

    /**
     * Build an URL based on given path and options.
     *
     * Options:
     * - model (name or instance); to create a prefix for the path
     * - requestType; type of request used for creating the path,
     *   			  defaults to 'findRecord' for instance and 'findAll' for non instance
     * - params; to add extra query parameters to the URL
     * - jsonData; to create a JSON request
     * - any other options will be passed to the native jquery ajax method
     *
     * @method buildURL
     * @param {String} path The path
     * @param {Object} options Options for building the URL (optional)
     * @return {String} The full URL
     */
	buildURL(path, options = {}) {
		let id = null,
            snapshot = null,
            requestType = null,
            modelName = null,
            model = options.model,
            adapter = null,
            result = null;

        if (model instanceof Model) {
            id = model.get('id');
            assert('ember-api-requests expects the model instance to have an id for building an URL.', id);
            requestType = options.requestType || 'findRecord';
            snapshot = model._createSnapshot();
            modelName = model.constructor.modelName || model.constructor.typeKey;
        } else if (typeof model === 'string') {
            modelName = options.model;
            requestType = options.requestType || 'findAll';
        }

        adapter = this.get('store').adapterFor(modelName || 'application');
        result = adapter.buildURL(modelName, id, snapshot, requestType);
        result += path.charAt(0) === '/' ? path : `/${path}`;

        if (options.params) {
            result += `?${Ember.$.param(options.params)}`;
        }

        return result;
	},

	/**
     * @method options
	 * @private
     * @param {String} url
     * @param {Object} options
     * @return {Object}
     */
	options(url, options = {}) {
		url = this.buildURL(url, options);

		if (options.jsonData) {
            options.data = JSON.stringify(options.jsonData);
            options.contentType = options.contentType || 'application/json;charset=UTF-8';
            options.processData = !!options.processData;
        }

		['jsonData','params','model','requestType'].forEach(i => delete options[i]);

        let hasDataType = !!options.dataType;
		let result = this._super(url, options);

		// Only set dataType when defined as option, this differs from ember-ajax
        if (!hasDataType) {
            delete result.dataType;
        }

        return result;
	}
});<commit_msg>Take host into account when building api URL.<commit_after>import Ember from 'ember';
import DS from 'ember-data';
import AjaxService from 'ember-ajax/services/ajax';

const { assert, inject } = Ember;
const { Model } = DS;

/**
 * Service for making custom requests to a backend API.
 * This service extends ember-ajax but uses ember-data for building the URL.
 *
 * @namespace ember-api-requests
 * @class ApiService
 * @extends AjaxService
 */
export default AjaxService.extend({
    store: inject.service(),

    /**
     * Build an URL based on given path and options.
     *
     * Options:
     * - model (name or instance); to create a prefix for the path
     * - requestType; type of request used for creating the path,
     *   			  defaults to 'findRecord' for instance and 'findAll' for non instance
     * - params; to add extra query parameters to the URL
     * - jsonData; to create a JSON request
     * - any other options will be passed to the native jquery ajax method
     *
     * @method buildURL
     * @param {String} path The path
     * @param {Object} options Options for building the URL (optional)
     * @return {String} The full URL
     */
	buildURL(path, options = {}) {
		return this._buildURL(this._buildApiURL(path, options));
	},

    /**
     * @method _buildApiURL
	 * @private
     * @param {String} path
     * @param {Object} options
     * @return {String}
     */
    _buildApiURL(path, options = {}) {
		let id = null,
            snapshot = null,
            requestType = null,
            modelName = null,
            model = options.model,
            adapter = null,
            result = null;

        if (model instanceof Model) {
            id = model.get('id');
            assert('ember-api-requests expects the model instance to have an id for building an URL.', id);
            requestType = options.requestType || 'findRecord';
            snapshot = model._createSnapshot();
            modelName = model.constructor.modelName || model.constructor.typeKey;
        } else if (typeof model === 'string') {
            modelName = options.model;
            requestType = options.requestType || 'findAll';
        }

        adapter = this.get('store').adapterFor(modelName || 'application');
        result = adapter.buildURL(modelName, id, snapshot, requestType);
        result += path.charAt(0) === '/' ? path : `/${path}`;

        if (options.params) {
            result += `?${Ember.$.param(options.params)}`;
        }

        return result;
    },

	/**
     * @method options
	 * @private
     * @param {String} url
     * @param {Object} options
     * @return {Object}
     */
	options(url, options = {}) {
		url = this._buildApiURL(url, options);

		if (options.jsonData) {
            options.data = JSON.stringify(options.jsonData);
            options.contentType = options.contentType || 'application/json;charset=UTF-8';
            options.processData = !!options.processData;
        }

		['jsonData','params','model','requestType'].forEach(i => delete options[i]);

        let hasDataType = !!options.dataType;
		let result = this._super(url, options);

		// Only set dataType when defined as option, this differs from ember-ajax
        if (!hasDataType) {
            delete result.dataType;
        }

        return result;
	}
});<|endoftext|>


<commit_before>// -------------------------------------------------------------------------- \\
// File: ScrollView.js                                                        \\
// Module: ContainerViews                                                     \\
// Requires: Core, Foundation, View                                           \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2014 FastMail Pty Ltd. All rights reserved.                \\
// -------------------------------------------------------------------------- \\

"use strict";

( function ( NS ) {

var ScrollAnimation = NS.Class({

    Extends: NS.Animation,

    duration: 250,

    prepare: function ( coordinates ) {
        var object = this.object,
            startX = this.startX = object.get( 'scrollLeft' ),
            startY = this.startY = object.get( 'scrollTop' ),
            endX = this.endX = coordinates.x || 0,
            endY = this.endY = coordinates.y || 0,
            deltaX = this.deltaX = endX - startX,
            deltaY = this.deltaY = endY - startY;

        return !!( deltaX || deltaY );
    },

    drawFrame: function ( position ) {
        var x = position < 1 ?
                this.startX + ( position * this.deltaX ) : this.endX,
            y = position < 1 ?
                this.startY + ( position * this.deltaY ) : this.endY;
        this.object._scrollTo( x, y );
    }
});

/**
    Class: O.ScrollView

    Extends: O.View

    An O.ScrollView instance is a fixed size container, which can be scrolled if
    its contents overflows the bounds of the view. By default, a scrollbar will
    only be shown for vertical overflow. Set the <O.ScrollView#showScrollbarX>
    property to `true` to show a scrollbar on horizontal overflow as well.
*/
var ScrollView = NS.Class({

    Extends: NS.View,

    /**
        Property: O.ScrollView#showScrollbarX
        Type: Boolean
        Default: false

        Show a scrollbar if the content horizontally overflows the bounds of the
        DOM element representing this view?
    */
    showScrollbarX: false,

    /**
        Property: O.ScrollView#showScrollbarY
        Type: Boolean
        Default: true

        Show a scrollbar if the content vertically overflows the bounds of the
        DOM element representing this view?
    */
    showScrollbarY: true,

    /**
        Property: O.ScrollView#positioning
        Type: String
        Default: 'absolute'

        Overrides default in <O.View#positioning>.
   */
    positioning: 'absolute',

    /**
        Property: O.ScrollView#layout
        Type: Object
        Default:
                {
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%'
                }

        Overrides default in <O.View#layout>.
    */
    layout: NS.View.LAYOUT_FILL_PARENT,

    /**
        Property: O.ScrollView#layerStyles
        Type: Object

        Sets the overflow styles to show the scrollbars.
    */
    layerStyles: function () {
        var styles = NS.View.prototype.layerStyles.call( this );
        styles.overflowX = this.get( 'showScrollbarX' ) ? 'auto' : 'hidden';
        styles.overflowY = this.get( 'showScrollbarY' ) ? 'auto' : 'hidden';
        styles.WebkitOverflowScrolling = 'touch';
        return styles;
    }.property( 'layout', 'allowTextSelection', 'positioning',
        'showScrollbarX', 'showScrollbarY' ),

    /**
        Property: O.ScrollView#keys
        Type: Object
        Default: {}

        Keyboard shortcuts to scroll the view. A map of keyboard shortcut to the
        method name to call on the O.ScrollView instance. These shortcuts will
        automatically be activated/deactivated when the view is added/removed
        to/from the document.

        For example, on the main scroll view for you content, you might set:

            {
                'pagedown': 'scrollPage',
                'pageup': 'reverseScrollPage',
                'space': 'scrollPage',
                'shift-space': 'reverseScrollPage',
                'down': 'scrollLine',
                'up': 'reverseScrollLine'
            }
    */
    keys: {},

    didCreateLayer: function ( layer ) {
        this.scrollLayer = layer;
    },

    didEnterDocument: function () {
        this.get( 'scrollLayer' ).addEventListener( 'scroll', this, false );

        // Add keyboard shortcuts:
        var keys = this.get( 'keys' ),
            shortcuts = NS.ViewEventsController.kbShortcuts,
            key;
        for ( key in keys ) {
            shortcuts.register( key, this, keys[ key ] );
        }

        return ScrollView.parent.didEnterDocument.call( this );
    },

    willLeaveDocument: function () {
        // Remove keyboard shortcuts:
        var keys = this.get( 'keys' ),
            shortcuts = NS.ViewEventsController.kbShortcuts,
            key;
        for ( key in keys ) {
            shortcuts.deregister( key, this, keys[ key ] );
        }

        this.get( 'scrollLayer' ).removeEventListener( 'scroll', this, false );

        return ScrollView.parent.willLeaveDocument.call( this );
    },

    _restoreScroll: function () {
        // Scroll is reset to 0 in some browsers whenever it is removed from the
        // DOM, so we need to set it to what it should be.
        if ( this.get( 'isInDocument' ) ) {
            var layer = this.get( 'scrollLayer' );
            layer.scrollLeft = this.get( 'scrollLeft' );
            layer.scrollTop = this.get( 'scrollTop' );
        }
    }.queue( 'render' ).observes( 'isInDocument' ),

    /**
        Property: O.ScrollView#scrollAnimation
        Type: O.Animation

        An <O.Animation> object to animate scrolling on this object. Normally
        you will not need to interact with this directly, but just set the
        `withAnimation` argument to `true` when you call O.ScrollView#scrollTo.
        However, if you wish to change the duration or easing method, you can do
        so by setting it on this object.
    */
    scrollAnimation: function ( ) {
        return new ScrollAnimation({
            object: this
        });
    }.property(),

    /**
        Method: O.ScrollView#scrollPage

        Scrolls the view down by the view height - 50px.
    */
    scrollPage: function () {
        return this.scrollBy( 0, this.get( 'pxHeight' ) - 50, true );
    },

    /**
        Method: O.ScrollView#reverseScrollPage

        Scrolls the view up by the view height - 50px.
    */
    reverseScrollPage: function () {
        return this.scrollBy( 0, 50 - this.get( 'pxHeight' ), true );
    },

    /**
        Method: O.ScrollView#scrollLine

        Scrolls the view down by 40px.
    */
    scrollLine: function () {
        return this.scrollBy( 0, 40 );
    },

    /**
        Method: O.ScrollView#reverseScrollLine

        Scrolls the view up by 40px.
    */
    reverseScrollLine: function () {
        return this.scrollBy( 0, -40 );
    },

    /**
        Method: O.ScrollView#scrollBy

        Scroll the view by the given number of pixels (use negative values to
        scroll up/left).

        Parameters:
            x             - {Number} The number of pixels to scroll right.
            y             - {Number} The number of pixels to scroll down.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {Boolean} Did the view actually scroll (false if already at end)?
    */
    scrollBy: function ( x, y, withAnimation ) {
        var left = this.get( 'scrollLeft' ),
            top = this.get( 'scrollTop' );
        x += left;
        y += top;

        this.scrollTo( x, y, withAnimation );

        return top !== this.get( 'scrollTop' ) ||
            left !== this.get( 'scrollLeft' );
    },

    /**
        Method: O.ScrollView#scrollToView

        Scroll the view to show a sub-view in the top left of the view.

        Parameters:
            view          - {View} The sub-view to scroll to.
            offset        - {Object} (optional) If supplied, must contain
                            numerical `x` and `y` properties which give the
                            number of pixels to offset the subview from the top
                            left of the scroll view.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {O.ScrollView} Returns self.
    */
    scrollToView: function ( view, offset, withAnimation ) {
        var position = view.getPositionRelativeTo( this );
        return this.scrollTo(
            position.left + ( offset && offset.x || 0 ),
            position.top + ( offset && offset.y || 0 ),
            withAnimation
        );
    },

    /**
        Method: O.ScrollView#scrollBy

        Scroll the view to a given position, where (0,0) represents the scroll
        view fully .

        Parameters:
            x             - {Number} The number of pixels to set the horizontal
                            scroll-position to.
            y             - {Number} The number of pixels to set the vertical
                            scroll-position to.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {O.ScrollView} Returns self.
    */
    scrollTo: function ( x, y, withAnimation ) {
        // Can't have negative scroll values.
        if ( x < 0 ) { x = 0; }
        if ( y < 0 ) { y = 0; }

        var scrollAnimation = this.get( 'scrollAnimation' );
        scrollAnimation.stop();

        if ( withAnimation ) {
            scrollAnimation.animate({
                x: x,
                y: y
            });
        } else {
            this.beginPropertyChanges()
                .set( 'scrollLeft', x )
                .set( 'scrollTop', y )
            .endPropertyChanges()
            .propertyNeedsRedraw( this, 'scroll' );
        }
        return this;
    },

    /**
        Method (private): O.ScrollView#_scrollTo

        Set the new values and immediately redraw. Fast path for animation.
    */
    _scrollTo: function ( x, y ) {
        this.set( 'scrollLeft', x )
            .set( 'scrollTop', y );
        this.redrawScroll();
    },

    /**
        Method: O.ScrollView#redrawScroll

        Redraws the scroll position in the layer to match the view's state.
    */
    redrawScroll: function () {
        var layer = this.get( 'scrollLayer' ),
            x = this.get( 'scrollLeft' ),
            y = this.get( 'scrollTop' );
        layer.scrollLeft = x;
        layer.scrollTop = y;
        // In case we've gone past the end.
        if ( x || y ) {
            NS.RunLoop.queueFn( 'after', this.syncBackScroll, this );
        }
    },

    /**
        Method: O.ScrollView#syncBackScroll

        Parameters:
            event - {Event} (optional) The scroll event object.

        Updates the view properties when the layer scrolls.
    */
    syncBackScroll: function ( event ) {
        if ( this._needsRedraw ) {
            return;
        }
        var layer = this.get( 'scrollLayer' ),
            x = layer.scrollLeft,
            y = layer.scrollTop;
        this.beginPropertyChanges()
            .set( 'scrollLeft', x )
            .set( 'scrollTop', y )
            .endPropertyChanges();
        if ( event ) {
            event.stopPropagation();
            // Don't interpret tap to stop scroll as a real tap.
            if ( NS.Tap ) {
                NS.Tap.cancel();
            }
        }
    }.on( 'scroll' )
});

if ( NS.UA.isIOS ) {
    ScrollView.implement({
        isFixedDimensions: function () {
            var positioning = this.get( 'positioning' );
            return positioning === 'absolute' || positioning === 'fixed';
        }.property( 'positioning' ),

        draw: function ( layer, Element, el ) {
            var isFixedDimensions = this.get( 'isFixedDimensions' ),
                scrollFixerHeight = 1,
                wrapper = null,
                safariVersion = NS.UA.safari,
                children;

            // Render the children.
            children = ScrollView.parent.draw.call( this, layer, Element, el );

            // Trick 1: The dual overflow:scroll view.
            // By default, iOS Safari will scroll the containing scroll view
            // if you are at the top/bottom of an overflow:scroll view. This
            // means it scrolls the window instead of bouncing your scroll view.
            // The dual overflow:scroll fixes this in iOS < 8. You only need
            // this in Safari: if in a UIWebView you can disable the natural
            // scrolling of the window.
            if ( 0 < safariVersion && safariVersion < 8 ) {
                wrapper = this.scrollLayer = el( 'div', {
                    style: 'position:relative;height:100%;' +
                        '-webkit-overflow-scrolling:touch;' +
                        'overflow-x:' +
                            ( this.get( 'showScrollbarX' ) ?
                                'auto;' : 'hidden;' ) +
                        'overflow-y:' +
                            ( this.get( 'showScrollbarY' ) ?
                                'auto;' : 'hidden;' )
                });
                layer.appendChild( wrapper );
                layer = wrapper;
            }

            // Trick 2: Never leave the scroll view at the ends.
            // As Trick 1 doesn't work in Safari on iOS8, we have to use a more
            // crude method: ensure the scrollHeight is at least pxHeight + 2,
            // then make sure scrollTop is never at the absolute end, so there
            // is always room to scroll in both directions. We add a 1px tall
            // empty div at the top of the content so at scrollTop=1px, it
            // looks like it should.
            if ( safariVersion >= 8 ) {
                scrollFixerHeight = 2;
                layer.appendChild(
                    el( 'div', { style: 'height:1px' } )
                );
                this.on( 'scroll', this, '_setNotAtEnd' )
                    .addObserverForKey( 'isInDocument', this, '_setNotAtEnd' );
            }

            // Append the actual children of the scroll view.
            Element.appendChildren( layer, children );

            // Trick 3: Ensuring the view scrolls.
            // Following platform conventions, we assume a fixed height
            // ScrollView should always scroll, regardless of whether the
            // content is taller than the view, whereas a variable height
            // ScrollView just needs to scroll if the content requires it.
            // Therefore, if it's a fixed height view, we add an extra
            // invisible div permanently 1px below the height, so it always
            // has scrollable content.
            if ( isFixedDimensions ) {
                layer.appendChild(
                    el( 'div', {
                        style: 'position:absolute;top:100%;left:0px;' +
                            'width:1px;height:' + scrollFixerHeight + 'px;'
                    })
                );
            }
        },

        _setNotAtEnd: function () {
            if ( this.get( 'isInDocument' ) ) {
                var scrollTop = this.get( 'scrollTop' ),
                    scrollLeft = this.get( 'scrollLeft' );
                if ( !scrollTop ) {
                    this.scrollTo( scrollLeft, 1 );
                } else if ( scrollTop + this.get( 'pxHeight' ) ===
                        this.get( 'layer' ).scrollHeight ) {
                    this.scrollTo( scrollLeft, scrollTop - 1 );
                }
            }
        }.queue( 'after' ),

        preventRootScroll: function ( event ) {
            if ( !this.get( 'isFixedDimensions' ) ) {
                var layer = this.get( 'layer' );
                if ( layer.scrollHeight <= layer.offsetHeight ) {
                    event.preventDefault();
                }
            }
        }.on( 'touchmove' ),

        insertView: function ( view, relativeTo, where ) {
            if ( !relativeTo && this.get( 'isRendered' ) &&
                    where !== 'before' && where !== 'after' ) {
                relativeTo = this.get( 'scrollLayer' ).firstChild;
            }
            return ScrollView.parent.insertView.call(
                this, view, relativeTo, where );
        }
    });
}

NS.ScrollView = ScrollView;

}( this.O ) );
<commit_msg>Fix DOM location of views inserted into iOS ScrollView.<commit_after>// -------------------------------------------------------------------------- \\
// File: ScrollView.js                                                        \\
// Module: ContainerViews                                                     \\
// Requires: Core, Foundation, View                                           \\
// Author: Neil Jenkins                                                       \\
// License: © 2010-2014 FastMail Pty Ltd. All rights reserved.                \\
// -------------------------------------------------------------------------- \\

"use strict";

( function ( NS ) {

var ScrollAnimation = NS.Class({

    Extends: NS.Animation,

    duration: 250,

    prepare: function ( coordinates ) {
        var object = this.object,
            startX = this.startX = object.get( 'scrollLeft' ),
            startY = this.startY = object.get( 'scrollTop' ),
            endX = this.endX = coordinates.x || 0,
            endY = this.endY = coordinates.y || 0,
            deltaX = this.deltaX = endX - startX,
            deltaY = this.deltaY = endY - startY;

        return !!( deltaX || deltaY );
    },

    drawFrame: function ( position ) {
        var x = position < 1 ?
                this.startX + ( position * this.deltaX ) : this.endX,
            y = position < 1 ?
                this.startY + ( position * this.deltaY ) : this.endY;
        this.object._scrollTo( x, y );
    }
});

/**
    Class: O.ScrollView

    Extends: O.View

    An O.ScrollView instance is a fixed size container, which can be scrolled if
    its contents overflows the bounds of the view. By default, a scrollbar will
    only be shown for vertical overflow. Set the <O.ScrollView#showScrollbarX>
    property to `true` to show a scrollbar on horizontal overflow as well.
*/
var ScrollView = NS.Class({

    Extends: NS.View,

    /**
        Property: O.ScrollView#showScrollbarX
        Type: Boolean
        Default: false

        Show a scrollbar if the content horizontally overflows the bounds of the
        DOM element representing this view?
    */
    showScrollbarX: false,

    /**
        Property: O.ScrollView#showScrollbarY
        Type: Boolean
        Default: true

        Show a scrollbar if the content vertically overflows the bounds of the
        DOM element representing this view?
    */
    showScrollbarY: true,

    /**
        Property: O.ScrollView#positioning
        Type: String
        Default: 'absolute'

        Overrides default in <O.View#positioning>.
   */
    positioning: 'absolute',

    /**
        Property: O.ScrollView#layout
        Type: Object
        Default:
                {
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: '100%'
                }

        Overrides default in <O.View#layout>.
    */
    layout: NS.View.LAYOUT_FILL_PARENT,

    /**
        Property: O.ScrollView#layerStyles
        Type: Object

        Sets the overflow styles to show the scrollbars.
    */
    layerStyles: function () {
        var styles = NS.View.prototype.layerStyles.call( this );
        styles.overflowX = this.get( 'showScrollbarX' ) ? 'auto' : 'hidden';
        styles.overflowY = this.get( 'showScrollbarY' ) ? 'auto' : 'hidden';
        styles.WebkitOverflowScrolling = 'touch';
        return styles;
    }.property( 'layout', 'allowTextSelection', 'positioning',
        'showScrollbarX', 'showScrollbarY' ),

    /**
        Property: O.ScrollView#keys
        Type: Object
        Default: {}

        Keyboard shortcuts to scroll the view. A map of keyboard shortcut to the
        method name to call on the O.ScrollView instance. These shortcuts will
        automatically be activated/deactivated when the view is added/removed
        to/from the document.

        For example, on the main scroll view for you content, you might set:

            {
                'pagedown': 'scrollPage',
                'pageup': 'reverseScrollPage',
                'space': 'scrollPage',
                'shift-space': 'reverseScrollPage',
                'down': 'scrollLine',
                'up': 'reverseScrollLine'
            }
    */
    keys: {},

    didCreateLayer: function ( layer ) {
        this.scrollLayer = layer;
    },

    didEnterDocument: function () {
        this.get( 'scrollLayer' ).addEventListener( 'scroll', this, false );

        // Add keyboard shortcuts:
        var keys = this.get( 'keys' ),
            shortcuts = NS.ViewEventsController.kbShortcuts,
            key;
        for ( key in keys ) {
            shortcuts.register( key, this, keys[ key ] );
        }

        return ScrollView.parent.didEnterDocument.call( this );
    },

    willLeaveDocument: function () {
        // Remove keyboard shortcuts:
        var keys = this.get( 'keys' ),
            shortcuts = NS.ViewEventsController.kbShortcuts,
            key;
        for ( key in keys ) {
            shortcuts.deregister( key, this, keys[ key ] );
        }

        this.get( 'scrollLayer' ).removeEventListener( 'scroll', this, false );

        return ScrollView.parent.willLeaveDocument.call( this );
    },

    _restoreScroll: function () {
        // Scroll is reset to 0 in some browsers whenever it is removed from the
        // DOM, so we need to set it to what it should be.
        if ( this.get( 'isInDocument' ) ) {
            var layer = this.get( 'scrollLayer' );
            layer.scrollLeft = this.get( 'scrollLeft' );
            layer.scrollTop = this.get( 'scrollTop' );
        }
    }.queue( 'render' ).observes( 'isInDocument' ),

    /**
        Property: O.ScrollView#scrollAnimation
        Type: O.Animation

        An <O.Animation> object to animate scrolling on this object. Normally
        you will not need to interact with this directly, but just set the
        `withAnimation` argument to `true` when you call O.ScrollView#scrollTo.
        However, if you wish to change the duration or easing method, you can do
        so by setting it on this object.
    */
    scrollAnimation: function ( ) {
        return new ScrollAnimation({
            object: this
        });
    }.property(),

    /**
        Method: O.ScrollView#scrollPage

        Scrolls the view down by the view height - 50px.
    */
    scrollPage: function () {
        return this.scrollBy( 0, this.get( 'pxHeight' ) - 50, true );
    },

    /**
        Method: O.ScrollView#reverseScrollPage

        Scrolls the view up by the view height - 50px.
    */
    reverseScrollPage: function () {
        return this.scrollBy( 0, 50 - this.get( 'pxHeight' ), true );
    },

    /**
        Method: O.ScrollView#scrollLine

        Scrolls the view down by 40px.
    */
    scrollLine: function () {
        return this.scrollBy( 0, 40 );
    },

    /**
        Method: O.ScrollView#reverseScrollLine

        Scrolls the view up by 40px.
    */
    reverseScrollLine: function () {
        return this.scrollBy( 0, -40 );
    },

    /**
        Method: O.ScrollView#scrollBy

        Scroll the view by the given number of pixels (use negative values to
        scroll up/left).

        Parameters:
            x             - {Number} The number of pixels to scroll right.
            y             - {Number} The number of pixels to scroll down.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {Boolean} Did the view actually scroll (false if already at end)?
    */
    scrollBy: function ( x, y, withAnimation ) {
        var left = this.get( 'scrollLeft' ),
            top = this.get( 'scrollTop' );
        x += left;
        y += top;

        this.scrollTo( x, y, withAnimation );

        return top !== this.get( 'scrollTop' ) ||
            left !== this.get( 'scrollLeft' );
    },

    /**
        Method: O.ScrollView#scrollToView

        Scroll the view to show a sub-view in the top left of the view.

        Parameters:
            view          - {View} The sub-view to scroll to.
            offset        - {Object} (optional) If supplied, must contain
                            numerical `x` and `y` properties which give the
                            number of pixels to offset the subview from the top
                            left of the scroll view.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {O.ScrollView} Returns self.
    */
    scrollToView: function ( view, offset, withAnimation ) {
        var position = view.getPositionRelativeTo( this );
        return this.scrollTo(
            position.left + ( offset && offset.x || 0 ),
            position.top + ( offset && offset.y || 0 ),
            withAnimation
        );
    },

    /**
        Method: O.ScrollView#scrollBy

        Scroll the view to a given position, where (0,0) represents the scroll
        view fully .

        Parameters:
            x             - {Number} The number of pixels to set the horizontal
                            scroll-position to.
            y             - {Number} The number of pixels to set the vertical
                            scroll-position to.
            withAnimation - {Boolean} (optional) If true, animate the scroll.

        Returns:
            {O.ScrollView} Returns self.
    */
    scrollTo: function ( x, y, withAnimation ) {
        // Can't have negative scroll values.
        if ( x < 0 ) { x = 0; }
        if ( y < 0 ) { y = 0; }

        var scrollAnimation = this.get( 'scrollAnimation' );
        scrollAnimation.stop();

        if ( withAnimation ) {
            scrollAnimation.animate({
                x: x,
                y: y
            });
        } else {
            this.beginPropertyChanges()
                .set( 'scrollLeft', x )
                .set( 'scrollTop', y )
            .endPropertyChanges()
            .propertyNeedsRedraw( this, 'scroll' );
        }
        return this;
    },

    /**
        Method (private): O.ScrollView#_scrollTo

        Set the new values and immediately redraw. Fast path for animation.
    */
    _scrollTo: function ( x, y ) {
        this.set( 'scrollLeft', x )
            .set( 'scrollTop', y );
        this.redrawScroll();
    },

    /**
        Method: O.ScrollView#redrawScroll

        Redraws the scroll position in the layer to match the view's state.
    */
    redrawScroll: function () {
        var layer = this.get( 'scrollLayer' ),
            x = this.get( 'scrollLeft' ),
            y = this.get( 'scrollTop' );
        layer.scrollLeft = x;
        layer.scrollTop = y;
        // In case we've gone past the end.
        if ( x || y ) {
            NS.RunLoop.queueFn( 'after', this.syncBackScroll, this );
        }
    },

    /**
        Method: O.ScrollView#syncBackScroll

        Parameters:
            event - {Event} (optional) The scroll event object.

        Updates the view properties when the layer scrolls.
    */
    syncBackScroll: function ( event ) {
        if ( this._needsRedraw ) {
            return;
        }
        var layer = this.get( 'scrollLayer' ),
            x = layer.scrollLeft,
            y = layer.scrollTop;
        this.beginPropertyChanges()
            .set( 'scrollLeft', x )
            .set( 'scrollTop', y )
            .endPropertyChanges();
        if ( event ) {
            event.stopPropagation();
            // Don't interpret tap to stop scroll as a real tap.
            if ( NS.Tap ) {
                NS.Tap.cancel();
            }
        }
    }.on( 'scroll' )
});

if ( NS.UA.isIOS ) {
    ScrollView.implement({
        isFixedDimensions: function () {
            var positioning = this.get( 'positioning' );
            return positioning === 'absolute' || positioning === 'fixed';
        }.property( 'positioning' ),

        draw: function ( layer, Element, el ) {
            var isFixedDimensions = this.get( 'isFixedDimensions' ),
                scrollFixerHeight = 1,
                wrapper = null,
                safariVersion = NS.UA.safari,
                children;

            // Render the children.
            children = ScrollView.parent.draw.call( this, layer, Element, el );

            // Trick 1: The dual overflow:scroll view.
            // By default, iOS Safari will scroll the containing scroll view
            // if you are at the top/bottom of an overflow:scroll view. This
            // means it scrolls the window instead of bouncing your scroll view.
            // The dual overflow:scroll fixes this in iOS < 8. You only need
            // this in Safari: if in a UIWebView you can disable the natural
            // scrolling of the window.
            if ( 0 < safariVersion && safariVersion < 8 ) {
                wrapper = this.scrollLayer = el( 'div', {
                    style: 'position:relative;height:100%;' +
                        '-webkit-overflow-scrolling:touch;' +
                        'overflow-x:' +
                            ( this.get( 'showScrollbarX' ) ?
                                'auto;' : 'hidden;' ) +
                        'overflow-y:' +
                            ( this.get( 'showScrollbarY' ) ?
                                'auto;' : 'hidden;' )
                });
                layer.appendChild( wrapper );
                layer = wrapper;
            }

            // Trick 2: Never leave the scroll view at the ends.
            // As Trick 1 doesn't work in Safari on iOS8, we have to use a more
            // crude method: ensure the scrollHeight is at least pxHeight + 2,
            // then make sure scrollTop is never at the absolute end, so there
            // is always room to scroll in both directions. We add a 1px tall
            // empty div at the top of the content so at scrollTop=1px, it
            // looks like it should.
            if ( safariVersion >= 8 ) {
                scrollFixerHeight = 2;
                layer.appendChild(
                    el( 'div', { style: 'height:1px' } )
                );
                this.on( 'scroll', this, '_setNotAtEnd' )
                    .addObserverForKey( 'isInDocument', this, '_setNotAtEnd' );
            }

            // Append the actual children of the scroll view.
            Element.appendChildren( layer, children );

            // Trick 3: Ensuring the view scrolls.
            // Following platform conventions, we assume a fixed height
            // ScrollView should always scroll, regardless of whether the
            // content is taller than the view, whereas a variable height
            // ScrollView just needs to scroll if the content requires it.
            // Therefore, if it's a fixed height view, we add an extra
            // invisible div permanently 1px below the height, so it always
            // has scrollable content.
            if ( isFixedDimensions ) {
                layer.appendChild(
                    el( 'div', {
                        style: 'position:absolute;top:100%;left:0px;' +
                            'width:1px;height:' + scrollFixerHeight + 'px;'
                    })
                );
            }
        },

        _setNotAtEnd: function () {
            if ( this.get( 'isInDocument' ) ) {
                var scrollTop = this.get( 'scrollTop' ),
                    scrollLeft = this.get( 'scrollLeft' );
                if ( !scrollTop ) {
                    this.scrollTo( scrollLeft, 1 );
                } else if ( scrollTop + this.get( 'pxHeight' ) ===
                        this.get( 'layer' ).scrollHeight ) {
                    this.scrollTo( scrollLeft, scrollTop - 1 );
                }
            }
        }.queue( 'after' ),

        preventRootScroll: function ( event ) {
            if ( !this.get( 'isFixedDimensions' ) ) {
                var layer = this.get( 'layer' );
                if ( layer.scrollHeight <= layer.offsetHeight ) {
                    event.preventDefault();
                }
            }
        }.on( 'touchmove' ),

        insertView: function ( view, relativeTo, where ) {
            if ( !relativeTo && this.get( 'isRendered' ) ) {
                relativeTo = this.scrollLayer;
                if ( where === 'top' ) {
                    if ( NS.UA.safari >= 8 ) {
                        relativeTo = relativeTo.firstChild;
                        where = 'after';
                    }
                } else if ( where === 'bottom' ) {
                    if ( this.get( 'isFixedDimensions' ) ) {
                        relativeTo = relativeTo.lastChild;
                        where = 'before';
                    }
                }
            }
            return ScrollView.parent.insertView.call(
                this, view, relativeTo, where );
        }
    });
}

NS.ScrollView = ScrollView;

}( this.O ) );
<|endoftext|>


<commit_before>import {FefStorage} from '../classes/fef-storage';

const STORAGE_KEY = 'srf:rlp:selectable:selected';
const SELECTED_COLLECTION_CLASS = 'js-selected-collection';
const ANIMATION_PART_DURATION = 200;

export function init() {
    $('.js-selectable').each((i, elem) => {
        new SrfSelectableCollection(elem);
    });
}

export class SrfSelectableCollection {

    constructor(element) {
        this.$element = $(element);
        this.$brandingElement = $('.js-selectable-branding-element', this.$element);
        this.$animationWrapper = $('.js-selectable-animation-wrapper', this.$element);
        this.collectionTitle = this.$element.data('title');
        this.collectionURN = this.$element.data('urn');

        this.$groupButtons = $('.js-group-button', this.$element);

        this.$sourceCollections = $(`.js-collection[data-selectable-urn="${this.collectionURN}"]`);

        // Sources are read from a data attribute on the collection element.
        // A source is a plain old JS object (POJO) that contains URN, Title and Link of the landingpage it represents
        this.sources = this.initialSourceConversion(this.$element.data('groups'));

        // The selected source has to be saved temporarily to enable tracking when going "back" via empty state
        this.selectedSource = null;

        this.registerListener();
        this.setInitialState();
        this.removeEmptySourceButtons();
    }

    registerListener() {
        // Going back to the empty state when clicking on the buttons in the source collections
        $('.js-selectable-button', this.$sourceCollections).on('click', () => this.showSelectionElement());

        // Changing between sources
        this.$groupButtons.on('click', (e) => this.changeSelectedSource( $(e.currentTarget).data('source') ));
    }

    setInitialState() {
        if (FefStorage.isLocalStorageAvailable() && FefStorage.hasItem(STORAGE_KEY)) {
            let savedSelections = FefStorage.getItemJsonParsed(STORAGE_KEY);

            let selectedSourceURN = savedSelections[this.collectionURN];

            if (selectedSourceURN && this.connectsToCollection(selectedSourceURN)) {
                this.$element.hide();
                this.toggleCollections(selectedSourceURN);

                let $collection = $(this.$sourceCollections.toArray().find(c => $(c).data('urn') === selectedSourceURN));
                $collection.addClass(SELECTED_COLLECTION_CLASS);
                return;
            }
        }

        this.showSelectionElement(false);
    }

    /**
     * Upon changing a new 'Option', the following things happen:
     * - collection representing the selected landingpage is shown, the others hidden
     * - empty state (this.$element) gets hidden
     * - selection is saved in localstorage
     * - tracking
     *
     * @param {string} sourceURN
     */
    changeSelectedSource(sourceURN) {
        let nextSource = this.sources.find(source => source.urn === sourceURN);

        if (!nextSource) {
            throw new Error(`Source '${ sourceURN }' can't be found in the selectable collection ${ this.collectionURN }.`);
        }

        this.showSource(sourceURN);

        this.saveSelection(sourceURN);

        this.track(this.selectedSource, nextSource);

        this.selectedSource = nextSource;
    }

    toggleCollections(sourceURN) {
        this.$sourceCollections.each((_, collection) => {
            let $collection = $(collection),
                willBeShown = $collection.data('urn') === sourceURN;
            $collection.toggle(willBeShown);
        });
    }

    showSelectionElement(setFocus = true) {
        let $collection = this.$sourceCollections.filter(`.${SELECTED_COLLECTION_CLASS}`).first();
        // TODO: what if no collection?

        this.$element.css({'position': 'absolute', 'height': 0}).show();
        let $brandingWrapper = $collection.find('.js-collection-branding-wrapper');
        let $contentWrapper = $collection.find('.js-collection-content-wrapper');
        let newHeight = this.$animationWrapper.height();
        this.$animationWrapper.css('opacity', 0);

        $contentWrapper.animate({'opacity': 0}, ANIMATION_PART_DURATION, () => {
            $brandingWrapper.animate({'height': newHeight}, ANIMATION_PART_DURATION, () => {
                this.$element.css({'position': '', 'height': ''});
                $collection.hide();
                $contentWrapper.css({'opacity': 1});
                $brandingWrapper.css({'height': ''});

                this.$animationWrapper.animate({'opacity': 1}, ANIMATION_PART_DURATION, () => {
                    $collection.removeClass(SELECTED_COLLECTION_CLASS);
                });
            });
        });
        //this.$element.show();
        //this.toggleCollections(false);
        //this.animateStateChange(() => {});
    }

    showSource(nextSource) {
        this.$sourceCollections.each((_, coll) => $(coll).removeClass(SELECTED_COLLECTION_CLASS));

        let $collection = $(this.$sourceCollections.toArray().find(c => $(c).data('urn') === nextSource));
        $collection.css({'display': 'block', 'position': 'absolute', 'height': 0});
        let $brandingWrapper = $collection.find('.js-collection-branding-wrapper');
        let $contentWrapper = $collection.find('.js-collection-content-wrapper');
        let newHeight = $brandingWrapper.height();
        $contentWrapper.css('opacity', 0);

        this.$animationWrapper.animate({'opacity': 0}, ANIMATION_PART_DURATION, () => {
            this.$animationWrapper.animate({'height': newHeight}, ANIMATION_PART_DURATION, () => {
                $collection.css({'position': '', 'height': ''});
                this.$element.hide();
                this.$animationWrapper.css({'height': '', 'opacity': 1});

                $contentWrapper.animate({'opacity': 1}, ANIMATION_PART_DURATION, () => {
                    $collection.addClass(SELECTED_COLLECTION_CLASS);
                });
            });
        });
        //this.$element.hide();
        //this.toggleCollections(nextSource);
        //this.animateStateChange(() => {});
    }

    /**
     * Animates a state change, i.e. from "empty" to "default" state or back.
     *
     * @param changeContentFn function Function to be executed when the animation is halfway done.
     * @param getElementToFocusOnFn function Function to be executed to get the element to focus on after the animation
     */
    animateStateChange(changeContentFn, getElementToFocusOnFn = false) {
        // Animation:
        // 1. Remove focus (otherwise the flying focus will stay visible)
        // 2. Fade out content
        // 3. set height of collection so it doesn't jump around during next step
        // 4. show/hide/change content, teasers, etc.
        // 5. animate collection's height to the content-wrapper's height
        // 6. fade in content
        // 7. remove fixed height
        // 8. focus on the specified element if desired (=getElementToFocusOnFn is set)

        $(':focus').blur(); // 1

        this.$contentWrapper
            .animate({'opacity': 0}, ANIMATION_PART_DURATION, () => { // 2
                this.$element.height(this.$element.outerHeight()); // 3

                changeContentFn(); // 4

                this.$element.animate({'height': this.$brandingElement.outerHeight(true)}, ANIMATION_PART_DURATION, () => { // 5
                    this.$contentWrapper.animate({'opacity': 1}, ANIMATION_PART_DURATION, () => { // 6
                        this.$element.height(''); // 7
                        // TODO: focus on first teaser or first sourceBtn
                    });
                });
            });
    }

    /**
     * A JSON object with the following structure will be saved in localstorage:
     *   {
     *     "srf:urn:collection:1": "srf:urn:landingpage:1000",
     *     "srf:urn:collection:2": "srf:urn:landingpage:1001",
     *     ...
     *   }
     * where the URNs of the selectable collections are used as the keys and the URNs of the selected sources
     * (= landingpages) as the values. This makes it possible to have the same landingpage used in multiple selectable
     * collections.
     *
     * @param sourceUrn
     */
    saveSelection(sourceUrn) {
        if (FefStorage.isLocalStorageAvailable()) {
            let savedSelections = FefStorage.getItemJsonParsed(STORAGE_KEY);

            if (!savedSelections) {
                savedSelections = {};
            }

            // override the previously saved sourceURN for this collection (or set it if it didn't exist before)
            savedSelections[this.collectionURN] = sourceUrn;

            FefStorage.setItem(STORAGE_KEY, JSON.stringify(savedSelections));
        }
    }

    /**
     * It can happen that a landingpage returns no teasers. In that case we
     * remove the button so that the user can't chose a selection that is empty.
     */
    removeEmptySourceButtons() {
        this.$groupButtons.each((_, button) => {
            if (!this.connectsToCollection($(button).data('source'))) {
                $(button).remove();
            }
        });
    }

    /**
     * Check if a collection with the specified urn exists and has at least 1 teaser.
     *
     * @param {string} sourceUrn URN of landingpage
     * @returns {boolean}
     */
    connectsToCollection(sourceUrn) {
        let collection = this.$sourceCollections.toArray().find(collection => $(collection).data('urn') === sourceUrn);
        return collection && $(collection).find('.js-teaser').length > 0;
    }

    /**
     * Twig's rendering of a PHP array is { 0: {objectA}, 1: {objectB} } where we want [ {objectA}, {objectB} ]
     * If rendered correctly (as an array), return it unmodified.
     *
     * @param rawData
     * @return {Array}
     */
    initialSourceConversion(rawData) {
        if (Array.isArray(rawData)) {
            return rawData;
        }
        return Object.keys(rawData).map(key => rawData[key]);
    }

    /**
     * Trigger a window event so the tracking component will send the supplied data.
     *
     * event_name contains the title of the currently selected landingpage source,
     * event_value contains the title of the landingpage source the collection will change to
     *
     * When this is the first time the source LP is changed, event_name contains DEFAULT
     *
     * @param currentSourceData
     * @param nextSourceData
     */
    track(currentSourceData, nextSourceData) {
        let currentTitle = 'DEFAULT';

        if (currentSourceData) {
            currentTitle = currentSourceData.title;
        }

        // TODO: use const from CMS instead of string
        $(window).trigger('fef.track.interaction', {
            event_source: 'user_setting_changed',
            event_name: `collection_${this.collectionTitle}(${ currentTitle })`,
            event_value: `collection_${this.collectionTitle}(${ nextSourceData.title })`
        });
    }

    /**
     * Simply using .focus() doesn't suffice.
     *
     * @param $element jQuery.Element
     */
    setFocus($element) {
        $element.attr('tabindex', -1).on('blur focusout', () => {
            $element.removeAttr('tabindex');
        }).focus();
    }
}
<commit_msg>cleanup animation js<commit_after>import { FefStorage } from '../classes/fef-storage';
import { setFocus } from '../components/fef-flying-focus';

const STORAGE_KEY = 'srf:rlp:selectable:selected';
const SELECTED_COLLECTION_CLASS = 'js-selected-collection';
const ANIMATION_PART_DURATION = 200;

export function init() {
    $('.js-selectable').each((i, elem) => {
        new SrfSelectableCollection(elem);
    });
}

export class SrfSelectableCollection {

    constructor(element) {
        this.$element = $(element);
        this.$brandingElement = $('.js-selectable-branding-element', this.$element);
        this.$animationWrapper = $('.js-selectable-animation-wrapper', this.$element);
        this.collectionTitle = this.$element.data('title');
        this.collectionURN = this.$element.data('urn');

        this.$groupButtons = $('.js-group-button', this.$element);

        this.$sourceCollections = $(`.js-collection[data-selectable-urn="${this.collectionURN}"]`);

        // Sources are read from a data attribute on the collection element.
        // A source is a plain old JS object (POJO) that contains URN, Title and Link of the landingpage it represents
        this.sources = this.initialSourceConversion(this.$element.data('groups'));

        // The selected source has to be saved temporarily to enable tracking when going "back" via empty state
        this.selectedSource = null;

        this.registerListener();
        this.setInitialState();
        this.removeEmptySourceButtons();
    }

    registerListener() {
        // Going back to the empty state when clicking on the buttons in the source collections
        $('.js-selectable-button', this.$sourceCollections).on('click', () => this.showSelectionElement());

        // Changing between sources
        this.$groupButtons.on('click', (e) => this.changeSelectedSource( $(e.currentTarget).data('source') ));
    }

    setInitialState() {
        if (FefStorage.isLocalStorageAvailable() && FefStorage.hasItem(STORAGE_KEY)) {
            let savedSelections = FefStorage.getItemJsonParsed(STORAGE_KEY);

            let selectedSourceURN = savedSelections[this.collectionURN];

            if (selectedSourceURN && this.connectsToCollection(selectedSourceURN)) {
                this.$element.hide();
                this.toggleCollections(selectedSourceURN);

                let $collection = $(this.$sourceCollections.toArray().find(c => $(c).data('urn') === selectedSourceURN));
                $collection.addClass(SELECTED_COLLECTION_CLASS);
                return;
            }
        }

        this.showSelectionElement(false);
    }

    /**
     * Upon changing a new 'Option', the following things happen:
     * - collection representing the selected landingpage is shown, the others hidden
     * - empty state (this.$element) gets hidden
     * - selection is saved in localstorage
     * - tracking
     *
     * @param {string} sourceURN
     */
    changeSelectedSource(sourceURN) {
        let nextSource = this.sources.find(source => source.urn === sourceURN);

        if (!nextSource) {
            throw new Error(`Source '${ sourceURN }' can't be found in the selectable collection ${ this.collectionURN }.`);
        }

        this.showSource(sourceURN);

        this.saveSelection(sourceURN);

        this.track(this.selectedSource, nextSource);

        this.selectedSource = nextSource;
    }

    toggleCollections(sourceURN) {
        this.$sourceCollections.each((_, collection) => {
            let $collection = $(collection),
                willBeShown = $collection.data('urn') === sourceURN;
            $collection.toggle(willBeShown);
        });
    }

    showSelectionElement(shouldFocus = true) {
        let $collection = this.$sourceCollections.filter(`.${SELECTED_COLLECTION_CLASS}`).first();
        if (!$collection) {
            this.$element.show();
            return;
        }

        this.$element.css({'position': 'absolute', 'height': 0}).show();
        let $contentWrapper = $collection.find('.js-collection-content-wrapper');
        let newHeight = this.$animationWrapper.height();
        this.$animationWrapper.css('opacity', 0);

        if (shouldFocus) {
            $(':focus').blur();
        }

        $contentWrapper.animate({'opacity': 0}, ANIMATION_PART_DURATION, () => {
            $contentWrapper.animate({'height': newHeight}, ANIMATION_PART_DURATION, 'easeInOutSine', () => {
                this.$element.css({'position': '', 'height': ''});
                $collection.hide();
                $contentWrapper.css({'opacity': 1, 'height': ''});

                this.$animationWrapper.animate({'opacity': 1}, ANIMATION_PART_DURATION, () => {
                    $collection.removeClass(SELECTED_COLLECTION_CLASS);

                    if (shouldFocus) {
                        setFocus(this.$groupButtons.first());
                    }
                });
            });
        });
    }

    showSource(nextSource) {
        this.$sourceCollections.each((_, coll) => $(coll).removeClass(SELECTED_COLLECTION_CLASS));

        let $collection = $(this.$sourceCollections.toArray().find(c => $(c).data('urn') === nextSource));
        $collection.css({'display': 'block', 'position': 'absolute', 'height': 0});
        let $contentWrapper = $collection.find('.js-collection-content-wrapper');
        let newHeight = $contentWrapper.height();
        $contentWrapper.css('opacity', 0);

        $(':focus').blur();

        this.$animationWrapper.animate({'opacity': 0}, ANIMATION_PART_DURATION, () => {
            this.$animationWrapper.animate({'height': newHeight}, ANIMATION_PART_DURATION, 'easeInOutSine', () => {
                $collection.css({'position': '', 'height': ''});
                this.$element.hide();
                this.$animationWrapper.css({'height': '', 'opacity': 1});

                $contentWrapper.animate({'opacity': 1}, ANIMATION_PART_DURATION, () => {
                    $collection.addClass(SELECTED_COLLECTION_CLASS);

                    setFocus($collection.find('.teaser__main').first());
                });
            });
        });
    }

    /**
     * A JSON object with the following structure will be saved in localstorage:
     *   {
     *     "srf:urn:collection:1": "srf:urn:landingpage:1000",
     *     "srf:urn:collection:2": "srf:urn:landingpage:1001",
     *     ...
     *   }
     * where the URNs of the selectable collections are used as the keys and the URNs of the selected sources
     * (= landingpages) as the values. This makes it possible to have the same landingpage used in multiple selectable
     * collections.
     *
     * @param sourceUrn
     */
    saveSelection(sourceUrn) {
        if (FefStorage.isLocalStorageAvailable()) {
            let savedSelections = FefStorage.getItemJsonParsed(STORAGE_KEY);

            if (!savedSelections) {
                savedSelections = {};
            }

            // override the previously saved sourceURN for this collection (or set it if it didn't exist before)
            savedSelections[this.collectionURN] = sourceUrn;

            FefStorage.setItem(STORAGE_KEY, JSON.stringify(savedSelections));
        }
    }

    /**
     * It can happen that a landingpage returns no teasers. In that case we
     * remove the button so that the user can't chose a selection that is empty.
     */
    removeEmptySourceButtons() {
        this.$groupButtons.each((_, button) => {
            if (!this.connectsToCollection($(button).data('source'))) {
                $(button).remove();
            }
        });
    }

    /**
     * Check if a collection with the specified urn exists and has at least 1 teaser.
     *
     * @param {string} sourceUrn URN of landingpage
     * @returns {boolean}
     */
    connectsToCollection(sourceUrn) {
        let collection = this.$sourceCollections.toArray().find(collection => $(collection).data('urn') === sourceUrn);
        return collection && $(collection).find('.js-teaser').length > 0;
    }

    /**
     * Twig's rendering of a PHP array is { 0: {objectA}, 1: {objectB} } where we want [ {objectA}, {objectB} ]
     * If rendered correctly (as an array), return it unmodified.
     *
     * @param rawData
     * @return {Array}
     */
    initialSourceConversion(rawData) {
        if (Array.isArray(rawData)) {
            return rawData;
        }
        return Object.keys(rawData).map(key => rawData[key]);
    }

    /**
     * Trigger a window event so the tracking component will send the supplied data.
     *
     * event_name contains the title of the currently selected landingpage source,
     * event_value contains the title of the landingpage source the collection will change to
     *
     * When this is the first time the source LP is changed, event_name contains DEFAULT
     *
     * @param currentSourceData
     * @param nextSourceData
     */
    track(currentSourceData, nextSourceData) {
        let currentTitle = 'DEFAULT';

        if (currentSourceData) {
            currentTitle = currentSourceData.title;
        }

        // TODO: use const from CMS instead of string
        $(window).trigger('fef.track.interaction', {
            event_source: 'user_setting_changed',
            event_name: `collection_${this.collectionTitle}(${ currentTitle })`,
            event_value: `collection_${this.collectionTitle}(${ nextSourceData.title })`
        });
    }
}
<|endoftext|>


<commit_before>import React from 'react';
import storyRouter from 'storybook-router';
import {withInfo} from '@storybook/addon-info';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import {mount} from 'enzyme';
import {assert} from 'chai';
import any from '@travi/any';
import {storiesOf, specs} from '../.storybook/facade-storybook';
import {describe, it} from '../.storybook/facade-mocha';
import ResourceList from '../src/resources/list/maybe-list';

storiesOf('Resource List', module)
  .addDecorator(story => (
    <MuiThemeProvider>
      {story()}
    </MuiThemeProvider>
  ))
  .addDecorator(storyRouter())
  .add(
    'empty list',
    withInfo('this is the empty state')(() => {
      const list = <ResourceList resourceType="foo" resources={[]} loading={false} />;

      specs(() => describe('empty list', () => {
        it('should show a message explaining that no resources are available', () => {
          const wrapper = mount(list);
          const alert = wrapper.find('p');

          assert.equal(alert.text(), 'No foo are available');
        });
      }));

      return list;
    })
  )
  .add(
    'loading',
    withInfo('this is the loading state')(() => <ResourceList resourceType="foo" resources={[]} loading={true} />)
  )
  .add(
    'rides list',
    withInfo('list of rides')(() => (
      <ResourceList
        resourceType="rides"
        loading={false}
        resources={[
          {id: 1, displayName: 'corvette', links: {self: {href: any.url()}}},
          {id: 2, displayName: 'truck', links: {self: {href: any.url()}}},
          {id: 3, displayName: 'camaro', links: {self: {href: any.url()}}}
        ]}
      />
    ))
  )
  .add(
    'users list',
    withInfo('list of users')(() => (
      <ResourceList
        resourceType="users"
        loading={false}
        resources={[
          {
            id: 1,
            displayName: 'Matt Travi',
            thumbnail: {src: 'https://www.gravatar.com/avatar/f69785efc7d990da20f1ab49fc2a6648?size=32'},
            links: {}
          }
        ]}
      />
    ))
  );
<commit_msg>fix(persons): switched from users to persons<commit_after>import React from 'react';
import storyRouter from 'storybook-router';
import {withInfo} from '@storybook/addon-info';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import {mount} from 'enzyme';
import {assert} from 'chai';
import any from '@travi/any';
import {storiesOf, specs} from '../.storybook/facade-storybook';
import {describe, it} from '../.storybook/facade-mocha';
import ResourceList from '../src/resources/list/maybe-list';

storiesOf('Resource List', module)
  .addDecorator(story => (
    <MuiThemeProvider>
      {story()}
    </MuiThemeProvider>
  ))
  .addDecorator(storyRouter())
  .add(
    'empty list',
    withInfo('this is the empty state')(() => {
      const list = <ResourceList resourceType="foo" resources={[]} loading={false} />;

      specs(() => describe('empty list', () => {
        it('should show a message explaining that no resources are available', () => {
          const wrapper = mount(list);
          const alert = wrapper.find('p');

          assert.equal(alert.text(), 'No foo are available');
        });
      }));

      return list;
    })
  )
  .add(
    'loading',
    withInfo('this is the loading state')(() => <ResourceList resourceType="foo" resources={[]} loading={true} />)
  )
  .add(
    'rides list',
    withInfo('list of rides')(() => (
      <ResourceList
        resourceType="rides"
        loading={false}
        resources={[
          {id: 1, displayName: 'corvette', links: {self: {href: any.url()}}},
          {id: 2, displayName: 'truck', links: {self: {href: any.url()}}},
          {id: 3, displayName: 'camaro', links: {self: {href: any.url()}}}
        ]}
      />
    ))
  )
  .add(
    'persons list',
    withInfo('list of persons')(() => (
      <ResourceList
        resourceType="persons"
        loading={false}
        resources={[
          {
            id: 1,
            displayName: 'Matt Travi',
            thumbnail: {src: 'https://www.gravatar.com/avatar/f69785efc7d990da20f1ab49fc2a6648?size=32'},
            links: {}
          }
        ]}
      />
    ))
  );
<|endoftext|>


<commit_before>/**
 * Mixin which adds basic filtering functionality to SelectBox and MultiSelectBox
 */
qx.Mixin.define("qxex.ui.form.MSelectBoxFilter", {

  construct : function()
  {

    this.addListener("keyinput", this._MonKeyInput, this);
    this.addListener("keypress", this._MonKeyPress, this);

    this.MIN_LIST_ITEMS_TO_SHOW_FILTER = 6; //we only display filter for 6 or more items

    this.showFilter=false;

    //child controls
    var list = this.getChildrenContainer(); //same as this.getChildControl("list"); , is in AbstractSelectBox
    var popup = this.getChildControl("popup");

    this.filterTextField = new qx.ui.form.TextField();

    // we fill the textfield by forwarding keyinputs and keypress (delete) to it so we dont have to give it focus,
    // which would interfere with the blur events
    this.filterTextField.setAnonymous(true);
    this.filterTextField.setKeepFocus(true);

    this.filterTextField.addListener("input",function(e){
         var filterText = e.getData();
         this.filterList(filterText);
    },this);

    this.filterLabel = new qx.ui.basic.Label();

    var box = new qx.ui.container.Composite(new qx.ui.layout.HBox());
    box.add(this.filterTextField,{flex:1});
    box.add(this.filterLabel);

    this.filterTextField.setPlaceholder(this.tr("type to filter, backspace to clear"));
    box.setToolTipText(this.trc("tooltip","Start typing to filter list entries. Use backspace to undo filtering character-wise."))

    popup.addBefore(box,list);

    this.helpLabelEmpty = new qx.ui.basic.Label(this.tr("Press backspace to clear the filter"));
    this.helpLabelEmpty.setTextColor("red");
    this.helpLabelEmpty.exclude();

    popup.addBefore(this.helpLabelEmpty,list);

    popup.addListener("changeVisibility",function(e){
      var l = this.getChildren().length;
      if(l>=this.MIN_LIST_ITEMS_TO_SHOW_FILTER){
        this.showFilter=true;
        box.show();
      }else{
        this.showFilter=false;
        box.exclude();
      }
    },this);

  },

  members :
  {

    filterList : function(filterText){
      if(!this.showFilter) return;
      var filterTextLower = filterText.toLowerCase();
      var count=0;
      var children = this.getChildren();
      children.forEach(function(item){
          var show = item.getLabel().toLowerCase().indexOf(filterTextLower) > -1;
          if(show){
            item.show();
            count++;
          }
          else item.exclude();
      },this);

      var all=children.length;

      this.filterLabel.setValue(count + "/" + all);
      if(count==0){
        this.helpLabelEmpty.show();
      }else{
        this.helpLabelEmpty.exclude();
      }
    },

    _MonKeyInput : function(e)
    {
      var old = this.filterTextField.getValue() || "";
      var newVal = old+e.getChar();
      this.filterTextField.setValue(newVal);
      this.filterList(newVal);
      //forward to the filter:

//       var clone = e.clone();
//       clone.setTarget(this.filterTextField);
//       clone.setBubbles(false);
//       this.filterTextField.dispatchEvent(clone);
    },

        // overridden
    _MonKeyPress : function(e)
    {
      var iden = e.getKeyIdentifier();
      if(iden=="Backspace"){
        var old = this.filterTextField.getValue() || "";
        var newVal = old.slice(0, -1);
        this.filterTextField.setValue(newVal);
        this.filterList(newVal);

        e.preventDefault();
      }
    }
  }
});<commit_msg>added function setFilterText to programmaticaly filter selectbox to public API of MSelectBoxFilter<commit_after>/**
 * Mixin which adds basic filtering functionality to SelectBox and MultiSelectBox
 */
qx.Mixin.define("qxex.ui.form.MSelectBoxFilter", {

  construct : function()
  {

    this.addListener("keyinput", this._MonKeyInput, this);
    this.addListener("keypress", this._MonKeyPress, this);

    //child controls
    var list = this.getChildrenContainer(); //same as this.getChildControl("list"); , is in AbstractSelectBox
    var popup = this.getChildControl("popup");

    this.__filterTextField = new qx.ui.form.TextField();

    // we fill the textfield by forwarding keyinputs and keypress (delete) to it so we dont have to give it focus,
    // which would interfere with the blur events
    this.__filterTextField.setAnonymous(true);
    this.__filterTextField.setKeepFocus(true);

    this.__filterTextField.addListener("input",function(e){
         var filterText = e.getData();
         this.__filterList(filterText);
    },this);

    this.__filterLabel = new qx.ui.basic.Label();

    var box = new qx.ui.container.Composite(new qx.ui.layout.HBox());
    box.add(this.__filterTextField,{flex:1});
    box.add(this.__filterLabel);

    this.__filterTextField.setPlaceholder(this.tr("type to filter, backspace to clear"));
    box.setToolTipText(this.trc("tooltip","Start typing to filter list entries. Use backspace to undo filtering character-wise."))

    popup.addBefore(box,list);

    this.helpLabelEmpty = new qx.ui.basic.Label(this.tr("Press backspace to clear the filter"));
    this.helpLabelEmpty.setTextColor("red");
    this.helpLabelEmpty.exclude();

    popup.addBefore(this.helpLabelEmpty,list);

    popup.addListener("changeVisibility",function(e){
      var l = this.getChildren().length;
      if(l>=this.MIN_LIST_ITEMS_TO_SHOW_FILTER){
        this.__showFilter=true;
        box.show();
      }else{
        this.__showFilter=false;
        box.exclude();
      }
    },this);

  },

  members :
  {

    //PUBLIC

    MIN_LIST_ITEMS_TO_SHOW_FILTER : 6, //we only display filter for 6 or more items

    setFilterText : function(filterText){
      MIN_LIST_ITEMS_TO_SHOW_FILTER = 0; //if we set programmatically, we want to be able to remove filter again even if we have less than 5 entries!
      this.__filterTextField.setValue(filterText);
      this.__filterList(filterText);
    },

    //PRIVATE

    __filterTextField : null,
    __filterLabel : null,
    __showFilter : false,

    __filterList : function(filterText){
      if(!this.__showFilter) return;
      var filterTextLower = filterText.toLowerCase();
      var count=0;
      var children = this.getChildren();
      children.forEach(function(item){
          var show = item.getLabel().toLowerCase().indexOf(filterTextLower) > -1;
          if(show){
            item.show();
            count++;
          }
          else item.exclude();
      },this);

      var all=children.length;

      this.__filterLabel.setValue(count + "/" + all);
      if(count==0){
        this.helpLabelEmpty.show();
      }else{
        this.helpLabelEmpty.exclude();
      }
    },

    _MonKeyInput : function(e)
    {
      var old = this.__filterTextField.getValue() || "";
      var newVal = old+e.getChar();
      this.__filterTextField.setValue(newVal);
      this.__filterList(newVal);
      //forward to the filter:

//       var clone = e.clone();
//       clone.setTarget(this.__filterTextField);
//       clone.setBubbles(false);
//       this.__filterTextField.dispatchEvent(clone);
    },

        // overridden
    _MonKeyPress : function(e)
    {
      var iden = e.getKeyIdentifier();
      if(iden=="Backspace"){
        var old = this.__filterTextField.getValue() || "";
        var newVal = old.slice(0, -1);
        this.__filterTextField.setValue(newVal);
        this.__filterList(newVal);

        e.preventDefault();
      }
    }
  }
});<|endoftext|>


<commit_before>"use strict";

/**
 * Orbit controls object can be controlled using the mouse.
 *
 * It can be rotated using the mouse left button, moved with the mouse right button or mouse wheel.
 *
 * @class OrbitControls
 * @constructor
 * @extends {Object3D}
 * @module Controls
 */
/**
 * Initial distance of the object relative to the center.
 *
 * @property distance
 * @default 3
 * @type {Number}
 */
/**
 * Mouse sensitivity.
 *
 * @property sensitivity
 * @default 0.001
 * @type {Number}
 */
/**
 * Top limit angle.
 *
 * @property limitUp
 * @default 1.57
 * @type {Number}
 */
/**
 * Bottom limit angle.
 *
 * @property limitDown
 * @default -1.57
 * @type {Number}
 */
/**
 * Indicates if the button left button needs to be pressed to rotate the object.
 *
 * @property needsButtonPressed
 * @default true
 * @type {Boolean}
 */
/**
 * Indicates if its possible to move the object around.
 *
 * @property movementEnabled
 * @default true
 * @type {Boolean}
 */
function OrbitControls()
{
	THREE.Object3D.call(this);

	this.name = "orbit";
	this.type = "OrbitControls";

	this.distance = 4;
	this.sensitivity = 0.002;
	this.limitUp = 1.57;
	this.limitDown = -1.57;
	this.needsButtonPressed = true;
	this.movementEnabled = true;

	this.center = new THREE.Vector3(0, 0, 0);
	this.vector = new THREE.Vector2(0, 0);
	this.mouse = null;
	this.keyboard = null;
}

OrbitControls.UP = new THREE.Vector3(0, 1, 0);
OrbitControls.ZERO = new THREE.Vector3(0, 0, 0);

OrbitControls.prototype = Object.create(THREE.Object3D.prototype);

OrbitControls.prototype.initialize = function()
{
	var node = this;
	while(node.parent !== null)
	{
		node = node.parent;

		if(node instanceof Program)
		{
			this.mouse = node.mouse;
			this.keyboard = node.keyboard;
		}
	}

	this.center.copy(this.position);

	for(var i = 0; i < this.children.length; i++)
	{
		this.children[i].initialize();
	}
};

OrbitControls.prototype.update = function()
{
	if(!this.needsButtonPressed || this.mouse.buttonPressed(Mouse.LEFT))
	{
		this.vector.y -= this.sensitivity * this.mouse.delta.y;
		this.vector.x -= this.sensitivity * this.mouse.delta.x;

		if(this.vector.y < this.limitDown)
		{
			this.vector.y = this.limitDown;
		}
		else if(this.vector.y > this.limitUp)
		{
			this.vector.y = this.limitUp;
		}
	}

	if(this.mouse.wheel !== 0)
	{
		this.distance += this.mouse.wheel * this.sensitivity;
		if(this.distance < 0)
		{
			this.distance = 0;
		}
	}

	if(this.mouse.buttonPressed(Mouse.MIDDLE))
	{
		this.distance += this.mouse.delta.y * this.sensitivity;
		if(this.distance < 0)
		{
			this.distance = 0;
		}
	}

	if(this.movementEnabled && this.mouse.buttonPressed(Mouse.RIGHT))
	{
		var direction = this.getWorldDirection();
		direction.y = 0;
		direction.normalize();

		var y = this.mouse.delta.y * this.sensitivity * 10;
		this.center.x -= direction.x * y;
		this.center.z -= direction.z * y;

		direction.applyAxisAngle(OrbitControls.UP, 1.57);

		var x = this.mouse.delta.x * this.sensitivity * 10;
		this.center.x -= direction.x * x;
		this.center.z -= direction.z * x;
	}

	var cos = this.distance * Math.cos(this.vector.y);
	this.position.set(Math.cos(this.vector.x) * cos, this.distance * Math.sin(this.vector.y), Math.sin(this.vector.x) * cos);
	this.position.add(this.center);

	var matrix = new THREE.Matrix4();
	matrix.lookAt(this.position, this.center, OrbitControls.UP);
	this.quaternion.setFromRotationMatrix(matrix);

	for(var i = 0; i < this.children.length; i++)
	{
		this.children[i].update();
	}
};

OrbitControls.prototype.toJSON = function(meta)
{
	var data = THREE.Object3D.prototype.toJSON.call(this, meta);

	data.object.distance = this.distance;
	data.object.sensitivity = this.sensitivity;
	data.object.limitUp = this.limitUp;
	data.object.limitDown = this.limitDown;
	data.object.needsButtonPressed = this.needsButtonPressed;

	return data;
};<commit_msg>Ajusted movement on Orbit Controls<commit_after>"use strict";

/**
 * Orbit controls object can be controlled using the mouse.
 *
 * It can be rotated using the mouse left button, moved with the mouse right button or mouse wheel.
 *
 * @class OrbitControls
 * @constructor
 * @extends {Object3D}
 * @module Controls
 */
/**
 * Initial distance of the object relative to the center.
 *
 * @property distance
 * @default 3
 * @type {Number}
 */
/**
 * Mouse sensitivity.
 *
 * @property sensitivity
 * @default 0.001
 * @type {Number}
 */
/**
 * Top limit angle.
 *
 * @property limitUp
 * @default 1.57
 * @type {Number}
 */
/**
 * Bottom limit angle.
 *
 * @property limitDown
 * @default -1.57
 * @type {Number}
 */
/**
 * Indicates if the button left button needs to be pressed to rotate the object.
 *
 * @property needsButtonPressed
 * @default true
 * @type {Boolean}
 */
/**
 * Indicates if its possible to move the object around.
 *
 * @property movementEnabled
 * @default true
 * @type {Boolean}
 */
function OrbitControls()
{
	THREE.Object3D.call(this);

	this.name = "orbit";
	this.type = "OrbitControls";

	this.distance = 4;
	this.sensitivity = 0.001;
	this.limitUp = 1.57;
	this.limitDown = -1.57;
	this.needsButtonPressed = true;
	this.movementEnabled = true;

	this.center = new THREE.Vector3(0, 0, 0);
	this.vector = new THREE.Vector2(0, 0);
	this.mouse = null;
	this.keyboard = null;
}

OrbitControls.UP = new THREE.Vector3(0, 1, 0);
OrbitControls.ZERO = new THREE.Vector3(0, 0, 0);

OrbitControls.prototype = Object.create(THREE.Object3D.prototype);

OrbitControls.prototype.initialize = function()
{
	var node = this;
	while(node.parent !== null)
	{
		node = node.parent;

		if(node instanceof Program)
		{
			this.mouse = node.mouse;
			this.keyboard = node.keyboard;
		}
	}

	this.center.copy(this.position);

	for(var i = 0; i < this.children.length; i++)
	{
		this.children[i].initialize();
	}
};

OrbitControls.prototype.update = function()
{
	if(!this.needsButtonPressed || this.mouse.buttonPressed(Mouse.LEFT))
	{
		this.vector.y -= this.sensitivity * this.mouse.delta.y;
		this.vector.x -= this.sensitivity * this.mouse.delta.x;

		if(this.vector.y < this.limitDown)
		{
			this.vector.y = this.limitDown;
		}
		else if(this.vector.y > this.limitUp)
		{
			this.vector.y = this.limitUp;
		}
	}

	if(this.mouse.wheel !== 0)
	{
		this.distance += this.mouse.wheel * this.sensitivity * this.position.distanceTo(this.center);
		if(this.distance < 0)
		{
			this.distance = 0;
		}
	}

	if(this.mouse.buttonPressed(Mouse.MIDDLE))
	{
		this.distance += this.mouse.delta.y * this.sensitivity;
		if(this.distance < 0)
		{
			this.distance = 0;
		}
	}

	if(this.movementEnabled && this.mouse.buttonPressed(Mouse.RIGHT))
	{
		var direction = this.getWorldDirection();
		direction.y = 0;
		direction.normalize();

		var y = this.mouse.delta.y * this.sensitivity * this.distance;
		this.center.x -= direction.x * y;
		this.center.z -= direction.z * y;

		direction.applyAxisAngle(OrbitControls.UP, 1.57);

		var x = this.mouse.delta.x * this.sensitivity * this.distance;
		this.center.x -= direction.x * x;
		this.center.z -= direction.z * x;
	}

	var cos = this.distance * Math.cos(this.vector.y);
	this.position.set(Math.cos(this.vector.x) * cos, this.distance * Math.sin(this.vector.y), Math.sin(this.vector.x) * cos);
	this.position.add(this.center);

	var matrix = new THREE.Matrix4();
	matrix.lookAt(this.position, this.center, OrbitControls.UP);
	this.quaternion.setFromRotationMatrix(matrix);

	for(var i = 0; i < this.children.length; i++)
	{
		this.children[i].update();
	}
};

OrbitControls.prototype.toJSON = function(meta)
{
	var data = THREE.Object3D.prototype.toJSON.call(this, meta);

	data.object.distance = this.distance;
	data.object.sensitivity = this.sensitivity;
	data.object.limitUp = this.limitUp;
	data.object.limitDown = this.limitDown;
	data.object.needsButtonPressed = this.needsButtonPressed;

	return data;
};<|endoftext|>