<commit_before>// Copyright (c) Microsoft Open Technologies, Inc.  All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
define([
    '../Core/_Global',
    '../Core/_Base',
    '../Core/_BaseUtils',
    '../Core/_Events',
    '../Core/_Resources',
    '../Utilities/_Control',
    '../Utilities/_ElementUtilities',
    'require-style!less/styles-toggleswitch',
    'require-style!less/colors-toggleswitch'
    ],
    function toggleInit(_Global, _Base, _BaseUtils, _Events, _Resources, _Control, _ElementUtilities) {
        "use strict";

        _Base.Namespace.define("WinJS.UI", {
            /// <field>
            ///
            /// A control that lets the user switch an option on or off.
            ///
            /// </field>
            /// <icon src="ui_winjs.ui.toggleswitch.12x12.png" width="12" height="12" />
            /// <icon src="ui_winjs.ui.toggleswitch.16x16.png" width="16" height="16" />
            /// <htmlSnippet><![CDATA[

]]></htmlSnippet>
            /// <event name="change" bubbles="true" locid="WinJS.UI.ToggleSwitch_e:change">Raised when the switch is flipped to on (checked is set to true) or off (checked is set to false). </event>
            /// <part name="toggle" class="win-toggleswitch" locid="WinJS.UI.ToggleSwitch_part:toggle">The entire ToggleSwitch control.</part>
            /// <part name="track" class="win-toggleswitch-track" locid="WinJS.UI.ToggleSwitch_part:track">The slider portion of the toggle.</part>
            /// <part name="thumb" class="win-toggleswitch-thumb" locid="WinJS.UI.ToggleSwitch_part:thumb">The thumb of the slider.</part>
            /// <part name="title" class="win-toggleswitch-header" locid="WinJS.UI.ToggleSwitch_part:title">The main text for the ToggleSwitch control.</part>
            /// <part name="label-on" class="win-toggleswitch-value" locid="WinJS.UI.ToggleSwitch_part:label-on">The text for when the switch is on.</part>
            /// <part name="label-off" class="win-toggleswitch-value" locid="WinJS.UI.ToggleSwitch_part:label-off:">The text for when the switch is off.</part>
            /// <resource type="javascript" src="//$(TARGET_DESTINATION)/js/WinJS.js" shared="true" />
            /// <resource type="css" src="//$(TARGET_DESTINATION)/css/ui-dark.css" shared="true" />
            ToggleSwitch: _Base.Namespace._lazy(function () {

                // Store some class names
                var classContainer = 'win-toggleswitch';
                var classHeader = 'win-toggleswitch-header';
                var classClick = 'win-toggleswitch-clickregion';
                var classTrack = 'win-toggleswitch-track';
                var classThumb = 'win-toggleswitch-thumb';
                var classValues = 'win-toggleswitch-values';
                var classValue = 'win-toggleswitch-value';
                var classValueOn = 'win-toggleswitch-value-on';
                var classValueOff = 'win-toggleswitch-value-off';
                var classDescription = 'win-toggleswitch-description';
                var classOn = 'win-toggleswitch-on';
                var classOff = 'win-toggleswitch-off';
                var classDisabled = 'win-toggleswitch-disabled';
                var classEnabled = 'win-toggleswitch-enabled';
                var classDragging = 'win-toggleswitch-dragging';
                var classPressed = 'win-toggleswitch-pressed';

                // Localized  strings
                var strings = {
                    get on() {
                        return _Resources._getWinJSString("ui/on").value;
                    },
                    get off() {
                        return _Resources._getWinJSString("ui/off").value;
                    },
                };

                // Define the ToggleSwitch class
                var Toggle = _Base.Class.define(function ToggleSwitch_ctor(element, options) {
                    /// <signature helpKeyword="WinJS.UI.ToggleSwitch.ToggleSwitch">
                    ///
                    /// Creates a new ToggleSwitch.
                    ///
                    /// <param name="element" domElement="true" locid="WinJS.UI.ToggleSwitch.constructor_p:element">
                    /// The DOM element that hosts the ToggleSwitch.
                    /// </param>
                    /// <param name="options" type="Object" locid="WinJS.UI.ToggleSwitch.constructor_p:options">
                    /// An object that contains one or more property/value pairs to apply to the new control.
                    /// Each property of the options object corresponds to one of the control's properties or events.
                    /// Event names must begin with "on". For example, to provide a handler for the change event,
                    /// add a property named "onchange" to the options object and set its value to the event handler.
                    /// This parameter is optional.
                    /// </param>
                    /// <returns type="WinJS.UI.ToggleSwitch" locid="WinJS.UI.ToggleSwitch.constructor_returnValue">
                    /// The new ToggleSwitch.
                    /// </returns>
                    /// </signature>

                    // Main container
                    element = element || _Global.document.createElement('div');
                    this._domElement = element;
                    _ElementUtilities.addClass(this._domElement, classContainer);

                    // Set up DOM elements
                    this._domElement.innerHTML = [
                        '

',
                        '
',
                        '
',
                        '

',
                        '
',
                        '
',
                        '

',
                        '

',
                        '
',
                        '
',
                        '

'
                    ].join('\n');

                    // Get references to elements
                    this._headerElement = this._domElement.firstElementChild;
                    this._clickElement = this._headerElement.nextElementSibling;
                    this._trackElement = this._clickElement.firstElementChild;
                    this._thumbElement = this._trackElement.firstElementChild;
                    this._labelsElement = this._trackElement.nextElementSibling;
                    this._labelOnElement = this._labelsElement.firstElementChild;
                    this._labelOffElement = this._labelOnElement.nextElementSibling;
                    this._descriptionElement = this._clickElement.nextElementSibling;

                    // Set aria label info
                    this._headerElement.setAttribute('aria-hidden', true);
                    this._labelsElement.setAttribute('aria-hidden', true);
                    this._headerElement.setAttribute('id', _ElementUtilities._uniqueID(this._headerElement));
                    this._domElement.setAttribute('aria-labelledby', this._headerElement.id);
                    this._domElement.setAttribute('role', 'checkbox');

                    // Some initialization of main element
                    this._domElement.winControl = this;
                    _ElementUtilities.addClass(this._domElement, 'win-disposable');

                    // Add listeners
                    this._domElement.addEventListener('keydown', this._keyDownHandler.bind(this));
                    _ElementUtilities._addEventListener(this._clickElement, 'pointerdown', this._pointerDownHandler.bind(this));
                    _ElementUtilities._addEventListener(this._clickElement, 'pointercancel', this._pointerCancelHandler.bind(this));
                    this._boundPointerMove = this._pointerMoveHandler.bind(this);
                    this._boundPointerUp = this._pointerUpHandler.bind(this);

                    // Need mutation observer to listen for aria checked change
                    this._mutationObserver = new _ElementUtilities._MutationObserver(this._ariaChangedHandler.bind(this));
                    this._mutationObserver.observe(this._domElement, {attributes: true, attributeFilter: ['aria-checked']});

                    // Current x coord while being dragged
                    this._dragX = 0;
                    this._dragging = false;

                    // Default state
                    this.checked = false;
                    this.disabled = false;
                    this.labelOn = strings.on;
                    this.labelOff = strings.off;

                    // Apply options
                    _Control.setOptions(this, options);
                }, {
                    // Properties

                    /// <field type='HTMLElement' domElement='true' hidden='true' locid="WinJS.UI.ToggleSwitch.element" helpKeyword="WinJS.UI.ToggleSwitch.element">
                    /// The DOM element that hosts the ToggleSwitch control.
                    /// </field>
                    element: {
                        get: function () {
                            return this._domElement;
                        }
                    },
                    /// <field type="Boolean" locid="WinJS.UI.ToggleSwitch.checked" helpKeyword="WinJS.UI.ToggleSwitch.checked">
                    /// Gets or sets whether the control is on (checked is set to true) or off (checked is set to false).
                    /// </field>
                    checked: {
                        get: function () {
                            return this._checked;
                        },
                        set: function (value) {
                            value = !!value;
                            if (value === this.checked) {
                                return;
                            }

                            this._checked = value;
                            this._domElement.setAttribute('aria-checked', value);
                            if (value) {
                                _ElementUtilities.addClass(this._domElement, classOn);
                                _ElementUtilities.removeClass(this._domElement, classOff);
                            } else {
                                _ElementUtilities.addClass(this._domElement, classOff);
                                _ElementUtilities.removeClass(this._domElement, classOn);
                            }
                            this.dispatchEvent("change");
                        }
                    },
                    /// <field type="Boolean" locid="WinJS.UI.ToggleSwitch.disabled" helpKeyword="WinJS.UI.ToggleSwitch.disabled">
                    /// Gets or sets a value that specifies whether the control is disabled.
                    /// </field>
                    disabled: {
                        get: function () {
                            return this._disabled;
                        },
                        set: function (value) {
                            value = !!value;
                            if (value === this._disabled) {
                                return;
                            }

                            if (value) {
                                _ElementUtilities.addClass(this._domElement, classDisabled);
                                _ElementUtilities.removeClass(this._domElement, classEnabled);
                            } else {
                                _ElementUtilities.removeClass(this._domElement, classDisabled);
                                _ElementUtilities.addClass(this._domElement, classEnabled);
                            }

                            this._disabled = value;
                            this._domElement.setAttribute('aria-disabled', value);
                            this._domElement.setAttribute('tabIndex', value ? -1 : 0);
                        }
                    },
                    /// <field type="String" locid="WinJS.UI.ToggleSwitch.labelOn" helpKeyword="WinJS.UI.ToggleSwitch.labelOn">
                    /// Gets or sets the text that displays when the control is on (checked is set to true). The default value is "On".
                    /// </field>
                    labelOn: {
                        get: function () {
                            return this._labelOnElement.innerHTML;
                        },
                        set: function (value) {
                            this._labelOnElement.innerHTML = value;
                        }
                    },
                    /// <field type="String" locid="WinJS.UI.ToggleSwitch.labelOff" helpKeyword="WinJS.UI.ToggleSwitch.labelOff">
                    /// Gets or sets the text that displays when the control is off (checked is set to false). The default value is "Off".
                    /// </field>
                    labelOff: {
                        get: function () {
                            return this._labelOffElement.innerHTML;
                        },
                        set: function (value) {
                            this._labelOffElement.innerHTML = value;
                        }
                    },
                    /// <field type='String' locid="WinJS.UI.ToggleSwitch.title" helpKeyword="WinJS.UI.ToggleSwitch.title">
                    /// Gets or sets the main text for the ToggleSwitch control. This text is always displayed, regardless of whether
                    /// the control is switched on or off.
                    /// </field>
                    title: {
                        get: function () {
                            return this._headerElement.innerHTML;
                        },
                        set: function (value) {
                            this._headerElement.innerHTML = value;
                        }
                    },

                    // Events

                    /// <field type="Function" locid="WinJS.UI.ToggleSwitch.onchange" helpKeyword="WinJS.UI.ToggleSwitch.onchange">
                    /// Occurs when the ToggleSwitch control is flipped to on (checked == true) or off (checked == false).
                    /// </field>
                    onchange: _Events._createEventProperty('change'),

                    // Public methods
                    dispose: function ToggleSwitch_dispose() {
                        if (this._disposed) {
                            return;
                        }

                        this._disposed = true;
                    },

                    // Private event handlers
                    _ariaChangedHandler: function ToggleSwitch_ariaChanged() {
                        var value = this._domElement.getAttribute('aria-checked');
                        value = (value === 'true') ? true : false;
                        this.checked = value;
                    },
                    _keyDownHandler: function ToggleSwitch_keyDown(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Toggle checked on spacebar
                        if (e.keyCode === _ElementUtilities.Key.space) {
                            this.checked = !this.checked;
                        }

                        // Arrow keys set value
                        if (e.keyCode === _ElementUtilities.Key.rightArrow ||
                            e.keyCode === _ElementUtilities.Key.upArrow) {
                            this.checked = true;
                        }
                        if (e.keyCode === _ElementUtilities.Key.leftArrow ||
                            e.keyCode === _ElementUtilities.Key.downArrow) {
                            this.checked = false;
                        }

                    },
                    _pointerDownHandler: function ToggleSwitch_pointerDown(e) {
                        if (this.disabled || this._mousedown) {
                            return;
                        }

                        e.preventDefault();

                        this._mousedown = true;
                        this._dragXStart = e.pageX - this._trackElement.offsetLeft;
                        this._dragX = this._dragXStart;
                        this._dragging = false;
                        _ElementUtilities.addClass(this._domElement, classPressed);

                        _ElementUtilities._globalListener.addEventListener(this._domElement, 'pointermove', this._boundPointerMove, true);
                        _ElementUtilities._globalListener.addEventListener(this._domElement, 'pointerup', this._boundPointerUp, true);
                        if (e.pointerType === _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH) {
                            _ElementUtilities._setPointerCapture(this._domElement, e.pointerId);
                        }
                    },
                    _pointerCancelHandler: function ToggleSwitch_pointerCancel(e) {
                        this._resetPressedState();
                        if (e.pointerType === _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH) {
                            _ElementUtilities._releasePointerCapture(this._domElement, e.pointerId);
                        }
                    },
                    _pointerUpHandler: function ToggleSwitch_pointerUp(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Since up is a global event we should only take action
                        // if a mousedown was registered on us initially
                        if (!this._mousedown) {
                            return;
                        }

                        e = e.detail.originalEvent;
                        e.preventDefault();

                        // If the thumb is being dragged, pick a new value based on what the thumb
                        // was closest to
                        var isRTL = _Global.getComputedStyle(this._domElement).direction === 'rtl';
                        if (this._dragging) {
                            var maxX = this._trackElement.offsetWidth - this._thumbElement.offsetWidth;
                            this.checked = isRTL ? this._dragX < maxX / 2 : this._dragX >= maxX / 2;
                            this._dragging = false;
                            _ElementUtilities.removeClass(this._domElement, classDragging);
                        } else {
                            // Otherwise, just toggle the value as the up constitutes a
                            // click event
                            this.checked = !this.checked;
                        }

                        // Reset tracking variables and intermediate styles
                        this._resetPressedState();
                    },
                    _pointerMoveHandler: function ToggleSwitch_pointerMove(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Not dragging if mouse isn't down
                        if (!this._mousedown) {
                            return;
                        }

                        e = e.detail.originalEvent;
                        e.preventDefault();

                        // Get pointer x coord relative to control
                        var localMouseX = e.pageX - this._trackElement.offsetLeft;

                        // Not dragging if mouse is outside track
                        if (localMouseX > this._trackElement.offsetWidth) {
                            return;
                        }

                        // Calculate a position for the thumb
                        var maxX = this._trackElement.offsetWidth - this._thumbElement.offsetWidth - 6;
                        this._dragX = Math.min(maxX, localMouseX - this._thumbElement.offsetWidth / 2);
                        this._dragX = Math.max(2, this._dragX);

                        // Calculate if this pointermove constitutes switching to drag mode
                        if (!this._dragging && Math.abs(localMouseX - this._dragXStart) > 3) {
                            this._dragging = true;
                            _ElementUtilities.addClass(this._domElement, classDragging);
                        }

                        this._thumbElement.style.left = this._dragX + 'px';
                    },
                    _resetPressedState: function ToggleSwitch_resetPressedState() {
                        this._mousedown = false;
                        this._thumbElement.style.left = '';
                        _ElementUtilities.removeClass(this._domElement, classPressed);
                        _ElementUtilities._globalListener.removeEventListener(this._domElement, 'pointermove', this._boundPointerMove, true);
                        _ElementUtilities._globalListener.removeEventListener(this._domElement, 'pointerup', this._boundPointerUp, true);
                    }
                });

                // addEventListener, removeEventListener, dispatchEvent
                _Base.Class.mix(Toggle, _Control.DOMEventMixin);

                return Toggle;
            })
        });
    }
);<commit_msg>Use `getBoundingClientRect()` to get real x offset of toggleswitch. Closes #823<commit_after>// Copyright (c) Microsoft Open Technologies, Inc.  All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
define([
    '../Core/_Global',
    '../Core/_Base',
    '../Core/_BaseUtils',
    '../Core/_Events',
    '../Core/_Resources',
    '../Utilities/_Control',
    '../Utilities/_ElementUtilities',
    'require-style!less/styles-toggleswitch',
    'require-style!less/colors-toggleswitch'
    ],
    function toggleInit(_Global, _Base, _BaseUtils, _Events, _Resources, _Control, _ElementUtilities) {
        "use strict";

        _Base.Namespace.define("WinJS.UI", {
            /// <field>
            ///
            /// A control that lets the user switch an option on or off.
            ///
            /// </field>
            /// <icon src="ui_winjs.ui.toggleswitch.12x12.png" width="12" height="12" />
            /// <icon src="ui_winjs.ui.toggleswitch.16x16.png" width="16" height="16" />
            /// <htmlSnippet><![CDATA[

]]></htmlSnippet>
            /// <event name="change" bubbles="true" locid="WinJS.UI.ToggleSwitch_e:change">Raised when the switch is flipped to on (checked is set to true) or off (checked is set to false). </event>
            /// <part name="toggle" class="win-toggleswitch" locid="WinJS.UI.ToggleSwitch_part:toggle">The entire ToggleSwitch control.</part>
            /// <part name="track" class="win-toggleswitch-track" locid="WinJS.UI.ToggleSwitch_part:track">The slider portion of the toggle.</part>
            /// <part name="thumb" class="win-toggleswitch-thumb" locid="WinJS.UI.ToggleSwitch_part:thumb">The thumb of the slider.</part>
            /// <part name="title" class="win-toggleswitch-header" locid="WinJS.UI.ToggleSwitch_part:title">The main text for the ToggleSwitch control.</part>
            /// <part name="label-on" class="win-toggleswitch-value" locid="WinJS.UI.ToggleSwitch_part:label-on">The text for when the switch is on.</part>
            /// <part name="label-off" class="win-toggleswitch-value" locid="WinJS.UI.ToggleSwitch_part:label-off:">The text for when the switch is off.</part>
            /// <resource type="javascript" src="//$(TARGET_DESTINATION)/js/WinJS.js" shared="true" />
            /// <resource type="css" src="//$(TARGET_DESTINATION)/css/ui-dark.css" shared="true" />
            ToggleSwitch: _Base.Namespace._lazy(function () {

                // Store some class names
                var classContainer = 'win-toggleswitch';
                var classHeader = 'win-toggleswitch-header';
                var classClick = 'win-toggleswitch-clickregion';
                var classTrack = 'win-toggleswitch-track';
                var classThumb = 'win-toggleswitch-thumb';
                var classValues = 'win-toggleswitch-values';
                var classValue = 'win-toggleswitch-value';
                var classValueOn = 'win-toggleswitch-value-on';
                var classValueOff = 'win-toggleswitch-value-off';
                var classDescription = 'win-toggleswitch-description';
                var classOn = 'win-toggleswitch-on';
                var classOff = 'win-toggleswitch-off';
                var classDisabled = 'win-toggleswitch-disabled';
                var classEnabled = 'win-toggleswitch-enabled';
                var classDragging = 'win-toggleswitch-dragging';
                var classPressed = 'win-toggleswitch-pressed';

                // Localized  strings
                var strings = {
                    get on() {
                        return _Resources._getWinJSString("ui/on").value;
                    },
                    get off() {
                        return _Resources._getWinJSString("ui/off").value;
                    },
                };

                // Define the ToggleSwitch class
                var Toggle = _Base.Class.define(function ToggleSwitch_ctor(element, options) {
                    /// <signature helpKeyword="WinJS.UI.ToggleSwitch.ToggleSwitch">
                    ///
                    /// Creates a new ToggleSwitch.
                    ///
                    /// <param name="element" domElement="true" locid="WinJS.UI.ToggleSwitch.constructor_p:element">
                    /// The DOM element that hosts the ToggleSwitch.
                    /// </param>
                    /// <param name="options" type="Object" locid="WinJS.UI.ToggleSwitch.constructor_p:options">
                    /// An object that contains one or more property/value pairs to apply to the new control.
                    /// Each property of the options object corresponds to one of the control's properties or events.
                    /// Event names must begin with "on". For example, to provide a handler for the change event,
                    /// add a property named "onchange" to the options object and set its value to the event handler.
                    /// This parameter is optional.
                    /// </param>
                    /// <returns type="WinJS.UI.ToggleSwitch" locid="WinJS.UI.ToggleSwitch.constructor_returnValue">
                    /// The new ToggleSwitch.
                    /// </returns>
                    /// </signature>

                    // Main container
                    element = element || _Global.document.createElement('div');
                    this._domElement = element;
                    _ElementUtilities.addClass(this._domElement, classContainer);

                    // Set up DOM elements
                    this._domElement.innerHTML = [
                        '

',
                        '
',
                        '
',
                        '

',
                        '
',
                        '
',
                        '

',
                        '

',
                        '
',
                        '
',
                        '

'
                    ].join('\n');

                    // Get references to elements
                    this._headerElement = this._domElement.firstElementChild;
                    this._clickElement = this._headerElement.nextElementSibling;
                    this._trackElement = this._clickElement.firstElementChild;
                    this._thumbElement = this._trackElement.firstElementChild;
                    this._labelsElement = this._trackElement.nextElementSibling;
                    this._labelOnElement = this._labelsElement.firstElementChild;
                    this._labelOffElement = this._labelOnElement.nextElementSibling;
                    this._descriptionElement = this._clickElement.nextElementSibling;

                    // Set aria label info
                    this._headerElement.setAttribute('aria-hidden', true);
                    this._labelsElement.setAttribute('aria-hidden', true);
                    this._headerElement.setAttribute('id', _ElementUtilities._uniqueID(this._headerElement));
                    this._domElement.setAttribute('aria-labelledby', this._headerElement.id);
                    this._domElement.setAttribute('role', 'checkbox');

                    // Some initialization of main element
                    this._domElement.winControl = this;
                    _ElementUtilities.addClass(this._domElement, 'win-disposable');

                    // Add listeners
                    this._domElement.addEventListener('keydown', this._keyDownHandler.bind(this));
                    _ElementUtilities._addEventListener(this._clickElement, 'pointerdown', this._pointerDownHandler.bind(this));
                    _ElementUtilities._addEventListener(this._clickElement, 'pointercancel', this._pointerCancelHandler.bind(this));
                    this._boundPointerMove = this._pointerMoveHandler.bind(this);
                    this._boundPointerUp = this._pointerUpHandler.bind(this);

                    // Need mutation observer to listen for aria checked change
                    this._mutationObserver = new _ElementUtilities._MutationObserver(this._ariaChangedHandler.bind(this));
                    this._mutationObserver.observe(this._domElement, {attributes: true, attributeFilter: ['aria-checked']});

                    // Current x coord while being dragged
                    this._dragX = 0;
                    this._dragging = false;

                    // Default state
                    this.checked = false;
                    this.disabled = false;
                    this.labelOn = strings.on;
                    this.labelOff = strings.off;

                    // Apply options
                    _Control.setOptions(this, options);
                }, {
                    // Properties

                    /// <field type='HTMLElement' domElement='true' hidden='true' locid="WinJS.UI.ToggleSwitch.element" helpKeyword="WinJS.UI.ToggleSwitch.element">
                    /// The DOM element that hosts the ToggleSwitch control.
                    /// </field>
                    element: {
                        get: function () {
                            return this._domElement;
                        }
                    },
                    /// <field type="Boolean" locid="WinJS.UI.ToggleSwitch.checked" helpKeyword="WinJS.UI.ToggleSwitch.checked">
                    /// Gets or sets whether the control is on (checked is set to true) or off (checked is set to false).
                    /// </field>
                    checked: {
                        get: function () {
                            return this._checked;
                        },
                        set: function (value) {
                            value = !!value;
                            if (value === this.checked) {
                                return;
                            }

                            this._checked = value;
                            this._domElement.setAttribute('aria-checked', value);
                            if (value) {
                                _ElementUtilities.addClass(this._domElement, classOn);
                                _ElementUtilities.removeClass(this._domElement, classOff);
                            } else {
                                _ElementUtilities.addClass(this._domElement, classOff);
                                _ElementUtilities.removeClass(this._domElement, classOn);
                            }
                            this.dispatchEvent("change");
                        }
                    },
                    /// <field type="Boolean" locid="WinJS.UI.ToggleSwitch.disabled" helpKeyword="WinJS.UI.ToggleSwitch.disabled">
                    /// Gets or sets a value that specifies whether the control is disabled.
                    /// </field>
                    disabled: {
                        get: function () {
                            return this._disabled;
                        },
                        set: function (value) {
                            value = !!value;
                            if (value === this._disabled) {
                                return;
                            }

                            if (value) {
                                _ElementUtilities.addClass(this._domElement, classDisabled);
                                _ElementUtilities.removeClass(this._domElement, classEnabled);
                            } else {
                                _ElementUtilities.removeClass(this._domElement, classDisabled);
                                _ElementUtilities.addClass(this._domElement, classEnabled);
                            }

                            this._disabled = value;
                            this._domElement.setAttribute('aria-disabled', value);
                            this._domElement.setAttribute('tabIndex', value ? -1 : 0);
                        }
                    },
                    /// <field type="String" locid="WinJS.UI.ToggleSwitch.labelOn" helpKeyword="WinJS.UI.ToggleSwitch.labelOn">
                    /// Gets or sets the text that displays when the control is on (checked is set to true). The default value is "On".
                    /// </field>
                    labelOn: {
                        get: function () {
                            return this._labelOnElement.innerHTML;
                        },
                        set: function (value) {
                            this._labelOnElement.innerHTML = value;
                        }
                    },
                    /// <field type="String" locid="WinJS.UI.ToggleSwitch.labelOff" helpKeyword="WinJS.UI.ToggleSwitch.labelOff">
                    /// Gets or sets the text that displays when the control is off (checked is set to false). The default value is "Off".
                    /// </field>
                    labelOff: {
                        get: function () {
                            return this._labelOffElement.innerHTML;
                        },
                        set: function (value) {
                            this._labelOffElement.innerHTML = value;
                        }
                    },
                    /// <field type='String' locid="WinJS.UI.ToggleSwitch.title" helpKeyword="WinJS.UI.ToggleSwitch.title">
                    /// Gets or sets the main text for the ToggleSwitch control. This text is always displayed, regardless of whether
                    /// the control is switched on or off.
                    /// </field>
                    title: {
                        get: function () {
                            return this._headerElement.innerHTML;
                        },
                        set: function (value) {
                            this._headerElement.innerHTML = value;
                        }
                    },

                    // Events

                    /// <field type="Function" locid="WinJS.UI.ToggleSwitch.onchange" helpKeyword="WinJS.UI.ToggleSwitch.onchange">
                    /// Occurs when the ToggleSwitch control is flipped to on (checked == true) or off (checked == false).
                    /// </field>
                    onchange: _Events._createEventProperty('change'),

                    // Public methods
                    dispose: function ToggleSwitch_dispose() {
                        if (this._disposed) {
                            return;
                        }

                        this._disposed = true;
                    },

                    // Private event handlers
                    _ariaChangedHandler: function ToggleSwitch_ariaChanged() {
                        var value = this._domElement.getAttribute('aria-checked');
                        value = (value === 'true') ? true : false;
                        this.checked = value;
                    },
                    _keyDownHandler: function ToggleSwitch_keyDown(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Toggle checked on spacebar
                        if (e.keyCode === _ElementUtilities.Key.space) {
                            this.checked = !this.checked;
                        }

                        // Arrow keys set value
                        if (e.keyCode === _ElementUtilities.Key.rightArrow ||
                            e.keyCode === _ElementUtilities.Key.upArrow) {
                            this.checked = true;
                        }
                        if (e.keyCode === _ElementUtilities.Key.leftArrow ||
                            e.keyCode === _ElementUtilities.Key.downArrow) {
                            this.checked = false;
                        }

                    },
                    _pointerDownHandler: function ToggleSwitch_pointerDown(e) {
                        if (this.disabled || this._mousedown) {
                            return;
                        }

                        e.preventDefault();

                        this._mousedown = true;
                        this._dragXStart = e.pageX - this._trackElement.getBoundingClientRect().left;
                        this._dragX = this._dragXStart;
                        this._dragging = false;
                        _ElementUtilities.addClass(this._domElement, classPressed);

                        _ElementUtilities._globalListener.addEventListener(this._domElement, 'pointermove', this._boundPointerMove, true);
                        _ElementUtilities._globalListener.addEventListener(this._domElement, 'pointerup', this._boundPointerUp, true);
                        if (e.pointerType === _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH) {
                            _ElementUtilities._setPointerCapture(this._domElement, e.pointerId);
                        }
                    },
                    _pointerCancelHandler: function ToggleSwitch_pointerCancel(e) {
                        this._resetPressedState();
                        if (e.pointerType === _ElementUtilities._MSPointerEvent.MSPOINTER_TYPE_TOUCH) {
                            _ElementUtilities._releasePointerCapture(this._domElement, e.pointerId);
                        }
                    },
                    _pointerUpHandler: function ToggleSwitch_pointerUp(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Since up is a global event we should only take action
                        // if a mousedown was registered on us initially
                        if (!this._mousedown) {
                            return;
                        }

                        e = e.detail.originalEvent;
                        e.preventDefault();

                        // If the thumb is being dragged, pick a new value based on what the thumb
                        // was closest to
                        var trackRect = this._trackElement.getBoundingClientRect();
                        var thumbRect = this._thumbElement.getBoundingClientRect();
                        var isRTL = _Global.getComputedStyle(this._domElement).direction === 'rtl';
                        if (this._dragging) {
                            var maxX = trackRect.width - thumbRect.width;
                            this.checked = isRTL ? this._dragX < maxX / 2 : this._dragX >= maxX / 2;
                            this._dragging = false;
                            _ElementUtilities.removeClass(this._domElement, classDragging);
                        } else {
                            // Otherwise, just toggle the value as the up constitutes a
                            // click event
                            this.checked = !this.checked;
                        }

                        // Reset tracking variables and intermediate styles
                        this._resetPressedState();
                    },
                    _pointerMoveHandler: function ToggleSwitch_pointerMove(e) {
                        if (this.disabled) {
                            return;
                        }

                        // Not dragging if mouse isn't down
                        if (!this._mousedown) {
                            return;
                        }

                        e = e.detail.originalEvent;
                        e.preventDefault();

                        // Get pointer x coord relative to control
                        var trackRect = this._trackElement.getBoundingClientRect();
                        var localMouseX = e.pageX - trackRect.left;

                        // Not dragging if mouse is outside track
                        if (localMouseX > trackRect.width) {
                            return;
                        }

                        // Calculate a position for the thumb
                        var thumbRect = this._thumbElement.getBoundingClientRect();
                        var maxX = trackRect.width - thumbRect.width - 6;
                        this._dragX = Math.min(maxX, localMouseX - thumbRect.width / 2);
                        this._dragX = Math.max(2, this._dragX);

                        // Calculate if this pointermove constitutes switching to drag mode
                        if (!this._dragging && Math.abs(localMouseX - this._dragXStart) > 3) {
                            this._dragging = true;
                            _ElementUtilities.addClass(this._domElement, classDragging);
                        }

                        this._thumbElement.style.left = this._dragX + 'px';
                    },
                    _resetPressedState: function ToggleSwitch_resetPressedState() {
                        this._mousedown = false;
                        this._thumbElement.style.left = '';
                        _ElementUtilities.removeClass(this._domElement, classPressed);
                        _ElementUtilities._globalListener.removeEventListener(this._domElement, 'pointermove', this._boundPointerMove, true);
                        _ElementUtilities._globalListener.removeEventListener(this._domElement, 'pointerup', this._boundPointerUp, true);
                    }
                });

                // addEventListener, removeEventListener, dispatchEvent
                _Base.Class.mix(Toggle, _Control.DOMEventMixin);

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


<commit_before>// ==UserScript==
// @name       Twitch Player Plus
// @namespace  http://twitch.tv/ehsankia
// @version    1.8
// @description  Various tweaks to the Twitch HTML5 player UI
// @match      https://www.twitch.tv/*
// @match      https://player.twitch.tv/*
// @grant      GM_addStyle
// @grant      GM_getValue
// @grant      GM_setValue
// @require    http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @copyright  2015+, Ehsan Kia
// ==/UserScript==

var backend;
var html5Player;
var waitForPlayerReadyTimer = setInterval(function() {
    html5Player = $('div.player');
    if (html5Player.length > 0) {
      if (html5Player.attr('data-loading') === 'false') {
        html5Player.attr('data-tpp', 'true');
        clearInterval(waitForPlayerReadyTimer);
        registry = App.__container__.lookup('-view-registry:main');
        backend = registry[$('#player>div').attr('id')].get('player');
        setTimeout(applyFixes, 100);
        hostPlayerCheck();
      }
    }
}, 100);

function applyFixes() {

    // Move quality options to main bar
    $(".js-quality").insertAfter($('.js-quality-display-contain'));

    // Remove remaining label
    $("span:contains('Video Quality:')").remove();

    // Hide options if there are no transcoders
    checkForQualityOptions();
    setInterval(checkForQualityOptions, 5000);

    // Bind F key to toggle fullscreen
    $('body').keypress(function(e) {
      if (e.which === 102) {
        // fallback to event.target just in case
        var el = document.activeElement || e.target;
        var t  = (el && el.tagName.toLowerCase()) || '';

        // pass through to elements that take keyboard input
        if (/(input|textarea|select)/.test(t)) {
          return true;
        }

        $('.js-control-fullscreen').click();
        return false;
      }
    });

    // Add latency status under Live icon
    var liveIcon = $('.player-livestatus');
    liveIcon.append("

");
    setTimeout(updateLatency, 5000);

    // Remove old stats button and add new one
    $('.player-menu__item--stats').css('display', 'none');
    $('.js-control-fullscreen').before(" \
      <button type='button' class='player-button js-custom-stats-toggle'> \
         \
        <svg id='icon-stats' viewBox='0 0 1024 1024' style='width: 16px; fill: white; margin: 1px 6px;'> \
          <path d='M960 0h-896c-35.328 0-64 28.672-64 64v640c0 35.328 28.672 64 64 64h256l-128 256h32l230.4-256h115.2l230.4 256h32l-128-256h256c35.328 0 64-28.672 64-64v-640c0-35.328-28.672-64-64-64zM960 672c0 17.696-14.304 32-32 32h-832c-17.696 0-32-14.304-32-32v-576c0-17.696 14.304-32 32-32h832c17.696 0 32 14.304 32 32v576zM668.096 500.192l-144.672-372.128-158.016 297.28-88.192-90.72-149.216 92.992 42.112 24.256 95.616-59.584 115.36 118.784 133.6-251.296 147.712 380.128 125.984-265.216 51.328 109.248 56.288-9.44-107.328-228.224-120.576 253.92z'></path> \
        </svg> \
      </button>");
    $('.js-custom-stats-toggle').click(function(){
      var prev = $('.js-playback-stats').attr('data-state');
      var state = prev === 'on' ? 'off' : 'on';
      $('.js-playback-stats').attr('data-state', state);
    });

    // Check if it's a VOD and there isn't a seek argument in the url
    var vodID = html5Player.attr('data-video');
    var hasSeekParam = document.location.search.search("t=") >= 0;
    if (vodID !== undefined && !hasSeekParam) {
      //seek to previous position and keep track of the position
      var oldTime = GM_getValue("seek_" + vodID);
      if (oldTime !== undefined) {
        backend.setCurrentTime(parseFloat(oldTime));
      }
      setTimeout(function() {
        setInterval(trackSeekTime, 15000);
      }, 5 * 60 * 1000);
    }

    // Setup volume scrolling when in fullscreen or theatre mode
    $("#player").bind('mousewheel DOMMouseScroll', function(event){
      if (!backend.fullscreen && $('.app-main.theatre').length === 0 && !event.shiftKey) return;

      var dir = event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0;
      var delta = dir ? 0.05 : -0.05;

      var volume = backend.getVolume();
      volume = Math.min(Math.max(volume + delta, 0), 1);
      backend.setVolume(volume);

      event.preventDefault();
      event.stopPropagation();
      return false;
    });

    // Mark that we've fixed the player
    html5Player.attr('data-tpp', 'true');
}

function checkForQualityOptions() {
  var numQualityOptions = $(".js-quality > option").length;
  if (numQualityOptions > 1) {
    $('.js-quality').css('display', 'block');
  } else {
    $('.js-quality').css('display', 'none');
  }
}

function updateLatency() {
  var stats = backend.getVideoInfo();
  var lat = stats.hls_latency_broadcaster;
  if (lat === undefined || lat.length === 0) {
    setTimeout(updateLatency, 5000);
  } else {
    $('.lag-status').text((lat / 1000) + ' sec.');
    setTimeout(updateLatency, 1000);
  }
}

function trackSeekTime() {
  var seekTime = backend.getCurrentTime();
  if (seekTime < 5 * 60) return;
  GM_setValue("seek_" + vodID, seekTime);
}

function hostPlayerCheck() {
  setInterval(function() {
      $('div.player').each(function(ind, el){
        var player = $(el);
        if (player.attr('data-tpp') === 'true') return;
        if (player.attr('data-loading') !== 'false') return;

        // Move quality options to main bar
        container = player.find('.js-quality-display-contain');
        player.find(".js-quality").insertAfter(container);

        // Remove remaining label
        player.find("span:contains('Video Quality:')").remove();

        // Mark that we've fixed the player
        player.attr('data-tpp', 'true');
      });
  }, 5000);
}

GM_addStyle(" \
.js-volume-container { width: 13em; } \
select.js-quality:hover { color: #a991d4 !important; } \
select.js-quality, select.js-quality:focus { \
  float: left; \
  height: 29px; \
  margin: 0 6px 0 4px; \
  padding: 0; \
  color: white; \
  font-weight: bold; \
  background: none; \
  border: none; \
  box-shadow: 0 0 black; \
  appearance: none; \
  -moz-appearance: none; \
  -webkit-appearance: none; \
  outline: none; \
  cursor: pointer; \
} \
select.js-quality > option { \
  color: white; \
  background: black; \
  padding: 0 5px; \
  margin-right: -15px; \
  font-weight: bold; \
} \
.lag-status { \
  width: 60px; \
  text-align: center; \
} \
.js-custom-stats-toggle:hover > svg { \
  fill: #a991d4 !important; \
}");
<commit_msg>Use different string concatenation<commit_after>// ==UserScript==
// @name       Twitch Player Plus
// @namespace  http://twitch.tv/ehsankia
// @version    1.8
// @description  Various tweaks to the Twitch HTML5 player UI
// @match      https://www.twitch.tv/*
// @match      https://player.twitch.tv/*
// @grant      GM_addStyle
// @grant      GM_getValue
// @grant      GM_setValue
// @require    http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @copyright  2015+, Ehsan Kia
// ==/UserScript==

var backend;
var html5Player;
var waitForPlayerReadyTimer = setInterval(function() {
    html5Player = $('div.player');
    if (html5Player.length > 0) {
      if (html5Player.attr('data-loading') === 'false') {
        html5Player.attr('data-tpp', 'true');
        clearInterval(waitForPlayerReadyTimer);
        registry = App.__container__.lookup('-view-registry:main');
        backend = registry[$('#player>div').attr('id')].get('player');
        setTimeout(applyFixes, 100);
        hostPlayerCheck();
      }
    }
}, 100);

function applyFixes() {

    // Move quality options to main bar
    $(".js-quality").insertAfter($('.js-quality-display-contain'));

    // Remove remaining label
    $("span:contains('Video Quality:')").remove();

    // Hide options if there are no transcoders
    checkForQualityOptions();
    setInterval(checkForQualityOptions, 5000);

    // Bind F key to toggle fullscreen
    $('body').keypress(function(e) {
      if (e.which === 102) {
        // fallback to event.target just in case
        var el = document.activeElement || e.target;
        var t  = (el && el.tagName.toLowerCase()) || '';

        // pass through to elements that take keyboard input
        if (/(input|textarea|select)/.test(t)) {
          return true;
        }

        $('.js-control-fullscreen').click();
        return false;
      }
    });

    // Add latency status under Live icon
    var liveIcon = $('.player-livestatus');
    liveIcon.append("

");
    setTimeout(updateLatency, 5000);

    // Remove old stats button and add new one
    $('.player-menu__item--stats').css('display', 'none');
    $('.js-control-fullscreen').before(
      "<button type='button' class='player-button js-custom-stats-toggle'>" +
        "" +
        "<svg id='icon-stats' viewBox='0 0 1024 1024' style='width: 16px; fill: white; margin: 1px 6px;'>" +
          "<path d='M960 0h-896c-35.328 0-64 28.672-64 64v640c0 35.328 28.672 64 64 64h256l-128 256h32l230.4-256h115.2l230.4 256h32l-128-256h256c35.328 0 64-28.672 64-64v-640c0-35.328-28.672-64-64-64zM960 672c0 17.696-14.304 32-32 32h-832c-17.696 0-32-14.304-32-32v-576c0-17.696 14.304-32 32-32h832c17.696 0 32 14.304 32 32v576zM668.096 500.192l-144.672-372.128-158.016 297.28-88.192-90.72-149.216 92.992 42.112 24.256 95.616-59.584 115.36 118.784 133.6-251.296 147.712 380.128 125.984-265.216 51.328 109.248 56.288-9.44-107.328-228.224-120.576 253.92z'></path>" +
        "</svg>" +
      "</button>");
    $('.js-custom-stats-toggle').click(function(){
      var prev = $('.js-playback-stats').attr('data-state');
      var state = prev === 'on' ? 'off' : 'on';
      $('.js-playback-stats').attr('data-state', state);
    });

    // Check if it's a VOD and there isn't a seek argument in the url
    var vodID = html5Player.attr('data-video');
    var hasSeekParam = document.location.search.search("t=") >= 0;
    if (vodID !== undefined && !hasSeekParam) {
      //seek to previous position and keep track of the position
      var oldTime = GM_getValue("seek_" + vodID);
      if (oldTime !== undefined) {
        backend.setCurrentTime(parseFloat(oldTime));
      }
      setTimeout(function() {
        setInterval(trackSeekTime, 15000);
      }, 5 * 60 * 1000);
    }

    // Setup volume scrolling when in fullscreen or theatre mode
    $("#player").bind('mousewheel DOMMouseScroll', function(event){
      if (!backend.fullscreen && $('.app-main.theatre').length === 0 && !event.shiftKey) return;

      var dir = event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0;
      var delta = dir ? 0.05 : -0.05;

      var volume = backend.getVolume();
      volume = Math.min(Math.max(volume + delta, 0), 1);
      backend.setVolume(volume);

      event.preventDefault();
      event.stopPropagation();
      return false;
    });

    // Mark that we've fixed the player
    html5Player.attr('data-tpp', 'true');
}

function checkForQualityOptions() {
  var numQualityOptions = $(".js-quality > option").length;
  if (numQualityOptions > 1) {
    $('.js-quality').css('display', 'block');
  } else {
    $('.js-quality').css('display', 'none');
  }
}

function updateLatency() {
  var stats = backend.getVideoInfo();
  var lat = stats.hls_latency_broadcaster;
  if (lat === undefined || lat.length === 0) {
    setTimeout(updateLatency, 5000);
  } else {
    $('.lag-status').text((lat / 1000) + ' sec.');
    setTimeout(updateLatency, 1000);
  }
}

function trackSeekTime() {
  var seekTime = backend.getCurrentTime();
  if (seekTime < 5 * 60) return;
  GM_setValue("seek_" + vodID, seekTime);
}

function hostPlayerCheck() {
  setInterval(function() {
      $('div.player').each(function(ind, el){
        var player = $(el);
        if (player.attr('data-tpp') === 'true') return;
        if (player.attr('data-loading') !== 'false') return;

        // Move quality options to main bar
        container = player.find('.js-quality-display-contain');
        player.find(".js-quality").insertAfter(container);

        // Remove remaining label
        player.find("span:contains('Video Quality:')").remove();

        // Mark that we've fixed the player
        player.attr('data-tpp', 'true');
      });
  }, 5000);
}

GM_addStyle(
".js-volume-container { width: 13em; }" +
"select.js-quality:hover { color: #a991d4 !important; }" +
"select.js-quality, select.js-quality:focus {" +
"  float: left;" +
"  height: 29px;" +
"  margin: 0 6px 0 4px;" +
"  padding: 0;" +
"  color: white;" +
"  font-weight: bold;" +
"  background: none;" +
"  border: none;" +
"  box-shadow: 0 0 black;" +
"  appearance: none;" +
"  -moz-appearance: none;" +
"  -webkit-appearance: none;" +
"  outline: none;" +
"  cursor: pointer;" +
"}" +
"select.js-quality > option {" +
"  color: white;" +
"  background: black;" +
"  padding: 0 5px;" +
"  margin-right: -15px;" +
"  font-weight: bold;" +
"}" +
".lag-status {" +
"  width: 60px;" +
"  text-align: center;" +
"}" +
".js-custom-stats-toggle:hover > svg {" +
"  fill: #a991d4 !important;" +
"}");
<|endoftext|>


<commit_before>/* eslint indent: "off" */
import PerfectScrollbar from 'perfect-scrollbar';

function registerCodeSection($codeSection) {
    const $li = $codeSection.find("ul.nav li");
    const $blocks = $codeSection.find(".blocks div");

    $li.click(function () {
        const language = this.dataset.language;

        $li.removeClass("active");
        $li.filter("[data-language=" + language + "]").addClass("active");

        $blocks.removeClass("active");
        $blocks.filter("[data-language=" + language + "]").addClass("active");
    });

    $li.eq(0).click();
}

function highlight_current_article() {
    $('.help .sidebar a').removeClass('highlighted');
    var path = window.location.href.match(/\/(help|api)\/.*/);

    if (!path) {
        return;
    }

    var article = $('.help .sidebar a[href="' + path[0] + '"]');
    // Highlight current article link and the heading of the same
    article.closest('ul').css('display', 'block');
    article.addClass('highlighted');
}

function adjust_mac_shortcuts() {
    var keys_map = new Map([
        ['Backspace', 'Delete'],
        ['Enter', 'Return'],
        ['Home', 'Fn + ⇽'],
        ['End', 'Fn + ⇾'],
        ['PgUp', 'Fn + ↑'],
        ['PgDn', 'Fn + ↓'],
    ]);

    $(".markdown .content code").each(function () {
        var text = $(this).text();

        if (!keys_map.has(text)) {
            return;
        }

        var key_string = keys_map.get(text);
        var keys = key_string.match(/[^\s\+]+/g);

        _.each(keys, function (key) {
            key_string = key_string.replace(key, '' + key + '');
        });

        $(this).replaceWith(key_string);
    });
}

function render_code_sections() {
    $(".code-section").each(function () {
        registerCodeSection($(this));
    });

    highlight_current_article();

    if (/Mac/i.test(navigator.userAgent)) {
        adjust_mac_shortcuts();
    }

    $("table").each(function () {
        $(this).addClass("table table-striped");
    });
}

function scrollToHash(container) {
    var hash = window.location.hash;
    if (hash !== '') {
        container.scrollTop = $(hash).position().top - $('.markdown .content').position().top;
    } else {
        container.scrollTop = 0;
    }
}

(function () {
    var html_map = {};
    var loading = {
        name: null,
    };

    var markdownPS = new PerfectScrollbar($(".markdown")[0], {
        suppressScrollX: true,
        useKeyboard: false,
        wheelSpeed: 0.68,
        scrollingThreshold: 50,
    });

    var fetch_page = function (path, callback) {
        $.get(path, function (res) {
            var $html = $(res).find(".markdown .content");
            $html.find(".back-to-home").remove();

            callback($html.html().trim());
            render_code_sections();
        });
    };

    var update_page = function (html_map, path, container) {
        if (html_map[path]) {
            $(".markdown .content").html(html_map[path]);
            render_code_sections();
            markdownPS.update();
            scrollToHash(container);
        } else {
            loading.name = path;
            fetch_page(path, function (res) {
                html_map[path] = res;
                $(".markdown .content").html(html_map[path]);
                loading.name = null;
                markdownPS.update();
                scrollToHash(container);
            });
        }
    };

    new PerfectScrollbar($(".sidebar")[0], {
        suppressScrollX: true,
        useKeyboard: false,
        wheelSpeed: 0.68,
        scrollingThreshold: 50,
    });

    $(".sidebar.slide h2").click(function (e) {
        var $next = $(e.target).next();

        if ($next.is("ul")) {
            $next.slideToggle("fast", "swing", function () {
                markdownPS.update();
            });
        }
    });

    $(".sidebar a").click(function (e) {
        var path = $(this).attr("href");
        var path_dir = path.split('/')[1];
        var current_dir = window.location.pathname.split('/')[1];
        var container = $(".markdown")[0];

        // Do not block redirecting to external URLs
        if (path_dir !== current_dir) {
            return;
        }

        if (loading.name === path) {
            return;
        }

        history.pushState({}, "", path);

        update_page(html_map, path, container);

        $(".sidebar").removeClass("show");

        e.preventDefault();
    });

    // Show Guides user docs in sidebar by default
    $('.help .sidebar h2#guides + ul').css('display', 'block');

    // Remove ID attributes from sidebar links so they don't conflict with index page anchor links
    $('.help .sidebar h1, .help .sidebar h2, .help .sidebar h3').removeAttr('id');

    // Scroll to anchor link when clicked
    $('.markdown .content h1, .markdown .content h2, .markdown .content h3').on('click', function () {
        window.location.href = window.location.href.replace(/#.*/, '') + '#' + $(this).attr("id");
    });

    $(".hamburger").click(function () {
        $(".sidebar").toggleClass("show");
    });

    $(".markdown").click(function () {
        if ($(".sidebar.show").length) {
            $(".sidebar.show").toggleClass("show");
        }
    });

    render_code_sections();

    // Finally, make sure if we loaded a window with a hash, we scroll
    // to the right place.
    var container = $(".markdown")[0];
    scrollToHash(container);

    window.onresize = function () {
        markdownPS.update();
    };

    window.addEventListener("popstate", function () {
        var path = window.location.pathname;
        update_page(html_map, path, container);
    });

}());
<commit_msg>help: Close other article headings on click.<commit_after>/* eslint indent: "off" */
import PerfectScrollbar from 'perfect-scrollbar';

function registerCodeSection($codeSection) {
    const $li = $codeSection.find("ul.nav li");
    const $blocks = $codeSection.find(".blocks div");

    $li.click(function () {
        const language = this.dataset.language;

        $li.removeClass("active");
        $li.filter("[data-language=" + language + "]").addClass("active");

        $blocks.removeClass("active");
        $blocks.filter("[data-language=" + language + "]").addClass("active");
    });

    $li.eq(0).click();
}

function highlight_current_article() {
    $('.help .sidebar a').removeClass('highlighted');
    var path = window.location.href.match(/\/(help|api)\/.*/);

    if (!path) {
        return;
    }

    var article = $('.help .sidebar a[href="' + path[0] + '"]');
    // Highlight current article link and the heading of the same
    article.closest('ul').css('display', 'block');
    article.addClass('highlighted');
}

function adjust_mac_shortcuts() {
    var keys_map = new Map([
        ['Backspace', 'Delete'],
        ['Enter', 'Return'],
        ['Home', 'Fn + ⇽'],
        ['End', 'Fn + ⇾'],
        ['PgUp', 'Fn + ↑'],
        ['PgDn', 'Fn + ↓'],
    ]);

    $(".markdown .content code").each(function () {
        var text = $(this).text();

        if (!keys_map.has(text)) {
            return;
        }

        var key_string = keys_map.get(text);
        var keys = key_string.match(/[^\s\+]+/g);

        _.each(keys, function (key) {
            key_string = key_string.replace(key, '' + key + '');
        });

        $(this).replaceWith(key_string);
    });
}

function render_code_sections() {
    $(".code-section").each(function () {
        registerCodeSection($(this));
    });

    highlight_current_article();

    if (/Mac/i.test(navigator.userAgent)) {
        adjust_mac_shortcuts();
    }

    $("table").each(function () {
        $(this).addClass("table table-striped");
    });
}

function scrollToHash(container) {
    var hash = window.location.hash;
    if (hash !== '') {
        container.scrollTop = $(hash).position().top - $('.markdown .content').position().top;
    } else {
        container.scrollTop = 0;
    }
}

(function () {
    var html_map = {};
    var loading = {
        name: null,
    };

    var markdownPS = new PerfectScrollbar($(".markdown")[0], {
        suppressScrollX: true,
        useKeyboard: false,
        wheelSpeed: 0.68,
        scrollingThreshold: 50,
    });

    var fetch_page = function (path, callback) {
        $.get(path, function (res) {
            var $html = $(res).find(".markdown .content");
            $html.find(".back-to-home").remove();

            callback($html.html().trim());
            render_code_sections();
        });
    };

    var update_page = function (html_map, path, container) {
        if (html_map[path]) {
            $(".markdown .content").html(html_map[path]);
            render_code_sections();
            markdownPS.update();
            scrollToHash(container);
        } else {
            loading.name = path;
            fetch_page(path, function (res) {
                html_map[path] = res;
                $(".markdown .content").html(html_map[path]);
                loading.name = null;
                markdownPS.update();
                scrollToHash(container);
            });
        }
    };

    new PerfectScrollbar($(".sidebar")[0], {
        suppressScrollX: true,
        useKeyboard: false,
        wheelSpeed: 0.68,
        scrollingThreshold: 50,
    });

    $(".sidebar.slide h2").click(function (e) {
        var $next = $(e.target).next();

        if ($next.is("ul")) {
            // Close other article's headings first
            $('.sidebar ul').not($next).hide();
            // Toggle the heading
            $next.slideToggle("fast", "swing", function () {
                markdownPS.update();
            });
        }
    });

    $(".sidebar a").click(function (e) {
        var path = $(this).attr("href");
        var path_dir = path.split('/')[1];
        var current_dir = window.location.pathname.split('/')[1];
        var container = $(".markdown")[0];

        // Do not block redirecting to external URLs
        if (path_dir !== current_dir) {
            return;
        }

        if (loading.name === path) {
            return;
        }

        history.pushState({}, "", path);

        update_page(html_map, path, container);

        $(".sidebar").removeClass("show");

        e.preventDefault();
    });

    // Show Guides user docs in sidebar by default
    $('.help .sidebar h2#guides + ul').css('display', 'block');

    // Remove ID attributes from sidebar links so they don't conflict with index page anchor links
    $('.help .sidebar h1, .help .sidebar h2, .help .sidebar h3').removeAttr('id');

    // Scroll to anchor link when clicked
    $('.markdown .content h1, .markdown .content h2, .markdown .content h3').on('click', function () {
        window.location.href = window.location.href.replace(/#.*/, '') + '#' + $(this).attr("id");
    });

    $(".hamburger").click(function () {
        $(".sidebar").toggleClass("show");
    });

    $(".markdown").click(function () {
        if ($(".sidebar.show").length) {
            $(".sidebar.show").toggleClass("show");
        }
    });

    render_code_sections();

    // Finally, make sure if we loaded a window with a hash, we scroll
    // to the right place.
    var container = $(".markdown")[0];
    scrollToHash(container);

    window.onresize = function () {
        markdownPS.update();
    };

    window.addEventListener("popstate", function () {
        var path = window.location.pathname;
        update_page(html_map, path, container);
    });

}());
<|endoftext|>


<commit_before>'use strict';

/* globals Connector, _ */

var scrobbleMusicOnly = false;
chrome.storage.local.get('Connectors', function(data) {
	if (data && data.Connectors && data.Connectors.YouTube) {
		var options = data.Connectors.YouTube;
		if (options.scrobbleMusicOnly === true) {
			scrobbleMusicOnly = true;
		}

		console.log('connector options: ' + JSON.stringify(options));
	}
});

Connector.playerSelector = '#player-api';

Connector.artistTrackSelector = '#eow-title';

Connector.currentTimeSelector = '#player-api .ytp-time-current';

Connector.durationSelector = '#player-api .ytp-time-duration';

Connector.getUniqueID = function() {
	var url = window.location.href;
	var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
	var match = url.match(regExp);
	if (match && match[7].length==11){
		return match[7];
	}
};

Connector.isPlaying = function() {
	return (
		/* Can scrobble from any genre */ !scrobbleMusicOnly ||
		/* OR only music AND is music  */ ( scrobbleMusicOnly && $('meta[itemprop=\"genre\"]').attr('content') == 'Music' )
	)	? $('#player-api .html5-video-player').hasClass('playing-mode')
		: false;
};

Connector.getArtistTrack = function () {
	var text = $(Connector.artistTrackSelector).text();
	var separator = Connector.findSeparator(text);

	var artist = null;
	var track = null;

	if (separator !== null) {
		artist = text.substr(0, separator.index);
		track = text.substr(separator.index + separator.length);
	}

	return {
		artist: cleanseArtist(artist),
		track: cleanseTrack(track)
	};
};

var timestampPattern = '[0-9]{0,2}:*[0-9]{1,2}:[0-9]{2}';
var timestampRegex = new RegExp(timestampPattern,'gi');

// Run getPlaylist once very 5s, as it involves quite strenuous regex / jquery parsing of the DOM
// More to the point: the playlist only changes when the YT page does, so no point constantly scanning it.
Connector.getPlaylist = _.throttle(getPlaylist, 5000);

function getPlaylist() {
	var playlist = [];

	var $containers = $('#eow-description, .comment-text-content');
	var i = 0;
	var found = false;

	while($containers.get(i) && !found) {
		var $container = $($containers.get(i));
		var potentialPlaylist = null;

		if(timestampRegex.test($container.html())) {
			var potentialTracks = $container.html().split(/\r\n|\r|\n|
/g);
			potentialPlaylist = buildPlaylist(potentialTracks);
		}

		if (typeof potentialPlaylist !== 'undefined' && potentialPlaylist !== null) {
			playlist = potentialPlaylist;
			found = true;
		} else {
			i++;
		}
	}

	if(!found) {
		console.info('No (valid) playlists found; this is a single-track media.');
		return null;
	} else {
		console.info('This is a playlist.', playlist);
		return playlist;
	}
}

function buildPlaylist(potentialTracks) {
	var stringProperty = 'track'; // Used further down to ID which property to tweak en-masse.
	var potentialPlaylist = parsePlaylist(potentialTracks);

	/**
	 * Check that this 'playlist' actually has enough....
	*/
	if(typeof potentialPlaylist === 'undefined' || potentialPlaylist === null || potentialPlaylist.length < 3) {
		return null;
	}

	// Do NOT rely on playlist numbering! e.g. https://www.youtube.com/watch?v=AIQKIcNGfZ8
	potentialPlaylist = _.sortBy(potentialPlaylist, function(track) { return track.startTime; });

	/**
	 * If the last timestamp starts after the video ends... well, it's probably a broken playlist
	 * E.g. https://www.youtube.com/watch?v=EfcY9oFo1YQ
	*/
	if(Connector.getDuration() && potentialPlaylist[potentialPlaylist.length-1].startTime > Connector.getDuration()) {
		console.info('Invalid playlist - timestamps greater than clip duration');
		return null;
	}

	/**
	 * A playlist will be:
	 * - EITHER entirely `artistTrack` (1. artistName - trackName)
	 * - OR entirely `track` (1. trackName )
	 * So make corrections for occassional false-positive `artistTrack` recognitions.
	 * e.g. 5. Undone - The Sweater Song (by Weezer) in https://www.youtube.com/watch?v=VFOF47nalCY
	*/
	var propertyCount = _.countBy(potentialPlaylist, function(track) {
		if(track.artistTrack) { return 'artistTrack'; }
		if(track.track) { return 'track'; }
		return;
	});
	// If there's a mixture of tracks and ArtistTracks, something's clearly fishy...
	if(propertyCount.track > 0 && propertyCount.artistTrack > 0) {
		console.info('Playlist parser - fixing mashup of artistTrack + track', propertyCount);
		// if there are a large number of tracks identified as NOT artistTrack
		// then put the data in .track and cleanse .artistTrack;
		if(propertyCount.track > propertyCount.artistTrack) {
			// console.info('Playlist parser - converting all to TRACK');
			_.each(potentialPlaylist, function(track) {
				track.track = track.artistTrack || track.track;
				delete track.artistTrack;
			});
			stringProperty = 'track';
		}
		// Otherwise it's likely the reverse situation,
		// so put all data in .artistTrack and cleanse .track
		else {
			// # console.info('Playlist parser - converting all to ARTIST_TRACK');
			_.each(potentialPlaylist, function(track) {
				track.artistTrack = track.track || track.artistTrack;
				delete track.track;
			});
			stringProperty = 'artistTrack';
		}
	} else if(propertyCount.artistTrack > 0) {
		stringProperty = 'artistTrack';
	}

	/**
	 * If most tracks have numbers at the beginning, it's probably a case of shoddy playlist numbering
	 * e.g. https://www.youtube.com/watch?v=epSWiHu7ggk
	*/
	var beginningNumberRegex = new RegExp(/^\s*[0-9]{1,2}[\s\-:\/](.+)/i);
	var isNumbered = _.countBy(potentialPlaylist, function(track) {
		return track[stringProperty].match(beginningNumberRegex) ? 'is' : 'isnt';
	});
	if(isNumbered.is > isNumbered.isnt) {
		console.info('Playlist parser - removing non-delimitered numbering from all track starts.',isNumbered);
		_.each(potentialPlaylist, function(track) {
			track[stringProperty] = track[stringProperty].replace(beginningNumberRegex,'$1');
		});
	}

	return potentialPlaylist;
}

function parsePlaylist(potentialTracks) {
	var potentialPlaylist = [];

	_.each(potentialTracks, function(maybeTrack) {
		var entry = {};

		var $maybeTrack = $('
'+maybeTrack+'
');

		// YouTube automatically adds markup to timestamps...
		var $timestampEls = $maybeTrack.find('a[onclick*=\'seekTo\']');
		// ... but not when HH:MM exceeds 59:59, so also search for HH:MM
		var timestampStrs = maybeTrack.match(timestampRegex);

		// No timestamps in this line.
		if(($timestampEls === null || !$timestampEls.length) && (timestampStrs === null || !timestampStrs.length)) {
			return false;
		}

		if ($timestampEls !== null && $timestampEls.length) {
			entry.startTime = Connector.stringToSeconds($timestampEls.first().text());
		} else if (timestampStrs !== null && timestampStrs.length) {
			entry.startTime = Connector.stringToSeconds(timestampStrs[0]);
		}

		// Cleanse trackArtist data of timestamp, delimiters, etc.
		maybeTrack = cleansePlaylistLine(maybeTrack);
		maybeTrack = cleanseTrack(maybeTrack);

		// Check that it's not just a random sentence.
		// e.g. "Comment which is your favourite 19:13..." @ https://www.youtube.com/watch?v=_8pyf6ZW4Dk
		if(maybeTrack.length > 60) {
			return false;
		}

		// Are these tracks by a single artist, or artistTrack (a compilation e.g. https://www.youtube.com/watch?v=EzjX0QE_l8U)?
		var separator = Connector.findSeparator(maybeTrack);
		if (separator !== null) {
			entry.artistTrack = maybeTrack;
		} else {
			entry.track = maybeTrack;
		}

		potentialPlaylist.push(entry);
	});

	return potentialPlaylist;
}

function cleansePlaylistLine(maybeTrack) {
	maybeTrack = maybeTrack.replace(timestampRegex,'__TIMESTAMP__');
	maybeTrack = maybeTrack.replace(/[\[\(\{\–\-]*\s*[0-9]{1,2}\.[0-9]{2}\s*[\]\)\}\–\-:]/gi,''); // Chop out bullshit other misformatted timestamps (e.g. https://www.youtube.com/watch?v=9-NFosnfd2c)
	maybeTrack = maybeTrack.replace(/(__TIMESTAMP__)<\/a>/gi,'$1');
	maybeTrack = maybeTrack.replace(/\s*[\[\(\{\–\-]*\s*__TIMESTAMP__\s*[\]\)\}\–\-:]*\s*/gi,''); // [00:00] (e.g. https://www.youtube.com/watch?v=YKkOxFoE5yo / https://www.youtube.com/watch?v=thUQr7Q1vCY)
	maybeTrack = maybeTrack.replace('__TIMESTAMP__','');
	maybeTrack = maybeTrack.replace(/\s*&[a-z]+;\s*/gi,''); // remove HTML entity codes (e.g. https://www.youtube.com/watch?v=VFOF47nalCY)
	maybeTrack = maybeTrack.replace(/^\s*[-:=]\s*/gi,''); // HH:MM - Track
	maybeTrack = maybeTrack.replace(/\s*(track|number|no|no\.|song)?\s*[\[\(\{\-]*\s*[0-9]{1,2}\s*[\.\-:=\)\]\}]{1,3}\s*/i,''); // numbering  1.  1 -  (1) etc. (e.g. https://www.youtube.com/watch?v=Y7QQS5V3cnI) and "track 1." (e.g. https://www.youtube.com/watch?v=FijBkSvN6N8)

	return maybeTrack;
}

function cleanseArtist(artist) {
	if(typeof artist === 'undefined' | artist === null) { return; }

	artist = artist.replace(/^\s+|\s+$/g, '');
	artist = artist.replace(/\s+/g, ' ');

	return artist;
}

function cleanseTrack(track) {
	if(typeof track === 'undefined' | track === null) { return; }

	track = track.replace(/^\s+|\s+$/g, '');
	track = track.replace(/\s+/g, ' ');

	// Strip crap
	track = track.replace(/\s*\*+\s?\S+\s?\*+$/, ''); // **NEW**
	track = track.replace(/\s*\[[^\]]+\]$/, ''); // [whatever]
	track = track.replace(/\s*\([^\)]*version\)$/i, ''); // (whatever version)
	track = track.replace(/\s*\.(avi|wmv|mpg|mpeg|flv)$/i, ''); // video extensions
	track = track.replace(/\s*(LYRIC VIDEO\s*)?(lyric video\s*)/i, ''); // (LYRIC VIDEO)
	track = track.replace(/\s*(Official Track Stream*)/i, ''); // (Official Track Stream)
	track = track.replace(/\s*(of+icial\s*)?(music\s*)?video/i, ''); // (official)? (music)? video
	track = track.replace(/\s*(of+icial\s*)?(music\s*)?audio/i, ''); // (official)? (music)? audio
	track = track.replace(/\s*(ALBUM TRACK\s*)?(album track\s*)/i, ''); // (ALBUM TRACK)
	track = track.replace(/\s*(FULL ALBUM\s*)?(full album\s*)/i, ''); // (FULL ALBUM)
	track = track.replace(/\s*(COVER ART\s*)?(Cover Art\s*)/i, ''); // (Cover Art)
	track = track.replace(/$\s*(REMASTERED\s*)?(remastered\s*)/i, ''); // (REMASTERED)
	track = track.replace(/\s*\(\s*of+icial\s*\)/i, ''); // (official)
	track = track.replace(/\s*\(\s*[0-9]{4}\s*\)/i, ''); // (1999)
	track = track.replace(/\s+\(\s*(HD|HQ)\s*\)$/, ''); // HD (HQ)
	track = track.replace(/\s+(HD|HQ)\s*$/, ''); // HD (HQ)
	track = track.replace(/\s*video\s*clip/i, ''); // video clip
	track = track.replace(/\s+\(?live\)?$/i, ''); // live
	track = track.replace(/\(+\s*\)+/, ''); // Leftovers after e.g. (official video)
	track = track.replace(/^(|.*\s)"(.*)"(\s.*|)$/, '$2'); // Artist - The new "Track title" featuring someone
	track = track.replace(/^(|.*\s)'(.*)'(\s.*|)$/, '$2'); // 'Track title'
	track = track.replace(/^[\/\s,:;~-\s"]+/, ''); // trim starting white chars and dash
	track = track.replace(/[\/\s,:;~-\s"\s!]+$/, ''); // trim trailing white chars and dash
	//" and ! added because some track names end as {"Some Track" Official Music Video!} and it becomes {"Some Track"!} example: http://www.youtube.com/watch?v=xj_mHi7zeRQ

	return track;
}
<commit_msg>Reduce youtube update interval to 3 seconds.<commit_after>'use strict';

/* globals Connector, _ */

var scrobbleMusicOnly = false;
chrome.storage.local.get('Connectors', function(data) {
	if (data && data.Connectors && data.Connectors.YouTube) {
		var options = data.Connectors.YouTube;
		if (options.scrobbleMusicOnly === true) {
			scrobbleMusicOnly = true;
		}

		console.log('connector options: ' + JSON.stringify(options));
	}
});

Connector.playerSelector = '#player-api';

Connector.artistTrackSelector = '#eow-title';

Connector.currentTimeSelector = '#player-api .ytp-time-current';

Connector.durationSelector = '#player-api .ytp-time-duration';

Connector.getUniqueID = function() {
	var url = window.location.href;
	var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*/;
	var match = url.match(regExp);
	if (match && match[7].length==11){
		return match[7];
	}
};

Connector.isPlaying = function() {
	return (
		/* Can scrobble from any genre */ !scrobbleMusicOnly ||
		/* OR only music AND is music  */ ( scrobbleMusicOnly && $('meta[itemprop=\"genre\"]').attr('content') == 'Music' )
	)	? $('#player-api .html5-video-player').hasClass('playing-mode')
		: false;
};

Connector.getArtistTrack = function () {
	var text = $(Connector.artistTrackSelector).text();
	var separator = Connector.findSeparator(text);

	var artist = null;
	var track = null;

	if (separator !== null) {
		artist = text.substr(0, separator.index);
		track = text.substr(separator.index + separator.length);
	}

	return {
		artist: cleanseArtist(artist),
		track: cleanseTrack(track)
	};
};

var timestampPattern = '[0-9]{0,2}:*[0-9]{1,2}:[0-9]{2}';
var timestampRegex = new RegExp(timestampPattern,'gi');

// Run getPlaylist once very 3s, as it involves quite strenuous regex / jquery parsing of the DOM
// More to the point: the playlist only changes when the YT page does, so no point constantly scanning it.
Connector.setThrottleInterval(3000);

Connector.getPlaylist = function () {
	var playlist = [];

	var $containers = $('#eow-description, .comment-text-content');
	var i = 0;
	var found = false;

	while($containers.get(i) && !found) {
		var $container = $($containers.get(i));
		var potentialPlaylist = null;

		if(timestampRegex.test($container.html())) {
			var potentialTracks = $container.html().split(/\r\n|\r|\n|
/g);
			potentialPlaylist = buildPlaylist(potentialTracks);
		}

		if (typeof potentialPlaylist !== 'undefined' && potentialPlaylist !== null) {
			playlist = potentialPlaylist;
			found = true;
		} else {
			i++;
		}
	}

	if(!found) {
		console.info('No (valid) playlists found; this is a single-track media.');
		return null;
	} else {
		console.info('This is a playlist.', playlist);
		return playlist;
	}
};

function buildPlaylist(potentialTracks) {
	var stringProperty = 'track'; // Used further down to ID which property to tweak en-masse.
	var potentialPlaylist = parsePlaylist(potentialTracks);

	/**
	 * Check that this 'playlist' actually has enough....
	*/
	if(typeof potentialPlaylist === 'undefined' || potentialPlaylist === null || potentialPlaylist.length < 3) {
		return null;
	}

	// Do NOT rely on playlist numbering! e.g. https://www.youtube.com/watch?v=AIQKIcNGfZ8
	potentialPlaylist = _.sortBy(potentialPlaylist, function(track) { return track.startTime; });

	/**
	 * If the last timestamp starts after the video ends... well, it's probably a broken playlist
	 * E.g. https://www.youtube.com/watch?v=EfcY9oFo1YQ
	*/
	if(Connector.getDuration() && potentialPlaylist[potentialPlaylist.length-1].startTime > Connector.getDuration()) {
		console.info('Invalid playlist - timestamps greater than clip duration');
		return null;
	}

	/**
	 * A playlist will be:
	 * - EITHER entirely `artistTrack` (1. artistName - trackName)
	 * - OR entirely `track` (1. trackName )
	 * So make corrections for occassional false-positive `artistTrack` recognitions.
	 * e.g. 5. Undone - The Sweater Song (by Weezer) in https://www.youtube.com/watch?v=VFOF47nalCY
	*/
	var propertyCount = _.countBy(potentialPlaylist, function(track) {
		if(track.artistTrack) { return 'artistTrack'; }
		if(track.track) { return 'track'; }
		return;
	});
	// If there's a mixture of tracks and ArtistTracks, something's clearly fishy...
	if(propertyCount.track > 0 && propertyCount.artistTrack > 0) {
		console.info('Playlist parser - fixing mashup of artistTrack + track', propertyCount);
		// if there are a large number of tracks identified as NOT artistTrack
		// then put the data in .track and cleanse .artistTrack;
		if(propertyCount.track > propertyCount.artistTrack) {
			// console.info('Playlist parser - converting all to TRACK');
			_.each(potentialPlaylist, function(track) {
				track.track = track.artistTrack || track.track;
				delete track.artistTrack;
			});
			stringProperty = 'track';
		}
		// Otherwise it's likely the reverse situation,
		// so put all data in .artistTrack and cleanse .track
		else {
			// # console.info('Playlist parser - converting all to ARTIST_TRACK');
			_.each(potentialPlaylist, function(track) {
				track.artistTrack = track.track || track.artistTrack;
				delete track.track;
			});
			stringProperty = 'artistTrack';
		}
	} else if(propertyCount.artistTrack > 0) {
		stringProperty = 'artistTrack';
	}

	/**
	 * If most tracks have numbers at the beginning, it's probably a case of shoddy playlist numbering
	 * e.g. https://www.youtube.com/watch?v=epSWiHu7ggk
	*/
	var beginningNumberRegex = new RegExp(/^\s*[0-9]{1,2}[\s\-:\/](.+)/i);
	var isNumbered = _.countBy(potentialPlaylist, function(track) {
		return track[stringProperty].match(beginningNumberRegex) ? 'is' : 'isnt';
	});
	if(isNumbered.is > isNumbered.isnt) {
		console.info('Playlist parser - removing non-delimitered numbering from all track starts.',isNumbered);
		_.each(potentialPlaylist, function(track) {
			track[stringProperty] = track[stringProperty].replace(beginningNumberRegex,'$1');
		});
	}

	return potentialPlaylist;
}

function parsePlaylist(potentialTracks) {
	var potentialPlaylist = [];

	_.each(potentialTracks, function(maybeTrack) {
		var entry = {};

		var $maybeTrack = $('
'+maybeTrack+'
');

		// YouTube automatically adds markup to timestamps...
		var $timestampEls = $maybeTrack.find('a[onclick*=\'seekTo\']');
		// ... but not when HH:MM exceeds 59:59, so also search for HH:MM
		var timestampStrs = maybeTrack.match(timestampRegex);

		// No timestamps in this line.
		if(($timestampEls === null || !$timestampEls.length) && (timestampStrs === null || !timestampStrs.length)) {
			return false;
		}

		if ($timestampEls !== null && $timestampEls.length) {
			entry.startTime = Connector.stringToSeconds($timestampEls.first().text());
		} else if (timestampStrs !== null && timestampStrs.length) {
			entry.startTime = Connector.stringToSeconds(timestampStrs[0]);
		}

		// Cleanse trackArtist data of timestamp, delimiters, etc.
		maybeTrack = cleansePlaylistLine(maybeTrack);
		maybeTrack = cleanseTrack(maybeTrack);

		// Check that it's not just a random sentence.
		// e.g. "Comment which is your favourite 19:13..." @ https://www.youtube.com/watch?v=_8pyf6ZW4Dk
		if(maybeTrack.length > 60) {
			return false;
		}

		// Are these tracks by a single artist, or artistTrack (a compilation e.g. https://www.youtube.com/watch?v=EzjX0QE_l8U)?
		var separator = Connector.findSeparator(maybeTrack);
		if (separator !== null) {
			entry.artistTrack = maybeTrack;
		} else {
			entry.track = maybeTrack;
		}

		potentialPlaylist.push(entry);
	});

	return potentialPlaylist;
}

function cleansePlaylistLine(maybeTrack) {
	maybeTrack = maybeTrack.replace(timestampRegex,'__TIMESTAMP__');
	maybeTrack = maybeTrack.replace(/[\[\(\{\–\-]*\s*[0-9]{1,2}\.[0-9]{2}\s*[\]\)\}\–\-:]/gi,''); // Chop out bullshit other misformatted timestamps (e.g. https://www.youtube.com/watch?v=9-NFosnfd2c)
	maybeTrack = maybeTrack.replace(/(__TIMESTAMP__)<\/a>/gi,'$1');
	maybeTrack = maybeTrack.replace(/\s*[\[\(\{\–\-]*\s*__TIMESTAMP__\s*[\]\)\}\–\-:]*\s*/gi,''); // [00:00] (e.g. https://www.youtube.com/watch?v=YKkOxFoE5yo / https://www.youtube.com/watch?v=thUQr7Q1vCY)
	maybeTrack = maybeTrack.replace('__TIMESTAMP__','');
	maybeTrack = maybeTrack.replace(/\s*&[a-z]+;\s*/gi,''); // remove HTML entity codes (e.g. https://www.youtube.com/watch?v=VFOF47nalCY)
	maybeTrack = maybeTrack.replace(/^\s*[-:=]\s*/gi,''); // HH:MM - Track
	maybeTrack = maybeTrack.replace(/\s*(track|number|no|no\.|song)?\s*[\[\(\{\-]*\s*[0-9]{1,2}\s*[\.\-:=\)\]\}]{1,3}\s*/i,''); // numbering  1.  1 -  (1) etc. (e.g. https://www.youtube.com/watch?v=Y7QQS5V3cnI) and "track 1." (e.g. https://www.youtube.com/watch?v=FijBkSvN6N8)

	return maybeTrack;
}

function cleanseArtist(artist) {
	if(typeof artist === 'undefined' | artist === null) { return; }

	artist = artist.replace(/^\s+|\s+$/g, '');
	artist = artist.replace(/\s+/g, ' ');

	return artist;
}

function cleanseTrack(track) {
	if(typeof track === 'undefined' | track === null) { return; }

	track = track.replace(/^\s+|\s+$/g, '');
	track = track.replace(/\s+/g, ' ');

	// Strip crap
	track = track.replace(/\s*\*+\s?\S+\s?\*+$/, ''); // **NEW**
	track = track.replace(/\s*\[[^\]]+\]$/, ''); // [whatever]
	track = track.replace(/\s*\([^\)]*version\)$/i, ''); // (whatever version)
	track = track.replace(/\s*\.(avi|wmv|mpg|mpeg|flv)$/i, ''); // video extensions
	track = track.replace(/\s*(LYRIC VIDEO\s*)?(lyric video\s*)/i, ''); // (LYRIC VIDEO)
	track = track.replace(/\s*(Official Track Stream*)/i, ''); // (Official Track Stream)
	track = track.replace(/\s*(of+icial\s*)?(music\s*)?video/i, ''); // (official)? (music)? video
	track = track.replace(/\s*(of+icial\s*)?(music\s*)?audio/i, ''); // (official)? (music)? audio
	track = track.replace(/\s*(ALBUM TRACK\s*)?(album track\s*)/i, ''); // (ALBUM TRACK)
	track = track.replace(/\s*(FULL ALBUM\s*)?(full album\s*)/i, ''); // (FULL ALBUM)
	track = track.replace(/\s*(COVER ART\s*)?(Cover Art\s*)/i, ''); // (Cover Art)
	track = track.replace(/$\s*(REMASTERED\s*)?(remastered\s*)/i, ''); // (REMASTERED)
	track = track.replace(/\s*\(\s*of+icial\s*\)/i, ''); // (official)
	track = track.replace(/\s*\(\s*[0-9]{4}\s*\)/i, ''); // (1999)
	track = track.replace(/\s+\(\s*(HD|HQ)\s*\)$/, ''); // HD (HQ)
	track = track.replace(/\s+(HD|HQ)\s*$/, ''); // HD (HQ)
	track = track.replace(/\s*video\s*clip/i, ''); // video clip
	track = track.replace(/\s+\(?live\)?$/i, ''); // live
	track = track.replace(/\(+\s*\)+/, ''); // Leftovers after e.g. (official video)
	track = track.replace(/^(|.*\s)"(.*)"(\s.*|)$/, '$2'); // Artist - The new "Track title" featuring someone
	track = track.replace(/^(|.*\s)'(.*)'(\s.*|)$/, '$2'); // 'Track title'
	track = track.replace(/^[\/\s,:;~-\s"]+/, ''); // trim starting white chars and dash
	track = track.replace(/[\/\s,:;~-\s"\s!]+$/, ''); // trim trailing white chars and dash
	//" and ! added because some track names end as {"Some Track" Official Music Video!} and it becomes {"Some Track"!} example: http://www.youtube.com/watch?v=xj_mHi7zeRQ

	return track;
}
<|endoftext|>


<commit_before>aleph.directive('searchEntitySuggest', ['$location', '$q', '$route', '$http', '$rootScope', 'Authz', 'Entity',
    function($location, $q, $route, $http, $rootScope, Authz, Entity) {
  return {
    restrict: 'E',
    scope: {
      query: '='
    },
    templateUrl: 'templates/search_entity_suggest.html',
    link: function (scope, element, attrs) {

      scope.createQueryEntity = function(schema) {
        var name = scope.queryText;
        name = name.replace(/[\"\'\(\)\[\]\+]*/, ' ');
        name = titleCaps(name);
        Entity.create({$schema: schema, name: name}).then(function() {
          $route.reload();
        });
      };

      scope.setEntity = function(entity) {
        scope.query.set('q', '');
        scope.query.toggle('entity', entity.id);
      };

      var shouldSuggest = function(text) {
        text = text.trim();
        if (text && text.length > 3 && text.indexOf(' ') != -1) {
          return true;
        }
        return false;
      };

      scope.$watch('query', function updateSearchEntitySuggest(q) {
        scope.entity = null;
        scope.offerCreate = false;
        scope.offerSignin = false;
        scope.offerEntity = false;
        scope.queryText = q ? q.getQ() : '';

        if (shouldSuggest(scope.queryText)) {
          var params = {
              params: {'prefix': scope.queryText},
              ignoreLoadingBar: true,
              cache: true
          };
          $http.get('/api/1/entities/_suggest', params).then(function(res) {
            if (res.data.results && res.data.results.length && res.data.results[0].match) {
              scope.entity = res.data.results[0];
              scope.offerEntity = true;
            } else {
              scope.offerSignin = !Authz.logged_in();
              scope.offerCreate = Authz.logged_in();
            }
          });
        }
      });

    }
  };
}]);
<commit_msg>catch empty text<commit_after>aleph.directive('searchEntitySuggest', ['$location', '$q', '$route', '$http', '$rootScope', 'Authz', 'Entity',
    function($location, $q, $route, $http, $rootScope, Authz, Entity) {
  return {
    restrict: 'E',
    scope: {
      query: '='
    },
    templateUrl: 'templates/search_entity_suggest.html',
    link: function (scope, element, attrs) {

      scope.createQueryEntity = function(schema) {
        var name = scope.queryText;
        name = name.replace(/[\"\'\(\)\[\]\+]*/, ' ');
        name = titleCaps(name);
        Entity.create({$schema: schema, name: name}).then(function() {
          $route.reload();
        });
      };

      scope.setEntity = function(entity) {
        scope.query.set('q', '');
        scope.query.toggle('entity', entity.id);
      };

      var shouldSuggest = function(text) {
        if (!text) return false;
        text = text.trim();
        if (text && text.length > 3 && text.indexOf(' ') != -1) {
          return true;
        }
        return false;
      };

      scope.$watch('query', function updateSearchEntitySuggest(q) {
        scope.entity = null;
        scope.offerCreate = false;
        scope.offerSignin = false;
        scope.offerEntity = false;
        scope.queryText = q ? q.getQ() : '';

        if (shouldSuggest(scope.queryText)) {
          var params = {
              params: {'prefix': scope.queryText},
              ignoreLoadingBar: true,
              cache: true
          };
          $http.get('/api/1/entities/_suggest', params).then(function(res) {
            if (res.data.results && res.data.results.length && res.data.results[0].match) {
              scope.entity = res.data.results[0];
              scope.offerEntity = true;
            } else {
              scope.offerSignin = !Authz.logged_in();
              scope.offerCreate = Authz.logged_in();
            }
          });
        }
      });

    }
  };
}]);
<|endoftext|>


<commit_before>/**
 * jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!)
 * Common usage: wipe images (left and right to show the previous or next image)
 *
 * @author Andreas Waltl, netCU Internetagentur (http://www.netcu.de)
 * @version 1.1.1 (9th December 2010) - fix bug (older IE's had problems)
 * @version 1.1 (1st September 2010) - support wipe up and wipe down
 * @version 1.0 (15th July 2010)
 */
(function($) {
  $.fn.touchwipe = function(settings) {
    var config = {
      threshold: 20,
      wipeLeft: function() { },
      wipeRight: function() { },
      wipeUp: function() { },
      wipeDown: function() { },
      preventDefaultEvents: true
    };

    if (settings) $.extend(config, settings);

    this.each(function() {
      var startX;
      var startY;
      var isMoving = false;

      function cancelTouch() {
        this.removeEventListener('touchmove', onTouchMove);
        startX = null;
        isMoving = false;
      }

      function onTouchMove(e) {
        if(config.preventDefaultEvents) {
          e.preventDefault();
        }
        if(isMoving) {
          var x = e.touches[0].pageX;
          var y = e.touches[0].pageY;
          var dx = startX - x;
          var dy = startY - y;
          if(Math.abs(dx) >= config.threshold) {
            cancelTouch();
            if(dx > 0) {
              config.wipeLeft();
            }
            else {
              config.wipeRight();
            }
          }
          else if(Math.abs(dy) >= config.threshold) {
            cancelTouch();
            if(dy > 0) {
              config.wipeDown();
            }
            else {
              config.wipeUp();
            }
          }
        }
      }

      function onTouchStart(e) {
        if (e.touches.length == 1) {
          startX = e.touches[0].pageX;
          startY = e.touches[0].pageY;
          isMoving = true;
          this.addEventListener('touchmove', onTouchMove, false);
        }
      }
      if ('ontouchstart' in document.documentElement) {
        this.addEventListener('touchstart', onTouchStart, false);
      }
    });

    return this;
  };

})(jQuery);
<commit_msg>Remove useless code<commit_after>/**
 * jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!)
 * Common usage: wipe images (left and right to show the previous or next image)
 *
 * @author Andreas Waltl, netCU Internetagentur (http://www.netcu.de)
 * @version 1.1.1 (9th December 2010) - fix bug (older IE's had problems)
 * @version 1.1 (1st September 2010) - support wipe up and wipe down
 * @version 1.0 (15th July 2010)
 */
(function($) {
  $.fn.touchwipe = function(settings) {
    var config = {
      threshold: 20,
      wipeLeft: function() { },
      wipeRight: function() { },
      wipeUp: function() { },
      wipeDown: function() { },
      preventDefaultEvents: true
    };

    if (settings) $.extend(config, settings);

    this.each(function() {
      var startX;
      var startY;
      var isMoving = false;

      function cancelTouch() {
        this.removeEventListener('touchmove', onTouchMove);
        isMoving = false;
      }

      function onTouchMove(e) {
        if(config.preventDefaultEvents) {
          e.preventDefault();
        }
        if(isMoving) {
          var x = e.touches[0].pageX;
          var y = e.touches[0].pageY;
          var dx = startX - x;
          var dy = startY - y;
          if(Math.abs(dx) >= config.threshold) {
            cancelTouch();
            if(dx > 0) {
              config.wipeLeft();
            }
            else {
              config.wipeRight();
            }
          }
          else if(Math.abs(dy) >= config.threshold) {
            cancelTouch();
            if(dy > 0) {
              config.wipeDown();
            }
            else {
              config.wipeUp();
            }
          }
        }
      }

      function onTouchStart(e) {
        if (e.touches.length == 1) {
          startX = e.touches[0].pageX;
          startY = e.touches[0].pageY;
          isMoving = true;
          this.addEventListener('touchmove', onTouchMove, false);
        }
      }
      if ('ontouchstart' in document.documentElement) {
        this.addEventListener('touchstart', onTouchStart, false);
      }
    });

    return this;
  };

})(jQuery);
<|endoftext|>