<commit_before>import { Class } from '../../core/Core';
import RunLoop from '../../foundation/RunLoop';  // Also Function#queue
import '../../foundation/ComputedProps';  // For Function#property
import '../../foundation/EventTarget';  // For Function#on
import '../../foundation/ObservableProps';  // For Function#observes
import Animation from '../../animation/Animation';
import Tap from '../../touch/Tap';
import UA from '../../ua/UA';
import View from '../View';
import ViewEventsController from '../ViewEventsController';

const ScrollAnimation = Class({

    Extends: Animation,

    duration: 250,

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

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

    drawFrame ( position ) {
        const x = position < 1 ?
                this.startX + ( position * this.deltaX ) : this.endX;
        const 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.
*/
const ScrollView = Class({

    Extends: 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: View.LAYOUT_FILL_PARENT,

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

        Sets the overflow styles to show the scrollbars.
    */
    layerStyles: function () {
        const styles = 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 ( layer ) {
        this.scrollLayer = layer;
    },

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

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

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

    willLeaveDocument () {
        // Remove keyboard shortcuts:
        const keys = this.get( 'keys' );
        const shortcuts = ViewEventsController.kbShortcuts;
        for ( const 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' ) ) {
            const 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(),

    /**
        Property: O.ScrollView#isAnimating
        Type: Boolean

        Is the scroll currently animating?
    */
    isAnimating: false,

    willAnimate () {
        this.set( 'isAnimating', true );
    },

    didAnimate () {
        this.set( 'isAnimating', false );
    },

    /**
        Method: O.ScrollView#scrollToTop

        Scrolls the view to the top
    */
    scrollToTop () {
        return this.scrollTo( 0, 0, true );
    },

    /**
        Method: O.ScrollView#scrollToBottom

        Scrolls the view to the bottom
    */
    scrollToBottom () {
        return this.scrollTo( 0,
            this.get( 'scrollLayer' ).scrollHeight - this.get( 'pxHeight' ),
            true
        );
    },

    /**
        Method: O.ScrollView#scrollPage

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

    /**
        Method: O.ScrollView#reverseScrollPage

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

    /**
        Method: O.ScrollView#scrollLine

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

    /**
        Method: O.ScrollView#reverseScrollLine

        Scrolls the view up by 40px.
    */
    reverseScrollLine () {
        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 ( x, y, withAnimation ) {
        const left = this.get( 'scrollLeft' );
        const 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 ( view, offset, withAnimation ) {
        const 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 ( x, y, withAnimation ) {
        // Can't have negative scroll values.
        // Can't scroll to fractional positions
        x = x < 0 ? 0 : Math.round( x );
        y = y < 0 ? 0 : Math.round( y );

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

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

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

        Set the new values and immediately redraw. Fast path for animation.
    */
    _scrollTo ( 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 () {
        const layer = this.get( 'scrollLayer' );
        const x = this.get( 'scrollLeft' );
        const y = this.get( 'scrollTop' );
        layer.scrollLeft = x;
        layer.scrollTop = y;
        // In case we've gone past the end.
        if ( x || y ) {
            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;
        }
        const layer = this.get( 'scrollLayer' );
        const x = layer.scrollLeft;
        const 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 ( Tap ) {
                Tap.cancel();
            }
        }
    }.on( 'scroll' ),
});

if ( UA.isIOS ) {
    Object.assign( ScrollView.prototype, {
        isFixedDimensions: function () {
            const positioning = this.get( 'positioning' );
            return positioning === 'absolute' || positioning === 'fixed';
        }.property( 'positioning' ),

        draw ( layer, Element, el ) {
            const isFixedDimensions = this.get( 'isFixedDimensions' );
            let scrollFixerHeight = 1;
            let wrapper = null;
            const safariVersion = UA.safari;

            // Render the children.
            const 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, or any other WKWebView
            // based browser. 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 ( UA.isWKWebView ) {
                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' ) ) {
                const scrollTop = this.get( 'scrollTop' );
                const 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' ) ) {
                const layer = this.get( 'layer' );
                if ( layer.scrollHeight <= layer.offsetHeight ) {
                    event.preventDefault();
                }
            }
        }.on( 'touchmove' ),

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

export default ScrollView;
<commit_msg>Add ScrollView#focus method<commit_after>import { Class } from '../../core/Core';
import RunLoop from '../../foundation/RunLoop';  // Also Function#queue
import '../../foundation/ComputedProps';  // For Function#property
import '../../foundation/EventTarget';  // For Function#on
import '../../foundation/ObservableProps';  // For Function#observes
import Animation from '../../animation/Animation';
import Tap from '../../touch/Tap';
import UA from '../../ua/UA';
import View from '../View';
import ViewEventsController from '../ViewEventsController';

const ScrollAnimation = Class({

    Extends: Animation,

    duration: 250,

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

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

    drawFrame ( position ) {
        const x = position < 1 ?
                this.startX + ( position * this.deltaX ) : this.endX;
        const 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.
*/
const ScrollView = Class({

    Extends: 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: View.LAYOUT_FILL_PARENT,

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

        Sets the overflow styles to show the scrollbars.
    */
    layerStyles: function () {
        const styles = 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 ( layer ) {
        layer.tabIndex = -1;
        this.scrollLayer = layer;
    },

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

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

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

    willLeaveDocument () {
        // Remove keyboard shortcuts:
        const keys = this.get( 'keys' );
        const shortcuts = ViewEventsController.kbShortcuts;
        for ( const 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' ) ) {
            const 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(),

    /**
        Property: O.ScrollView#isAnimating
        Type: Boolean

        Is the scroll currently animating?
    */
    isAnimating: false,

    willAnimate () {
        this.set( 'isAnimating', true );
    },

    didAnimate () {
        this.set( 'isAnimating', false );
    },

    /**
        Method: O.ScrollView#scrollToTop

        Scrolls the view to the top
    */
    scrollToTop () {
        return this.scrollTo( 0, 0, true );
    },

    /**
        Method: O.ScrollView#scrollToBottom

        Scrolls the view to the bottom
    */
    scrollToBottom () {
        return this.scrollTo( 0,
            this.get( 'scrollLayer' ).scrollHeight - this.get( 'pxHeight' ),
            true
        );
    },

    /**
        Method: O.ScrollView#scrollPage

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

    /**
        Method: O.ScrollView#reverseScrollPage

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

    /**
        Method: O.ScrollView#scrollLine

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

    /**
        Method: O.ScrollView#reverseScrollLine

        Scrolls the view up by 40px.
    */
    reverseScrollLine () {
        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 ( x, y, withAnimation ) {
        const left = this.get( 'scrollLeft' );
        const 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 ( view, offset, withAnimation ) {
        const 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 ( x, y, withAnimation ) {
        // Can't have negative scroll values.
        // Can't scroll to fractional positions
        x = x < 0 ? 0 : Math.round( x );
        y = y < 0 ? 0 : Math.round( y );

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

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

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

        Set the new values and immediately redraw. Fast path for animation.
    */
    _scrollTo ( 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 () {
        const layer = this.get( 'scrollLayer' );
        const x = this.get( 'scrollLeft' );
        const y = this.get( 'scrollTop' );
        layer.scrollLeft = x;
        layer.scrollTop = y;
        // In case we've gone past the end.
        if ( x || y ) {
            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;
        }
        const layer = this.get( 'scrollLayer' );
        const x = layer.scrollLeft;
        const 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 ( Tap ) {
                Tap.cancel();
            }
        }
    }.on( 'scroll' ),

    // ---

    /**
        Method: O.ScrollView#focus

        Focuses the scrollable element. This will mean default browser shortcuts
        will work for scrolling (e.g. up/down/space etc.).

        Returns:
            {O.ScrollView} Returns self.
    */
    focus () {
        this.scrollLayer.focus();
        return this;
    },
});

if ( UA.isIOS ) {
    Object.assign( ScrollView.prototype, {
        isFixedDimensions: function () {
            const positioning = this.get( 'positioning' );
            return positioning === 'absolute' || positioning === 'fixed';
        }.property( 'positioning' ),

        draw ( layer, Element, el ) {
            const isFixedDimensions = this.get( 'isFixedDimensions' );
            let scrollFixerHeight = 1;
            let wrapper = null;
            const safariVersion = UA.safari;

            // Render the children.
            const 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', {
                    tabIndex: -1,
                    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, or any other WKWebView
            // based browser. 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 ( UA.isWKWebView ) {
                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' ) ) {
                const scrollTop = this.get( 'scrollTop' );
                const 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' ) ) {
                const layer = this.get( 'layer' );
                if ( layer.scrollHeight <= layer.offsetHeight ) {
                    event.preventDefault();
                }
            }
        }.on( 'touchmove' ),

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

export default ScrollView;
<|endoftext|>


<commit_before>export default function (val, props) {
  const { zIndex, position, width, height, handleWidth, handleHeight, overlayColor, fadeOut, offset } = props;
  const { clientWidth, clientHeight } = document.body;
  const opacity = (val - offset) / width;
  let transformVal = val;
  let transform;
  let drawer;
  let overlay;
  let overlayTransform;

  if (position === 'right') {
    transformVal = width - val;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingLeft: handleWidth,
      maxWidth: width,
      height,
      top: 0,
      right: 0,
      margin: 0,
      transform: `translate3d(${transformVal}px, 0, 0)`,
      WebkitTransform: `translate3d(${transformVal}px, 0, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = -width;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: clientWidth,
      height: '100%',
      background: overlayColor,
      opacity,
      top: 0,
      right: 0,
      margin: 0,
      transform: `translate3d(${overlayTransform}px, 0, 0)`,
      WebkitTransform: `translate3d(${overlayTransform}px, 0, 0)`,
    };
  }
  if (position === 'left') {
    transformVal = val - width;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingRight: handleWidth,
      maxWidth: width,
      height,
      top: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(${transformVal}px, 0, 0)`,
      WebkitTransform: `translate3d(${transformVal}px, 0, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = width;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: clientWidth,
      height: '100%',
      background: overlayColor,
      opacity,
      top: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(${overlayTransform}px, 0, 0)`,
      WebkitTransform: `translate3d(${overlayTransform}px, 0, 0)`,
    };
  }
  if (position === 'bottom') {
    transformVal = height - val;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingTop: handleHeight,
      maxHeight: height,
      height,
      left: 0,
      bottom: 0,
      margin: 0,
      transform: `translate3d(0, ${transformVal}px, 0)`,
      WebkitTransform: `translate3d(0, ${transformVal}px, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = height;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: clientWidth,
      height: '100%',
      background: overlayColor,
      opacity,
      top: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(0, ${overlayTransform}px, 0)`,
      WebkitTransform: `translate3d(0, ${overlayTransform}px, 0)`,
    };
  }
  if (position === 'top') {
    transformVal = val - height;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingBottom: handleHeight,
      maxHeight: height,
      height,
      left: 0,
      top: 0,
      margin: 0,
      transform: `translate3d(0, ${transformVal}px, 0)`,
      WebkitTransform: `translate3d(0, ${transformVal}px, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = -height;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: '100%',
      height: clientHeight,
      background: overlayColor,
      opacity,
      left: 0,
      bottom: 0,
      margin: 0,
      transform: `translate3d(0, ${overlayTransform}px, 0)`,
      WebkitTransform: `translate3d(0, ${overlayTransform}px, 0)`,
    };
  }

  return {
    drawer,
    transform,
    overlay,
  };
}
<commit_msg>WIP3: IT WARKS<commit_after>export default function (val, props) {
  const { zIndex, position, width, height, handleWidth, handleHeight, overlayColor, fadeOut, offset } = props;
  const { clientWidth, clientHeight } = document.body;
  let opacity;
  let transformVal = val;
  let transform;
  let drawer;
  let overlay;
  let overlayTransform;

  if (position === 'right') {
    opacity = (val - offset) / width;
    transformVal = width - val;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingLeft: handleWidth,
      maxWidth: width,
      height,
      top: 0,
      right: 0,
      margin: 0,
      transform: `translate3d(${transformVal}px, 0, 0)`,
      WebkitTransform: `translate3d(${transformVal}px, 0, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = -width;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: clientWidth,
      height: '100%',
      background: overlayColor,
      opacity,
      top: 0,
      right: 0,
      margin: 0,
      transform: `translate3d(${overlayTransform}px, 0, 0)`,
      WebkitTransform: `translate3d(${overlayTransform}px, 0, 0)`,
    };
  }
  if (position === 'left') {
    opacity = (val - offset) / width;
    transformVal = val - width;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingRight: handleWidth,
      maxWidth: width,
      height,
      top: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(${transformVal}px, 0, 0)`,
      WebkitTransform: `translate3d(${transformVal}px, 0, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = width;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      width: clientWidth,
      height: '100%',
      background: overlayColor,
      opacity,
      top: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(${overlayTransform}px, 0, 0)`,
      WebkitTransform: `translate3d(${overlayTransform}px, 0, 0)`,
    };
  }
  if (position === 'bottom') {
    opacity = (val - offset) / height;
    transformVal = height - val;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingTop: handleHeight,
      maxHeight: height,
      height,
      left: 0,
      bottom: 0,
      margin: 0,
      transform: `translate3d(0, ${transformVal}px, 0)`,
      WebkitTransform: `translate3d(0, ${transformVal}px, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = -height;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      height: clientHeight,
      width: '100%',
      background: overlayColor,
      opacity,
      bottom: 0,
      left: 0,
      margin: 0,
      transform: `translate3d(0, ${overlayTransform}px, 0)`,
      WebkitTransform: `translate3d(0, ${overlayTransform}px, 0)`,
    };
  }
  if (position === 'top') {
    opacity = (val - offset) / height;
    transformVal = val - height;
    drawer = {
      display: 'block',
      width,
      height,
      overflow: 'auto',
    };

    transform = {
      boxSizing: 'content-box',
      pointer: 'cursor',
      position: 'absolute',
      display: 'block',
      zIndex,
      width,
      paddingBottom: handleHeight,
      maxHeight: height,
      height,
      left: 0,
      top: 0,
      margin: 0,
      transform: `translate3d(0, ${transformVal}px, 0)`,
      WebkitTransform: `translate3d(0, ${transformVal}px, 0)`,
      opacity: fadeOut ? opacity : 1,
    };

    overlayTransform = height;
    overlay = {
      zIndex: -2,
      pointer: 'cursor',
      position: 'absolute',
      height: clientHeight,
      width: '100%',
      background: overlayColor,
      opacity,
      left: 0,
      top: 0,
      margin: 0,
      transform: `translate3d(0, ${overlayTransform}px, 0)`,
      WebkitTransform: `translate3d(0, ${overlayTransform}px, 0)`,
    };
  }

  return {
    drawer,
    transform,
    overlay,
  };
}
<|endoftext|>


<commit_before>/*jshint bitwise:true, indent:2, curly:true eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true white:true*/
/*global XT:true, XM:true, XV:true, enyo:true*/

(function () {

  /**
    Must include a component called `list`.
    List must be of sub-kind `XV.ListRelations`.
    The `value` must be set to a collection of `XM.Info` models.
  */
  enyo.kind({
    name: "XV.ListRelationsBox",
    kind: "XV.Groupbox",
    title: "",
    published: {
      attr: null,
      value: null,
      parentKey: "",
      searchList: ""
    },
    events: {
      onSearch: "",
      onWorkspace: ""
    },
    handlers: {
      onSelect: "selectionChanged",
      onDeselect: "selectionChanged"
    },
    components: [
      {kind: "onyx.GroupboxHeader", content: "_opportunities".loc()},
      {kind: "XV.AccountOpportunityListRelations", name: "list",
        attr: "opportunityRelations", fit: true},
      {kind: 'FittableColumns', classes: "xv-groupbox-buttons", components: [
        {kind: "onyx.Button", name: "newButton", onclick: "newRecord",
          content: "_new".loc(), classes: "xv-groupbox-button-left",
          disabled: true},
        {kind: "onyx.Button", name: "attachButton", onclick: "attachRecord",
          content: "_attach".loc(), classes: "xv-groupbox-button-center",
          disabled: true},
        {kind: "onyx.Button", name: "detachButton", onclick: "detachRecord",
          content: "_detach".loc(), classes: "xv-groupbox-button-center",
          disabled: true},
        {kind: "onyx.Button", name: "openButton", onclick: "openRecord",
          content: "_open".loc(), classes: "xv-groupbox-button-right",
          disabled: true, fit: true}
      ]}
    ],
    attachRecord: function () {
      var list = this.$.list,
        key = this.parentKey,
        parentId = list.getParent().id,

        // Callback to handle selection...
        callback = function (selectedModel) {

          // Instantiate the models involved
          var Klass = XT.getObjectByName(selectedModel.editableModel),
            model = new Klass({id: selectedModel.id}),
            infoModel = new list.model({id: parentId}),
            setAndSave = function () {
              var K = XM.Model,
                options = {};
              if (model.getStatus() === K.READY_CLEAN &&
                  infoModel.getStatus() === K.READY_CLEAN) {
                model.off('statusChange', setAndSave);
                infoModel.off('statusChange', setAndSave);

                // Callback to update our list with changes when save complete
                options.success = function () {
                  list._collection.add(selectedModel);
                  list.setCount(list._collection.length);
                  list.refresh();
                };

                // Set and save our contact with the new account relation
                model.set(key, infoModel);
                model.save(null, options);
              }
            };

          // When fetch complete, trigger set and save
          model.on('statusChange', setAndSave);
          infoModel.on('statusChange', setAndSave);

          // Go get the data
          model.fetch();
          infoModel.fetch();
        };

      this.doSearch({list: this.searchList, callback: callback});
    },
    attrChanged: function () {
      this.$.list.setAttr(this.attr);
    },
    detachRecord: function () {
      var list = this.$.list,
        key = this.parentKey,
        index = list.getFirstSelected(),
        infoModel = list.getModel(index),
        Klass = XT.getObjectByName(infoModel.editableModel),
        model = new Klass({id: infoModel.id}),
        setAndSave = function () {
          var K = XM.Model,
            options = {};
          if (model.getStatus() === K.READY_CLEAN) {
            model.off('statusChange', setAndSave);

            // Callback to update our list with changes when save complete
            options.success = function () {
              list._collection.remove(infoModel);
              list.setCount(list._collection.length);
              list.refresh();
            };

            // Set and save our contact without account relation
            model.set(key, null);
            model.save(null, options);
          }
        };

      // When fetch complete, trigger set and save
      model.on('statusChange', setAndSave);

      // Go get the data
      model.fetch();
    },
    newRecord: function () {
      var list = this.$.list,
        parent = this.$.list.getParent(),
        id = parent ? parent.id : null,
        key = this.parentKey,
        attributes = {},
        callback = function (model) {
          var Model = list._collection.model,
            value = new Model({id: model.id}),
            options = {};
          options.success = function () {
            list._collection.add(value);
            list.setCount(list._collection.length);
            list.refresh();
          };
          value.fetch(options);
        },
        inEvent;
      attributes[key] = id;
      inEvent = {
        originator: this,
        workspace: list.workspace,
        attributes: attributes,
        callback: callback
      };
      this.doWorkspace(inEvent);
    },
    openRecord: function () {
      var list = this.$.list,
        index = list.getFirstSelected(),
        model = list.getModel(index),
        id = model.id,
        callback = function () {
          var options = {};
          options.success = function () {
            list.refresh();
          };
          // Refresh
          model.fetch(options);
        },
        inEvent = {
          workspace: list.workspace,
          id: id,
          callback: callback
        };

      this.doWorkspace(inEvent);
    },
    selectionChanged: function (inSender, inEvent) {
      var index = this.$.list.getFirstSelected(),
        model = index ? this.$.list.getModel(index) : null,
        couldNotRead = model ? !model.couldRead() : true,
        couldNotUpdate = model ? !model.couldUpdate() : true;
      this.$.detachButton.setDisabled(couldNotUpdate);
      this.$.openButton.setDisabled(couldNotRead);
    },
    valueChanged: function () {
      var value = this.getValue(), // Must be a collection of Info models
        Klass = value ?
          XT.getObjectByName(value.model.prototype.editableModel) : null,
        canNotCreate = Klass ? !Klass.canCreate() : true,
        canNotUpdate = Klass ? !Klass.canUpdate() : true;
      this.$.list.setValue(value);
      this.$.newButton.setDisabled(canNotCreate);
      this.$.attachButton.setDisabled(canNotUpdate);
    }
  });

}());
<commit_msg>swat!<commit_after>/*jshint bitwise:true, indent:2, curly:true eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true white:true*/
/*global XT:true, XM:true, XV:true, enyo:true*/

(function () {

  /**
    Must include a component called `list`.
    List must be of sub-kind `XV.ListRelations`.
    The `value` must be set to a collection of `XM.Info` models.
  */
  enyo.kind({
    name: "XV.ListRelationsBox",
    kind: "XV.Groupbox",
    title: "",
    published: {
      attr: null,
      value: null,
      parentKey: "",
      searchList: ""
    },
    events: {
      onSearch: "",
      onWorkspace: ""
    },
    handlers: {
      onSelect: "selectionChanged",
      onDeselect: "selectionChanged"
    },
    components: [
      {kind: "onyx.GroupboxHeader", content: "_opportunities".loc()},
      {kind: "XV.AccountOpportunityListRelations", name: "list",
        attr: "opportunityRelations", fit: true},
      {kind: 'FittableColumns', classes: "xv-groupbox-buttons", components: [
        {kind: "onyx.Button", name: "newButton", onclick: "newRecord",
          content: "_new".loc(), classes: "xv-groupbox-button-left",
          disabled: true},
        {kind: "onyx.Button", name: "attachButton", onclick: "attachRecord",
          content: "_attach".loc(), classes: "xv-groupbox-button-center",
          disabled: true},
        {kind: "onyx.Button", name: "detachButton", onclick: "detachRecord",
          content: "_detach".loc(), classes: "xv-groupbox-button-center",
          disabled: true},
        {kind: "onyx.Button", name: "openButton", onclick: "openRecord",
          content: "_open".loc(), classes: "xv-groupbox-button-right",
          disabled: true, fit: true}
      ]}
    ],
    attachRecord: function () {
      var list = this.$.list,
        collection = this.value,
        key = this.parentKey,
        parentId = list.getParent().id,

        // Callback to handle selection...
        callback = function (selectedModel) {

          // Instantiate the models involved
          var Klass = XT.getObjectByName(selectedModel.editableModel),
            model = new Klass({id: selectedModel.id}),
            infoModel = new collection.model({id: parentId}),
            setAndSave = function () {
              var K = XM.Model,
                options = {};
              if (model.getStatus() === K.READY_CLEAN &&
                  infoModel.getStatus() === K.READY_CLEAN) {
                model.off('statusChange', setAndSave);
                infoModel.off('statusChange', setAndSave);

                // Callback to update our list with changes when save complete
                options.success = function () {
                  list._collection.add(selectedModel);
                  list.setCount(list._collection.length);
                  list.refresh();
                };

                // Set and save our contact with the new account relation
                model.set(key, infoModel);
                model.save(null, options);
              }
            };

          // When fetch complete, trigger set and save
          model.on('statusChange', setAndSave);
          infoModel.on('statusChange', setAndSave);

          // Go get the data
          model.fetch();
          infoModel.fetch();
        };

      this.doSearch({list: this.searchList, callback: callback});
    },
    attrChanged: function () {
      this.$.list.setAttr(this.attr);
    },
    detachRecord: function () {
      var list = this.$.list,
        key = this.parentKey,
        index = list.getFirstSelected(),
        infoModel = list.getModel(index),
        Klass = XT.getObjectByName(infoModel.editableModel),
        model = new Klass({id: infoModel.id}),
        setAndSave = function () {
          var K = XM.Model,
            options = {};
          if (model.getStatus() === K.READY_CLEAN) {
            model.off('statusChange', setAndSave);

            // Callback to update our list with changes when save complete
            options.success = function () {
              list._collection.remove(infoModel);
              list.setCount(list._collection.length);
              list.refresh();
            };

            // Set and save our contact without account relation
            model.set(key, null);
            model.save(null, options);
          }
        };

      // When fetch complete, trigger set and save
      model.on('statusChange', setAndSave);

      // Go get the data
      model.fetch();
    },
    newRecord: function () {
      var list = this.$.list,
        parent = this.$.list.getParent(),
        id = parent ? parent.id : null,
        key = this.parentKey,
        attributes = {},
        callback = function (model) {
          var Model = list._collection.model,
            value = new Model({id: model.id}),
            options = {};
          options.success = function () {
            list._collection.add(value);
            list.setCount(list._collection.length);
            list.refresh();
          };
          value.fetch(options);
        },
        inEvent;
      attributes[key] = id;
      inEvent = {
        originator: this,
        workspace: list.workspace,
        attributes: attributes,
        callback: callback
      };
      this.doWorkspace(inEvent);
    },
    openRecord: function () {
      var list = this.$.list,
        index = list.getFirstSelected(),
        model = list.getModel(index),
        id = model.id,
        callback = function () {
          var options = {};
          options.success = function () {
            list.refresh();
          };
          // Refresh
          model.fetch(options);
        },
        inEvent = {
          workspace: list.workspace,
          id: id,
          callback: callback
        };

      this.doWorkspace(inEvent);
    },
    selectionChanged: function (inSender, inEvent) {
      var index = this.$.list.getFirstSelected(),
        model = index ? this.$.list.getModel(index) : null,
        couldNotRead = model ? !model.couldRead() : true,
        couldNotUpdate = model ? !model.couldUpdate() : true;
      this.$.detachButton.setDisabled(couldNotUpdate);
      this.$.openButton.setDisabled(couldNotRead);
    },
    valueChanged: function () {
      var value = this.getValue(), // Must be a collection of Info models
        Klass = value ?
          XT.getObjectByName(value.model.prototype.editableModel) : null,
        canNotCreate = Klass ? !Klass.canCreate() : true,
        canNotUpdate = Klass ? !Klass.canUpdate() : true;
      this.$.list.setValue(value);
      this.$.newButton.setDisabled(canNotCreate);
      this.$.attachButton.setDisabled(canNotUpdate);
    }
  });

}());
<|endoftext|>


<commit_before>//---------------------------------------------------------------------------------------//

/**
 * data contains the file to upload
 */
Handlebars.registerHelper('email', function(options) {

   var email = "";
   var guymal_enc;
   switch(options.hash.name){

      case "djoul":
         guymal_enc= "lsjoch(diurFkgvctogj(eik";
         break;

      case "chris":
         guymal_enc= "entou(bsahcFkgvctogj(eik";
         break;

      case "maperial":
      default:
         guymal_enc= "eihrgerFkgvctogj(eik";
      break;

   }

   for(guymal_i=0;guymal_i<guymal_enc.length;++guymal_i)
      email += String.fromCharCode(6^guymal_enc.charCodeAt(guymal_i));

   return email;
});

//---------------------------------------------------------------------------------------//

Handlebars.registerHelper('T', function(key) {
   try{
      return Translator.messages[key];
   }
   catch(e){
      if(!Translator.messages)
         console.log("Translator missconfig");
      else
         console.log("key not found : " + key);
      return key;
   }
});
<commit_msg>static helper<commit_after>//---------------------------------------------------------------------------------------//

/**
 * data contains the file to upload
 */
Handlebars.registerHelper('email', function(options) {

   var email = "";
   var guymal_enc;
   switch(options.hash.name){

      case "djoul":
         guymal_enc= "lsjoch(diurFkgvctogj(eik";
         break;

      case "chris":
         guymal_enc= "entou(bsahcFkgvctogj(eik";
         break;

      case "maperial":
      default:
         guymal_enc= "eihrgerFkgvctogj(eik";
      break;

   }

   for(guymal_i=0;guymal_i<guymal_enc.length;++guymal_i)
      email += String.fromCharCode(6^guymal_enc.charCodeAt(guymal_i));

   return email;
});

//---------------------------------------------------------------------------------------//

/**
 * HTML doesnt know which static to call.
 * Js does since Renamer.jar replace urls for min.js : so replace here as well
 */
Handlebars.registerHelper('static', function(options) {
   return "http://static.maperial.localhost" + options.hash.path;
});
<|endoftext|>


<commit_before>/* eslint-disable no-console */

var fs = require('fs');
var rollup = require('rollup');
var nodeResolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var json = require('rollup-plugin-json');
var gaze = require('gaze');
var ecstatic = require('ecstatic');
var express = require('express'),
    app = express(),
    url = require('url'),
    proxy = require('express-http-proxy');

var building = false;

if (process.argv[2] === 'develop') {
    build();

    gaze(['modules/**/*.js', 'data/**/*.{js,json}'], function(err, watcher) {
        watcher.on('all', function() {
            build();
        });
    });

    app.use(
        ecstatic({ root: __dirname, cache: 0 })
    ).listen(port);

    var port = 8080;
    var hootHost = 'localhost';
    var hootPort = 8888;
    var hootUrl = 'http://' + hootHost + ':' + hootPort;

    app.get('/hootenanny-id', function(req, res) {
        res.writeHead(301,
          { Location: 'http://localhost:8080' }
        );
        res.end();
    });

    app.use('/hoot-services', proxy(hootUrl, {
        forwardPath: function(req) {
            return '/hoot-services' + url.parse(req.url).path;
        }
    }));

    app.use('/static', proxy(hootUrl, {
        forwardPath: function(req) {
            return '/static' + url.parse(req.url).path;
        }
    }));

    console.log('Listening on ' + port);

} else {
    build();
}

function unlink(f) {
    try { fs.unlinkSync(f); } catch (e) { /* noop */ }
}

function build() {
    if (building) return;

    // Start clean
    unlink('dist/iD.js');
    unlink('dist/iD.js.map');

    building = true;
    console.log('Rebuilding');
    console.time('Rebuilt');

    rollup.rollup({
        entry: './modules/id.js',
        plugins: [
            nodeResolve({
                jsnext: true, main: true, browser: false
            }),
            commonjs(),
            json()
        ]

    }).then(function (bundle) {
        bundle.write({
            format: 'iife',
            dest: 'dist/iD.js',
            sourceMap: true,
            useStrict: false
        });
        building = false;
        console.timeEnd('Rebuilt');

    }, function(err) {
        building = false;
        console.error(err);
    });
}

<commit_msg>fix dev server port<commit_after>/* eslint-disable no-console */

var fs = require('fs');
var rollup = require('rollup');
var nodeResolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var json = require('rollup-plugin-json');
var gaze = require('gaze');
var ecstatic = require('ecstatic');
var express = require('express'),
    app = express(),
    url = require('url'),
    proxy = require('express-http-proxy');

var building = false;

if (process.argv[2] === 'develop') {
    build();

    gaze(['modules/**/*.js', 'data/**/*.{js,json}'], function(err, watcher) {
        watcher.on('all', function() {
            build();
        });
    });

    var port = 8080;
    var hootHost = 'localhost';
    var hootPort = 8888;
    var hootUrl = 'http://' + hootHost + ':' + hootPort;

    app.use(
        ecstatic({ root: __dirname, cache: 0 })
    ).listen(port);

    app.get('/hootenanny-id', function(req, res) {
        res.writeHead(301,
          { Location: 'http://localhost:8080' }
        );
        res.end();
    });

    app.use('/hoot-services', proxy(hootUrl, {
        forwardPath: function(req) {
            return '/hoot-services' + url.parse(req.url).path;
        }
    }));

    app.use('/static', proxy(hootUrl, {
        forwardPath: function(req) {
            return '/static' + url.parse(req.url).path;
        }
    }));

    console.log('Listening on ' + port);

} else {
    build();
}

function unlink(f) {
    try { fs.unlinkSync(f); } catch (e) { /* noop */ }
}

function build() {
    if (building) return;

    // Start clean
    unlink('dist/iD.js');
    unlink('dist/iD.js.map');

    building = true;
    console.log('Rebuilding');
    console.time('Rebuilt');

    rollup.rollup({
        entry: './modules/id.js',
        plugins: [
            nodeResolve({
                jsnext: true, main: true, browser: false
            }),
            commonjs(),
            json()
        ]

    }).then(function (bundle) {
        bundle.write({
            format: 'iife',
            dest: 'dist/iD.js',
            sourceMap: true,
            useStrict: false
        });
        building = false;
        console.timeEnd('Rebuilt');

    }, function(err) {
        building = false;
        console.error(err);
    });
}

<|endoftext|>


<commit_before>// Model
// ---------------
var _             = require('lodash');

var Sync          = require('./sync').Sync;
var Helpers       = require('./helpers').Helpers;
var EagerRelation = require('./eager').EagerRelation;

var ModelBase = require('../base/model').ModelBase;
var Promise   = require('../base/promise').Promise;

exports.Model = ModelBase.extend({

  // The `hasOne` relation specifies that this table has exactly one of another type of object,
  // specified by a foreign key in the other table. The foreign key is assumed to be the singular of this
  // object's `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified.
  hasOne: function(Target, foreignKey) {
    return this._relation('hasOne', Target, {foreignKey: foreignKey}).init(this);
  },

  // The `hasMany` relation specifies that this object has one or more rows in another table which
  // match on this object's primary key. The foreign key is assumed to be the singular of this object's
  // `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified.
  hasMany: function(Target, foreignKey) {
    return this._relation('hasMany', Target, {foreignKey: foreignKey}).init(this);
  },

  // A reverse `hasOne` relation, the `belongsTo`, where the specified key in this table
  // matches the primary `idAttribute` of another table.
  belongsTo: function(Target, foreignKey) {
    return this._relation('belongsTo', Target, {foreignKey: foreignKey}).init(this);
  },

  // A `belongsToMany` relation is when there are many-to-many relation
  // between two models, with a joining table.
  belongsToMany: function(Target, joinTableName, foreignKey, otherKey) {
    return this._relation('belongsToMany', Target, {
      joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey
    }).init(this);
  },

  // A `morphOne` relation is a one-to-one polymorphic association from this model
  // to another model.
  morphOne: function(Target, name, morphValue) {
    return this._morphOneOrMany(Target, name, morphValue, 'morphOne');
  },

  // A `morphMany` relation is a polymorphic many-to-one relation from this model
  // to many another models.
  morphMany: function(Target, name, morphValue) {
    return this._morphOneOrMany(Target, name, morphValue, 'morphMany');
  },

  // Defines the opposite end of a `morphOne` or `morphMany` relationship, where
  // the alternate end of the polymorphic model is defined.
  morphTo: function(morphName) {
    if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.');
    return this._relation('morphTo', null, {morphName: morphName, candidates: _.rest(arguments)}).init(this);
  },

  // Used to define passthrough relationships - `hasOne`, `hasMany`,
  // `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection.
  through: function(Interim, foreignKey, otherKey) {
    return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey});
  },

  // Fetch a model based on the currently set attributes,
  // returning a model to the callback, along with any options.
  // Returns a deferred promise through the `Bookshelf.Sync`.
  // If `{require: true}` is set as an option, the fetch is considered
  // a failure if the model comes up blank.
  fetch: Promise.method(function(options) {
    options = options ? _.clone(options) : {};

    // Run the `first` call on the `sync` object to fetch a single model.
    var sync = this.sync(options)
      .first()
      .bind(this)

      // Jump the rest of the chain if the response doesn't exist...
      .tap(function(response) {
        if (!response || response.length === 0) {
          if (options.require) throw new Error('EmptyResponse');
          return Promise.reject(null);
        }
      })

      // Now, load all of the data into the model as necessary.
      .tap(this._handleResponse);

    // If the "withRelated" is specified, we also need to eager load all of the
    // data on the model, as a side-effect, before we ultimately jump into the
    // next step of the model. Since the `columns` are only relevant to the current
    // level, ensure those are omitted from the options.
    if (options.withRelated) {
      sync = sync.tap(this._handleEager(_.omit(options, 'columns')));
    }

    return sync.tap(function(response) {
      return this.triggerThen('fetched', this, response, options);
    })
    .yield(this)
    .caught(function(err) {
      if (err === null) return err;
      throw err;
    });

  }),

  // Eager loads relationships onto an already populated `Model` instance.
  load: Promise.method(function(relations, options) {
    return Promise.bind(this)
      .then(function() {
        return [this.toJSON({shallow: true})];
      })
      .then(this._handleEager(_.extend({}, options, {
        shallow: true,
        withRelated: _.isArray(relations) ? relations : [relations]
      }))).yield(this);
  }),

  // Sets and saves the hash of model attributes, triggering
  // a "creating" or "updating" event on the model, as well as a "saving" event,
  // to bind listeners for any necessary validation, logging, etc.
  // If an error is thrown during these events, the model will not be saved.
  save: Promise.method(function(key, val, options) {
    var attrs;

    // Handle both `"key", value` and `{key: value}` -style arguments.
    if (key == null || typeof key === "object") {
      attrs = key || {};
      options = val || {};
    } else {
      (attrs = {})[key] = val;
      options = options ? _.clone(options) : {};
    }

    return Promise.bind(this).then(function() {
      return this.isNew(options);
    }).then(function(isNew) {

      // If the model has timestamp columns,
      // set them as attributes on the model, even
      // if the "patch" option is specified.
      if (this.hasTimestamps) _.extend(attrs, this.timestamp(options));

      // Determine whether the model is new, based on whether the model has an `idAttribute` or not.
      var method = options.method || (options.method = isNew ? 'insert' : 'update');
      var vals = attrs;

      // If the object is being created, we merge any defaults here
      // rather than during object creation.
      if (method === 'insert' || options.defaults) {
        var defaults = _.result(this, 'defaults');
        if (defaults) {
          vals = _.extend({}, defaults, this.attributes, vals);
        }
      }

      // Set the attributes on the model.
      this.set(vals, {silent: true});

      // If there are any save constraints, set them on the model.
      if (this.relatedData && this.relatedData.type !== 'morphTo') {
        Helpers.saveConstraints(this, this.relatedData);
      }

      // Gives access to the `query` object in the `options`, in case we need it
      // in any event handlers.
      var sync = this.sync(options);
      options.query = sync.query;

      return Promise.all([
        this.triggerThen((method === 'insert' ? 'creating' : 'updating'), this, attrs, options),
        this.triggerThen('saving', this, attrs, options)
      ])
      .bind(this)
      .then(function() {
        return sync[options.method](method === 'update' && options.patch ? attrs : this.attributes);
      })
      .then(function(resp) {

        // After a successful database save, the id is updated if the model was created
        if (method === 'insert' && resp) {
          this.attributes[this.idAttribute] = this[this.idAttribute] = resp[0];
        }

        // In case we need to reference the `previousAttributes` for the this
        // in the following event handlers.
        options.previousAttributes = this._previousAttributes;

        this._reset();

        return Promise.all([
          this.triggerThen((method === 'insert' ? 'created' : 'updated'), this, resp, options),
          this.triggerThen('saved', this, resp, options)
        ]);

      });

    }).yield(this);
  }),

  // Reset the query builder, called internally
  // each time a query is run.
  resetQuery: function() {
    this._knex = null;
    return this;
  },

  // Returns an instance of the query builder.
  query: function() {
    return Helpers.query(this, _.toArray(arguments));
  },

  // Creates and returns a new `Sync` instance.
  sync: function(options) {
    return new Sync(this, options);
  },

  // Helper for setting up the `morphOne` or `morphMany` relations.
  _morphOneOrMany: function(Target, morphName, morphValue, type) {
    if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.');
    return this._relation(type, Target, {morphName: morphName, morphValue: morphValue}).init(this);
  },

  // Handles the response data for the model, returning from the model's fetch call.
  // Todo: {silent: true, parse: true}, for parity with collection#set
  // need to check on Backbone's status there, ticket #2636
  _handleResponse: function(response) {
    var relatedData = this.relatedData;
    this.set(this.parse(response[0]), {silent: true})._reset();
    if (relatedData && relatedData.isJoined()) {
      relatedData.parsePivot([this]);
    }
  },

  // Handle the related data loading on the model.
  _handleEager: function(options) {
    return function(response) {
      return new EagerRelation([this], response, this).fetch(options);
    };
  }

});
<commit_msg>allow capitalized method overrides in model.save<commit_after>// Model
// ---------------
var _             = require('lodash');

var Sync          = require('./sync').Sync;
var Helpers       = require('./helpers').Helpers;
var EagerRelation = require('./eager').EagerRelation;

var ModelBase = require('../base/model').ModelBase;
var Promise   = require('../base/promise').Promise;

exports.Model = ModelBase.extend({

  // The `hasOne` relation specifies that this table has exactly one of another type of object,
  // specified by a foreign key in the other table. The foreign key is assumed to be the singular of this
  // object's `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified.
  hasOne: function(Target, foreignKey) {
    return this._relation('hasOne', Target, {foreignKey: foreignKey}).init(this);
  },

  // The `hasMany` relation specifies that this object has one or more rows in another table which
  // match on this object's primary key. The foreign key is assumed to be the singular of this object's
  // `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified.
  hasMany: function(Target, foreignKey) {
    return this._relation('hasMany', Target, {foreignKey: foreignKey}).init(this);
  },

  // A reverse `hasOne` relation, the `belongsTo`, where the specified key in this table
  // matches the primary `idAttribute` of another table.
  belongsTo: function(Target, foreignKey) {
    return this._relation('belongsTo', Target, {foreignKey: foreignKey}).init(this);
  },

  // A `belongsToMany` relation is when there are many-to-many relation
  // between two models, with a joining table.
  belongsToMany: function(Target, joinTableName, foreignKey, otherKey) {
    return this._relation('belongsToMany', Target, {
      joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey
    }).init(this);
  },

  // A `morphOne` relation is a one-to-one polymorphic association from this model
  // to another model.
  morphOne: function(Target, name, morphValue) {
    return this._morphOneOrMany(Target, name, morphValue, 'morphOne');
  },

  // A `morphMany` relation is a polymorphic many-to-one relation from this model
  // to many another models.
  morphMany: function(Target, name, morphValue) {
    return this._morphOneOrMany(Target, name, morphValue, 'morphMany');
  },

  // Defines the opposite end of a `morphOne` or `morphMany` relationship, where
  // the alternate end of the polymorphic model is defined.
  morphTo: function(morphName) {
    if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.');
    return this._relation('morphTo', null, {morphName: morphName, candidates: _.rest(arguments)}).init(this);
  },

  // Used to define passthrough relationships - `hasOne`, `hasMany`,
  // `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection.
  through: function(Interim, foreignKey, otherKey) {
    return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey});
  },

  // Fetch a model based on the currently set attributes,
  // returning a model to the callback, along with any options.
  // Returns a deferred promise through the `Bookshelf.Sync`.
  // If `{require: true}` is set as an option, the fetch is considered
  // a failure if the model comes up blank.
  fetch: Promise.method(function(options) {
    options = options ? _.clone(options) : {};

    // Run the `first` call on the `sync` object to fetch a single model.
    var sync = this.sync(options)
      .first()
      .bind(this)

      // Jump the rest of the chain if the response doesn't exist...
      .tap(function(response) {
        if (!response || response.length === 0) {
          if (options.require) throw new Error('EmptyResponse');
          return Promise.reject(null);
        }
      })

      // Now, load all of the data into the model as necessary.
      .tap(this._handleResponse);

    // If the "withRelated" is specified, we also need to eager load all of the
    // data on the model, as a side-effect, before we ultimately jump into the
    // next step of the model. Since the `columns` are only relevant to the current
    // level, ensure those are omitted from the options.
    if (options.withRelated) {
      sync = sync.tap(this._handleEager(_.omit(options, 'columns')));
    }

    return sync.tap(function(response) {
      return this.triggerThen('fetched', this, response, options);
    })
    .yield(this)
    .caught(function(err) {
      if (err === null) return err;
      throw err;
    });

  }),

  // Eager loads relationships onto an already populated `Model` instance.
  load: Promise.method(function(relations, options) {
    return Promise.bind(this)
      .then(function() {
        return [this.toJSON({shallow: true})];
      })
      .then(this._handleEager(_.extend({}, options, {
        shallow: true,
        withRelated: _.isArray(relations) ? relations : [relations]
      }))).yield(this);
  }),

  // Sets and saves the hash of model attributes, triggering
  // a "creating" or "updating" event on the model, as well as a "saving" event,
  // to bind listeners for any necessary validation, logging, etc.
  // If an error is thrown during these events, the model will not be saved.
  save: Promise.method(function(key, val, options) {
    var attrs;

    // Handle both `"key", value` and `{key: value}` -style arguments.
    if (key == null || typeof key === "object") {
      attrs = key || {};
      options = val || {};
    } else {
      (attrs = {})[key] = val;
      options = options ? _.clone(options) : {};
    }

    return Promise.bind(this).then(function() {
      return this.isNew(options);
    }).then(function(isNew) {

      // If the model has timestamp columns,
      // set them as attributes on the model, even
      // if the "patch" option is specified.
      if (this.hasTimestamps) _.extend(attrs, this.timestamp(options));

      // Determine whether the model is new, based on whether the model has an `idAttribute` or not.
      options.method = (options.method || (isNew ? 'insert' : 'update')).toLowerCase();
      var method = options.method;
      var vals = attrs;

      // If the object is being created, we merge any defaults here
      // rather than during object creation.
      if (method === 'insert' || options.defaults) {
        var defaults = _.result(this, 'defaults');
        if (defaults) {
          vals = _.extend({}, defaults, this.attributes, vals);
        }
      }

      // Set the attributes on the model.
      this.set(vals, {silent: true});

      // If there are any save constraints, set them on the model.
      if (this.relatedData && this.relatedData.type !== 'morphTo') {
        Helpers.saveConstraints(this, this.relatedData);
      }

      // Gives access to the `query` object in the `options`, in case we need it
      // in any event handlers.
      var sync = this.sync(options);
      options.query = sync.query;

      return Promise.all([
        this.triggerThen((method === 'insert' ? 'creating' : 'updating'), this, attrs, options),
        this.triggerThen('saving', this, attrs, options)
      ])
      .bind(this)
      .then(function() {
        return sync[options.method](method === 'update' && options.patch ? attrs : this.attributes);
      })
      .then(function(resp) {

        // After a successful database save, the id is updated if the model was created
        if (method === 'insert' && resp) {
          this.attributes[this.idAttribute] = this[this.idAttribute] = resp[0];
        }

        // In case we need to reference the `previousAttributes` for the this
        // in the following event handlers.
        options.previousAttributes = this._previousAttributes;

        this._reset();

        return Promise.all([
          this.triggerThen((method === 'insert' ? 'created' : 'updated'), this, resp, options),
          this.triggerThen('saved', this, resp, options)
        ]);

      });

    }).yield(this);
  }),

  // Reset the query builder, called internally
  // each time a query is run.
  resetQuery: function() {
    this._knex = null;
    return this;
  },

  // Returns an instance of the query builder.
  query: function() {
    return Helpers.query(this, _.toArray(arguments));
  },

  // Creates and returns a new `Sync` instance.
  sync: function(options) {
    return new Sync(this, options);
  },

  // Helper for setting up the `morphOne` or `morphMany` relations.
  _morphOneOrMany: function(Target, morphName, morphValue, type) {
    if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.');
    return this._relation(type, Target, {morphName: morphName, morphValue: morphValue}).init(this);
  },

  // Handles the response data for the model, returning from the model's fetch call.
  // Todo: {silent: true, parse: true}, for parity with collection#set
  // need to check on Backbone's status there, ticket #2636
  _handleResponse: function(response) {
    var relatedData = this.relatedData;
    this.set(this.parse(response[0]), {silent: true})._reset();
    if (relatedData && relatedData.isJoined()) {
      relatedData.parsePivot([this]);
    }
  },

  // Handle the related data loading on the model.
  _handleEager: function(options) {
    return function(response) {
      return new EagerRelation([this], response, this).fetch(options);
    };
  }

});
<|endoftext|>


<commit_before>// -----------------------------------------------------------------
// @author:  Mark McCann (www.markmccann.me)
// @license: None, Public Domain
// -----------------------------------------------------------------

var Modality = (function () {

    /**
     * list of Modality defaults
     */
    var defaults = {
        modalClass: "modality-modal", // outer-most container
        innerClass: "mm-wrap", // inner wrapper
        openClass: "mm-show", // when modal is visible
        clickOffClose: true, // click anywhere off of modal to close it
        closeOnEscape: true, // close modal with 'esc' key
        autoOpen: false, // open on page load
        autoBind: true, // automatically bind triggers to modal
        effect: "effect-1", // animation style
        onOpen: function(){}, // function to run when modal opens
        onClose: function(){} // function to run when modal closes
    },

    // grab the document's body only once
    body = document.getElementsByTagName('body')[0],

    /**
     * combines javascript objects
     * @return {object}
     */
    extend = function () {
        for( var i = 1; i < arguments.length; i++ ) {
            for( var key in arguments[i] ) {
                if(arguments[i].hasOwnProperty(key)) {
                    arguments[0][key] = arguments[i][key];
                }
            }
        }
        return arguments[0];
    },

    // -----------------------------------------------------------------

    /**
     * the modality object
     * @param {object} modal
     * @param {object} options
     * @param {function} callback
     */
    Modality = function ( modal, options, callback ) {

        // set initial attributes
        this._defaults = defaults;
        this._body     = body;
        this.id        = modal.getAttribute( 'id' );
        this.settings  = extend( {}, defaults, options );

        // build and set modal wrapper
        this.wrapper = document.createElement('div');
        this.wrapper.setAttribute( 'class', this.settings.modalClass + ' ' + this.settings.effect );
        this.wrapper.innerHTML = '
' + modal.outerHTML + '
';
        modal.parentNode.replaceChild( this.wrapper, modal );

        // collect the triggers and set the original modal
        this.triggers  = document.querySelectorAll( 'a[href="#'+this.id+'"], [data-modality="#'+this.id+'"]' );
        this.element = document.getElementById( this.id );

        // ------------------------------------------------------------

        // set local var to avoid scope issues
        var base = this;

        // toggle modal on all triggers
        if( this.settings.autoBind ) {
            for( var i = 0; i < this.triggers.length; i++ ) {
                this.setTrigger( this.triggers[i] );
            }
        }

        // close modal if users clicks anywhere off of it
        if( this.settings.clickOffClose ) {
            this.wrapper.addEventListener( "click", function(e) {
                e.preventDefault(); if(e.target == base.wrapper) base.close();
            }, false );
        }

        // close modal with 'esc' key
        if( this.settings.closeOnEscape ) {
            this._body.addEventListener( "keyup", function (e) {
                if(e.keyCode == 27){ base.close(); }
            }, false);
        }

        // open modal if set to true
        if( this.settings.autoOpen ) this.open();

        // run the user's callback function
        if( typeof callback == 'function' ) callback();

        // save modal and return it
        return Modality.modals[this.id] = this;

    };

    // -----------------------------------------------------------------

    /**
     * class methods for the modality object
     */
    extend(Modality.prototype, {

        /**
         * opens the modal
         * @param  {function} callback
         * @return {instance}
         */
        open: function ( callback ) {

            // add classes to open the modal
            this.wrapper.classList.add( this.settings.openClass );
            this._body.classList.add( this.settings.openClass );

            // run the callback(s)
            if ( typeof this.settings.onOpen == 'function' ) this.settings.onOpen();
            if ( typeof callback == 'function' ) callback();

            return this;

        },

        /**
         * closes the modal
         * @param  {function} callback
         * @return {instance}
         */
        close: function ( callback ) {

            // remove classes to close the modal
            this.wrapper.classList.remove( this.settings.openClass );
            this._body.classList.remove( this.settings.openClass );

            // run the callback(s)
            if ( typeof this.settings.onClose == 'function' ) this.settings.onClose();
            if ( typeof callback == 'function' ) callback();

            return this;

        },

        /**
         * toggles the modal
         * @param  {function} callback
         * @return {instance}
         */
        toggle: function ( callback ) {
            return ( this.isOpen() ) ? this.close(callback) : this.open(callback);
        },

        /**
         * determines if the modal is open or not
         * @return {Boolean}
         */
        isOpen: function () {
            return ( this.wrapper.classList.contains(this.settings.openClass) ) ? true : false;
        },

        /**
         * manually set trigger for a modal
         * @param  {object} - the element you want to be the trigger
         * @return {instance}
         */
        setTrigger: function ( trigger ) {

            // set local var for instance
            var base = this;

            // set click event for new trigger
            trigger.addEventListener( "click", function (e) {
                e.preventDefault(); base.toggle();
            }, false );

            return this;
        }

    });

    // -----------------------------------------------------------------

    /**
     * initalizes modal(s)
     * @param  {string} query
     * @param  {object} options
     * @param  {function} fn
     * @return {object,array}
     */
    Modality.init = function ( query, options, fn ) {

        // collect the modals from the DOM
        var modals = document.querySelectorAll(query);

        // loop through the modals and initialize each one
        for( var i = 0; i < modals.length; i++ ) {

            // Initialize the modal
            var modal = new Modality(modals[i], options, fn);

            // return only this modal if only one
            if(modals.length == 1) { return modal; }
        }

        // return array of modals
        return Modality.modals;
    }

    /**
     * an empty object to collect all modals on page
     */
    Modality.modals = {};

    // -----------------------------------------------------------------

    return Modality;

})();
<commit_msg>Update modality.js<commit_after>// -----------------------------------------------------------------
// @author:  Mark McCann (www.markmccann.me)
// @license: None, Public Domain
// -----------------------------------------------------------------

var Modality = (function () {

    /**
     * list of Modality defaults
     */
    var defaults = {
        modalClass: "modality-modal", // outer-most container
        innerClass: "mm-wrap", // inner wrapper
        openClass: "mm-show", // when modal is visible
        clickOffClose: true, // click anywhere off of modal to close it
        closeOnEscape: true, // close modal with 'esc' key
        autoOpen: false, // open on page load
        autoBind: true, // automatically bind triggers to modal
        effect: "effect-1", // animation style
        onOpen: function(){}, // function to run when modal opens
        onClose: function(){} // function to run when modal closes
    },

    // grab the document's body only once
    body = document.getElementsByTagName('body')[0],

    /**
     * combines javascript objects
     * @return {object}
     */
    extend = function () {
        for( var i = 1; i < arguments.length; i++ ) {
            for( var key in arguments[i] ) {
                if(arguments[i].hasOwnProperty(key)) {
                    arguments[0][key] = arguments[i][key];
                }
            }
        }
        return arguments[0];
    },

    /**
     * wraps the users modal element in modality's frame:
     *
     *

     *

     *        // user's modal goes here
     *

     *

     *
     * @return {object}
     */
     wrap = function ( element, settings ) {

        // create the container and insert markup
        var container = document.createElement('div');
        container.setAttribute( 'class', settings.modalClass + ' ' + settings.effect );
        container.innerHTML = '
' + element.outerHTML + '
';

        // replace the old modal with the new
        element.parentNode.replaceChild( container, element );

        return container;
     },

    // -----------------------------------------------------------------

    /**
     * the modality object
     * @param {object} modal
     * @param {object} options
     * @param {function} callback
     */
    Modality = function ( modal, options, callback ) {

        var $ = this; // set local var to avoid scope issues

        $._body     = body;
        $._defaults = defaults;
        $.id        = modal.getAttribute( 'id' );
        $.settings  = extend( {}, defaults, options );
        $.wrapper   = wrap( modal, $.settings );
        $.triggers  = document.querySelectorAll( 'a[href="#'+$.id+'"], [data-modality="#'+$.id+'"]' );
        $.element   = document.getElementById( $.id );

        // ------------------------------------------------------------

        // toggle modal on all triggers
        if( $.settings.autoBind ) {
            for( var i = 0; i < $.triggers.length; i++ ) {
                $.setTrigger( $.triggers[i] );
            }
        }

        // close modal if users clicks anywhere off of it
        if( $.settings.clickOffClose ) {
            $.wrapper.addEventListener( "click", function(e) {
                e.preventDefault(); if(e.target == $.wrapper) $.close();
            }, false );
        }

        // close modal with 'esc' key
        if( $.settings.closeOnEscape ) {
            $._body.addEventListener( "keyup", function (e) {
                if(e.keyCode == 27){ $.close(); }
            }, false);
        }

        // open modal if set to true
        if( $.settings.autoOpen ) $.open();

        // run the user's callback function
        if( typeof callback == 'function' ) callback();

        // save modal and return it
        return Modality.modals[$.id] = $;

    };

    // -----------------------------------------------------------------

    /**
     * class methods for the modality object
     */
    extend(Modality.prototype, {

        /**
         * opens the modal
         * @param  {function} callback
         * @return {instance}
         */
        open: function ( callback ) {

            // add classes to open the modal
            this.wrapper.classList.add( this.settings.openClass );
            this._body.classList.add( this.settings.openClass );

            // run the callback(s)
            if ( typeof this.settings.onOpen == 'function' ) this.settings.onOpen();
            if ( typeof callback == 'function' ) callback();

            return this;

        },

        /**
         * closes the modal
         * @param  {function} callback
         * @return {instance}
         */
        close: function ( callback ) {

            // remove classes to close the modal
            this.wrapper.classList.remove( this.settings.openClass );
            this._body.classList.remove( this.settings.openClass );

            // run the callback(s)
            if ( typeof this.settings.onClose == 'function' ) this.settings.onClose();
            if ( typeof callback == 'function' ) callback();

            return this;

        },

        /**
         * toggles the modal
         * @param  {function} callback
         * @return {instance}
         */
        toggle: function ( callback ) {
            return ( this.isOpen() ) ? this.close(callback) : this.open(callback);
        },

        /**
         * determines if the modal is open or not
         * @return {Boolean}
         */
        isOpen: function () {
            return ( this.wrapper.classList.contains(this.settings.openClass) ) ? true : false;
        },

        /**
         * manually set trigger for a modal
         * @param  {object} - the element you want to be the trigger
         * @return {instance}
         */
        setTrigger: function ( trigger ) {

            // set local var for instance
            var base = this;

            // set click event for new trigger
            trigger.addEventListener( "click", function (e) {
                e.preventDefault(); base.toggle();
            }, false );

            return this;
        }

    });

    // -----------------------------------------------------------------

    /**
     * initalizes modal(s)
     * @param  {string} query
     * @param  {object} options
     * @param  {function} fn
     * @return {object,array}
     */
    Modality.init = function ( query, options, fn ) {

        // collect the modals from the DOM
        var modals = document.querySelectorAll(query);

        // loop through the modals and initialize each one
        for( var i = 0; i < modals.length; i++ ) {

            // Initialize the modal
            var modal = new Modality(modals[i], options, fn);

            // return only this modal if only one
            if(modals.length == 1) { return modal; }
        }

        // return array of modals
        return Modality.modals;
    }

    /**
     * an empty object to collect all modals on page
     */
    Modality.modals = {};

    // -----------------------------------------------------------------

    return Modality;

})();
<|endoftext|>