<commit_before>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var HTML5AudioSound = new Class({
    Extends: BaseSound,
    /**
     * HTML5 Audio implementation of the sound.
     *
     * @class Phaser.Sound.HTML5AudioSound
     * @constructor
     * @param {Phaser.Sound.HTML5AudioSoundManager} manager - Reference to the current sound manager instance.
     * @param {string} key - Asset key for the sound.
     * @param {ISoundConfig} [config={}] - An optional config object containing default sound settings.
     */
    initialize: function HTML5AudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * An array containing all HTML5 Audio tags that could be used for individual
         * sound's playback. Number of instances depends on the config value passed
         * to the Loader#audio method call, default is 1.
         *
         * @private
         * @property {HTMLAudioElement[]} tags
         */
        this.tags = manager.game.cache.audio.get(key);
        if (!this.tags) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * Reference to an HTML5 Audio tag used for playing sound.
         *
         * @private
         * @property {HTMLAudioElement} audio
         * @default null
         */
        this.audio = null;
        /**
         * Timestamp as generated by the Request Animation Frame or SetTimeout
         * representing the time at which the delayed sound playback should start.
         * Set to 0 if sound playback is not delayed.
         *
         * @private
         * @property {number} startTime
         * @default 0
         */
        this.startTime = 0;
        /**
         * Audio tag's playback position recorded on previous
         * update method call. Set to 0 if sound is not playing.
         *
         * @private
         * @property {number} previousTime
         * @default 0
         */
        this.previousTime = 0;
        this.duration = this.tags[0].duration;
        this.totalDuration = this.tags[0].duration;
        BaseSound.call(this, manager, key, config);
    },
    /**
     * Play this sound, or a marked section of it.
     * It always plays the sound from the start. If you want to start playback from a specific time
     * you can set 'seek' setting of the config object, provided to this call, to that value.
     *
     * @method Phaser.Sound.HTML5AudioSound#play
     * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound.
     * @param {ISoundConfig} [config] - Optional sound config object to be applied to this marker or entire sound if no marker name is provided. It gets memorized for future plays of current section of the sound.
     * @returns {boolean} Whether the sound started playing successfully.
     */
    play: function (markerName, config) {
        if (this.manager.isLocked(this, 'play', [markerName, config])) {
            return false;
        }
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag()) {
            return false;
        }
        this.emit('play', this);
        return true;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.manager.isLocked(this, 'pause')) {
            return false;
        }
        if (this.startTime > 0) {
            return false;
        }
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.audio.currentTime
            - (this.currentMarker ? this.currentMarker.start : 0);
        this.stopAndReleaseAudioTag();
        this.emit('pause', this);
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (this.manager.isLocked(this, 'resume')) {
            return false;
        }
        if (this.startTime > 0) {
            return false;
        }
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag()) {
            return false;
        }
        this.emit('resume', this);
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (this.manager.isLocked(this, 'stop')) {
            return false;
        }
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndReleaseAudioTag();
        this.emit('stop', this);
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#pickAndPlayAudioTag
     * @returns {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAndPlayAudioTag: function () {
        if (!this.pickAudioTag()) {
            this.reset();
            return false;
        }
        var seek = this.currentConfig.seek;
        var delay = this.currentConfig.delay;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        this.previousTime = offset;
        this.audio.currentTime = offset;
        this.applyConfig();
        if (delay === 0) {
            this.startTime = 0;
            if (this.audio.paused) {
                this.playCatchPromise();
            }
        }
        else {
            this.startTime = window.performance.now() + delay * 1000;
            if (!this.audio.paused) {
                this.audio.pause();
            }
        }
        this.resetConfig();
        return true;
    },
    /**
     * This method performs the audio tag pooling logic. It first looks for
     * unused audio tag to assign to this sound object. If there are no unused
     * audio tags, based on HTML5AudioSoundManager#override property value, it
     * looks for sound with most advanced playback and hijacks its audio tag or
     * does nothing.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#pickAudioTag
     * @returns {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAudioTag: function () {
        if (this.audio) {
            return true;
        }
        for (var i = 0; i < this.tags.length; i++) {
            var audio = this.tags[i];
            if (audio.dataset.used === 'false') {
                audio.dataset.used = 'true';
                this.audio = audio;
                return true;
            }
        }
        if (!this.manager.override) {
            return false;
        }
        var otherSounds = [];
        this.manager.forEachActiveSound(function (sound) {
            if (sound.key === this.key && sound.audio) {
                otherSounds.push(sound);
            }
        }, this);
        otherSounds.sort(function (a1, a2) {
            if (a1.loop === a2.loop) {
                // sort by progress
                return (a2.seek / a2.duration) - (a1.seek / a1.duration);
            }
            return a1.loop ? 1 : -1;
        });
        var selectedSound = otherSounds[0];
        this.audio = selectedSound.audio;
        selectedSound.reset();
        selectedSound.audio = null;
        selectedSound.startTime = 0;
        selectedSound.previousTime = 0;
        return true;
    },
    /**
     * Method used for playing audio tag and catching possible exceptions
     * thrown from rejected Promise returned from play method call.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#playCatchPromise
     */
    playCatchPromise: function () {
        var playPromise = this.audio.play();
        if (playPromise) {
            playPromise.catch(function (reason) { });
        }
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#stopAndReleaseAudioTag
     */
    stopAndReleaseAudioTag: function () {
        this.audio.pause();
        this.audio.dataset.used = 'false';
        this.audio = null;
        this.startTime = 0;
        this.previousTime = 0;
    },
    /**
     * Method used internally to reset sound state, usually when stopping sound
     * or when hijacking audio tag from another sound.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#reset
     */
    reset: function () {
        BaseSound.prototype.stop.call(this);
    },
    /**
     * Method used internally by sound manager for pausing sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSoundManager#onBlur
     */
    onBlur: function () {
        this.isPlaying = false;
        this.isPaused = true;
        this.currentConfig.seek = this.audio.currentTime -
            (this.currentMarker ? this.currentMarker.start : 0);
        this.currentConfig.delay = Math.max(0, (this.startTime - window.performance.now()) / 1000);
        this.stopAndReleaseAudioTag();
    },
    /**
     * Method used internally by sound manager for resuming sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#onFocus
     */
    onFocus: function () {
        this.isPlaying = true;
        this.isPaused = false;
        this.pickAndPlayAudioTag();
    },
    update: function (time, delta) {
        if (!this.isPlaying) {
            return;
        }
        // handling delayed playback
        if (this.startTime > 0) {
            if (this.startTime < time - this.manager.audioPlayDelay) {
                this.audio.currentTime += Math.max(0, time - this.startTime) / 1000;
                this.startTime = 0;
                this.previousTime = this.audio.currentTime;
                this.playCatchPromise();
            }
            return;
        }
        // handle looping and ending
        var startTime = this.currentMarker ? this.currentMarker.start : 0;
        var endTime = startTime + this.duration;
        var currentTime = this.audio.currentTime;
        if (this.currentConfig.loop) {
            if (currentTime >= endTime - this.manager.loopEndOffset) {
                this.audio.currentTime = startTime + Math.max(0, currentTime - endTime);
                currentTime = this.audio.currentTime;
            }
            else if (currentTime < startTime) {
                this.audio.currentTime += startTime;
                currentTime = this.audio.currentTime;
            }
            if (currentTime < this.previousTime) {
                this.emit('looped', this);
            }
        }
        else if (currentTime >= endTime) {
            this.reset();
            this.stopAndReleaseAudioTag();
            this.emit('ended', this);
            return;
        }
        this.previousTime = currentTime;
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.tags = null;
        if (this.audio) {
            this.stopAndReleaseAudioTag();
        }
    },
    setMute: function () {
        if (this.audio) {
            this.audio.muted = this.currentConfig.mute || this.manager.mute;
        }
    },
    setVolume: function () {
        if (this.audio) {
            this.audio.volume = this.currentConfig.volume * this.manager.volume;
        }
    },
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.audio) {
            this.audio.playbackRate = this.totalRate;
        }
    }
});
/**
 * Mute setting.
 *
 * @name Phaser.Sound.HTML5AudioSound#mute
 * @property {boolean} mute
 */
Object.defineProperty(HTML5AudioSound.prototype, 'mute', {
    get: function () {
        return this.currentConfig.mute;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        if (this.manager.isLocked(this, 'mute', value)) {
            return;
        }
        this.setMute();
        this.emit('mute', this, value);
    }
});
/**
 * Volume setting.
 *
 * @name Phaser.Sound.HTML5AudioSound#volume
 * @property {number} volume
 */
Object.defineProperty(HTML5AudioSound.prototype, 'volume', {
    get: function () {
        return this.currentConfig.volume;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        if (this.manager.isLocked(this, 'volume', value)) {
            return;
        }
        this.setVolume();
        this.emit('volume', this, value);
    }
});
/**
 * Playback rate.
 *
 * @name Phaser.Sound.HTML5AudioSound#rate
 * @property {number} rate
 */
Object.defineProperty(HTML5AudioSound.prototype, 'rate', {
    get: function () {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').get.call(this);
    },
    set: function (value) {
        this.currentConfig.rate = value;
        if (this.manager.isLocked(this, 'rate', value)) {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').set.call(this, value);
    }
});
/**
 * Detuning of sound.
 *
 * @name Phaser.Sound.HTML5AudioSound#detune
 * @property {number} detune
 */
Object.defineProperty(HTML5AudioSound.prototype, 'detune', {
    get: function () {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').get.call(this);
    },
    set: function (value) {
        this.currentConfig.detune = value;
        if (this.manager.isLocked(this, 'detune', value)) {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').set.call(this, value);
    }
});
/**
 * Current position of playing sound.
 *
 * @name Phaser.Sound.HTML5AudioSound#seek
 * @property {number} seek
 */
Object.defineProperty(HTML5AudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.audio.currentTime -
                (this.currentMarker ? this.currentMarker.start : 0);
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.manager.isLocked(this, 'seek', value)) {
            return;
        }
        if (this.startTime > 0) {
            return;
        }
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            if (this.isPlaying) {
                this.previousTime = value;
                this.audio.currentTime = value;
            }
            else if (this.isPaused) {
                this.currentConfig.seek = value;
            }
            this.emit('seek', this, value);
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @name Phaser.Sound.HTML5AudioSound#loop
 * @property {boolean} loop
 */
Object.defineProperty(HTML5AudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        this.currentConfig.loop = value;
        if (this.manager.isLocked(this, 'loop', value)) {
            return;
        }
        if (this.audio) {
            this.audio.loop = value;
        }
        this.emit('loop', this, value);
    }
});
module.exports = HTML5AudioSound;
<commit_msg>Added update method docs<commit_after>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var HTML5AudioSound = new Class({
    Extends: BaseSound,
    /**
     * HTML5 Audio implementation of the sound.
     *
     * @class Phaser.Sound.HTML5AudioSound
     * @constructor
     * @param {Phaser.Sound.HTML5AudioSoundManager} manager - Reference to the current sound manager instance.
     * @param {string} key - Asset key for the sound.
     * @param {ISoundConfig} [config={}] - An optional config object containing default sound settings.
     */
    initialize: function HTML5AudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * An array containing all HTML5 Audio tags that could be used for individual
         * sound's playback. Number of instances depends on the config value passed
         * to the Loader#audio method call, default is 1.
         *
         * @private
         * @property {HTMLAudioElement[]} tags
         */
        this.tags = manager.game.cache.audio.get(key);
        if (!this.tags) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * Reference to an HTML5 Audio tag used for playing sound.
         *
         * @private
         * @property {HTMLAudioElement} audio
         * @default null
         */
        this.audio = null;
        /**
         * Timestamp as generated by the Request Animation Frame or SetTimeout
         * representing the time at which the delayed sound playback should start.
         * Set to 0 if sound playback is not delayed.
         *
         * @private
         * @property {number} startTime
         * @default 0
         */
        this.startTime = 0;
        /**
         * Audio tag's playback position recorded on previous
         * update method call. Set to 0 if sound is not playing.
         *
         * @private
         * @property {number} previousTime
         * @default 0
         */
        this.previousTime = 0;
        this.duration = this.tags[0].duration;
        this.totalDuration = this.tags[0].duration;
        BaseSound.call(this, manager, key, config);
    },
    /**
     * Play this sound, or a marked section of it.
     * It always plays the sound from the start. If you want to start playback from a specific time
     * you can set 'seek' setting of the config object, provided to this call, to that value.
     *
     * @method Phaser.Sound.HTML5AudioSound#play
     * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound.
     * @param {ISoundConfig} [config] - Optional sound config object to be applied to this marker or entire sound if no marker name is provided. It gets memorized for future plays of current section of the sound.
     * @returns {boolean} Whether the sound started playing successfully.
     */
    play: function (markerName, config) {
        if (this.manager.isLocked(this, 'play', [markerName, config])) {
            return false;
        }
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag()) {
            return false;
        }
        this.emit('play', this);
        return true;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.manager.isLocked(this, 'pause')) {
            return false;
        }
        if (this.startTime > 0) {
            return false;
        }
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.audio.currentTime
            - (this.currentMarker ? this.currentMarker.start : 0);
        this.stopAndReleaseAudioTag();
        this.emit('pause', this);
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (this.manager.isLocked(this, 'resume')) {
            return false;
        }
        if (this.startTime > 0) {
            return false;
        }
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        if (!this.pickAndPlayAudioTag()) {
            return false;
        }
        this.emit('resume', this);
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.HTML5AudioSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (this.manager.isLocked(this, 'stop')) {
            return false;
        }
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndReleaseAudioTag();
        this.emit('stop', this);
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#pickAndPlayAudioTag
     * @returns {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAndPlayAudioTag: function () {
        if (!this.pickAudioTag()) {
            this.reset();
            return false;
        }
        var seek = this.currentConfig.seek;
        var delay = this.currentConfig.delay;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        this.previousTime = offset;
        this.audio.currentTime = offset;
        this.applyConfig();
        if (delay === 0) {
            this.startTime = 0;
            if (this.audio.paused) {
                this.playCatchPromise();
            }
        }
        else {
            this.startTime = window.performance.now() + delay * 1000;
            if (!this.audio.paused) {
                this.audio.pause();
            }
        }
        this.resetConfig();
        return true;
    },
    /**
     * This method performs the audio tag pooling logic. It first looks for
     * unused audio tag to assign to this sound object. If there are no unused
     * audio tags, based on HTML5AudioSoundManager#override property value, it
     * looks for sound with most advanced playback and hijacks its audio tag or
     * does nothing.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#pickAudioTag
     * @returns {boolean} Whether the sound was assigned an audio tag successfully.
     */
    pickAudioTag: function () {
        if (this.audio) {
            return true;
        }
        for (var i = 0; i < this.tags.length; i++) {
            var audio = this.tags[i];
            if (audio.dataset.used === 'false') {
                audio.dataset.used = 'true';
                this.audio = audio;
                return true;
            }
        }
        if (!this.manager.override) {
            return false;
        }
        var otherSounds = [];
        this.manager.forEachActiveSound(function (sound) {
            if (sound.key === this.key && sound.audio) {
                otherSounds.push(sound);
            }
        }, this);
        otherSounds.sort(function (a1, a2) {
            if (a1.loop === a2.loop) {
                // sort by progress
                return (a2.seek / a2.duration) - (a1.seek / a1.duration);
            }
            return a1.loop ? 1 : -1;
        });
        var selectedSound = otherSounds[0];
        this.audio = selectedSound.audio;
        selectedSound.reset();
        selectedSound.audio = null;
        selectedSound.startTime = 0;
        selectedSound.previousTime = 0;
        return true;
    },
    /**
     * Method used for playing audio tag and catching possible exceptions
     * thrown from rejected Promise returned from play method call.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#playCatchPromise
     */
    playCatchPromise: function () {
        var playPromise = this.audio.play();
        if (playPromise) {
            playPromise.catch(function (reason) { });
        }
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#stopAndReleaseAudioTag
     */
    stopAndReleaseAudioTag: function () {
        this.audio.pause();
        this.audio.dataset.used = 'false';
        this.audio = null;
        this.startTime = 0;
        this.previousTime = 0;
    },
    /**
     * Method used internally to reset sound state, usually when stopping sound
     * or when hijacking audio tag from another sound.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#reset
     */
    reset: function () {
        BaseSound.prototype.stop.call(this);
    },
    /**
     * Method used internally by sound manager for pausing sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSoundManager#onBlur
     */
    onBlur: function () {
        this.isPlaying = false;
        this.isPaused = true;
        this.currentConfig.seek = this.audio.currentTime -
            (this.currentMarker ? this.currentMarker.start : 0);
        this.currentConfig.delay = Math.max(0, (this.startTime - window.performance.now()) / 1000);
        this.stopAndReleaseAudioTag();
    },
    /**
     * Method used internally by sound manager for resuming sound if
     * Phaser.Sound.HTML5AudioSoundManager#pauseOnBlur is set to true.
     *
     * @private
     * @method Phaser.Sound.HTML5AudioSound#onFocus
     */
    onFocus: function () {
        this.isPlaying = true;
        this.isPaused = false;
        this.pickAndPlayAudioTag();
    },
    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @protected
     * @method Phaser.Sound.HTML5AudioSound#update
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (!this.isPlaying) {
            return;
        }
        // handling delayed playback
        if (this.startTime > 0) {
            if (this.startTime < time - this.manager.audioPlayDelay) {
                this.audio.currentTime += Math.max(0, time - this.startTime) / 1000;
                this.startTime = 0;
                this.previousTime = this.audio.currentTime;
                this.playCatchPromise();
            }
            return;
        }
        // handle looping and ending
        var startTime = this.currentMarker ? this.currentMarker.start : 0;
        var endTime = startTime + this.duration;
        var currentTime = this.audio.currentTime;
        if (this.currentConfig.loop) {
            if (currentTime >= endTime - this.manager.loopEndOffset) {
                this.audio.currentTime = startTime + Math.max(0, currentTime - endTime);
                currentTime = this.audio.currentTime;
            }
            else if (currentTime < startTime) {
                this.audio.currentTime += startTime;
                currentTime = this.audio.currentTime;
            }
            if (currentTime < this.previousTime) {
                this.emit('looped', this);
            }
        }
        else if (currentTime >= endTime) {
            this.reset();
            this.stopAndReleaseAudioTag();
            this.emit('ended', this);
            return;
        }
        this.previousTime = currentTime;
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.tags = null;
        if (this.audio) {
            this.stopAndReleaseAudioTag();
        }
    },
    setMute: function () {
        if (this.audio) {
            this.audio.muted = this.currentConfig.mute || this.manager.mute;
        }
    },
    setVolume: function () {
        if (this.audio) {
            this.audio.volume = this.currentConfig.volume * this.manager.volume;
        }
    },
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.audio) {
            this.audio.playbackRate = this.totalRate;
        }
    }
});
/**
 * Mute setting.
 *
 * @name Phaser.Sound.HTML5AudioSound#mute
 * @property {boolean} mute
 */
Object.defineProperty(HTML5AudioSound.prototype, 'mute', {
    get: function () {
        return this.currentConfig.mute;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        if (this.manager.isLocked(this, 'mute', value)) {
            return;
        }
        this.setMute();
        this.emit('mute', this, value);
    }
});
/**
 * Volume setting.
 *
 * @name Phaser.Sound.HTML5AudioSound#volume
 * @property {number} volume
 */
Object.defineProperty(HTML5AudioSound.prototype, 'volume', {
    get: function () {
        return this.currentConfig.volume;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        if (this.manager.isLocked(this, 'volume', value)) {
            return;
        }
        this.setVolume();
        this.emit('volume', this, value);
    }
});
/**
 * Playback rate.
 *
 * @name Phaser.Sound.HTML5AudioSound#rate
 * @property {number} rate
 */
Object.defineProperty(HTML5AudioSound.prototype, 'rate', {
    get: function () {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').get.call(this);
    },
    set: function (value) {
        this.currentConfig.rate = value;
        if (this.manager.isLocked(this, 'rate', value)) {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'rate').set.call(this, value);
    }
});
/**
 * Detuning of sound.
 *
 * @name Phaser.Sound.HTML5AudioSound#detune
 * @property {number} detune
 */
Object.defineProperty(HTML5AudioSound.prototype, 'detune', {
    get: function () {
        return Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').get.call(this);
    },
    set: function (value) {
        this.currentConfig.detune = value;
        if (this.manager.isLocked(this, 'detune', value)) {
            return;
        }
        Object.getOwnPropertyDescriptor(BaseSound.prototype, 'detune').set.call(this, value);
    }
});
/**
 * Current position of playing sound.
 *
 * @name Phaser.Sound.HTML5AudioSound#seek
 * @property {number} seek
 */
Object.defineProperty(HTML5AudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.audio.currentTime -
                (this.currentMarker ? this.currentMarker.start : 0);
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.manager.isLocked(this, 'seek', value)) {
            return;
        }
        if (this.startTime > 0) {
            return;
        }
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            if (this.isPlaying) {
                this.previousTime = value;
                this.audio.currentTime = value;
            }
            else if (this.isPaused) {
                this.currentConfig.seek = value;
            }
            this.emit('seek', this, value);
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @name Phaser.Sound.HTML5AudioSound#loop
 * @property {boolean} loop
 */
Object.defineProperty(HTML5AudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        this.currentConfig.loop = value;
        if (this.manager.isLocked(this, 'loop', value)) {
            return;
        }
        if (this.audio) {
            this.audio.loop = value;
        }
        this.emit('loop', this, value);
    }
});
module.exports = HTML5AudioSound;
<|endoftext|>


<commit_before>import request  from 'request-promise-native';
import config   from '../../config';
import distance from '../../helper/distance';
import inbox    from '../../helper/inbox';
var cron = require('node-cron');

var stopPoints = [];

const getRaw = () => {
    return request(config('MOBILITEIT_STOPPOINTS', true));
};

cron.schedule(config('MOBILITEIT_REFRESH_CRON', true), function(){
    loadStoppoints();
});

const loadStoppoints = async () => {
    console.log('loading mobiliteit');
    stopPoints = await load();
};

export const load = async () => {

    var raw = await getRaw();
    var stations = raw.trim().split('\n');
    var newStopPoints = [];

    for (var i = 0; i < stations.length; i++) {
        var paramParts = stations[i].split('@');
        var params = {};
        for (var j = 0; j < paramParts.length; j++) {
            var keyVal = paramParts[j].split('=', 2);
            params[keyVal[0]] = keyVal[1];
        }
        newStopPoints.push({
            id: parseInt(params.L, 10),
            name: params.O,
            longitude: parseFloat(params.X.replace(',', '.')),
            latitude: parseFloat(params.Y.replace(',', '.'))
        });
    }

    return newStopPoints;

};

const cache = async () => {
    if (stopPoints.length === 0) {
        await loadStoppoints();
    }
};

export const all = async () => {
    await cache();
    return stopPoints;
};

export const get = async stopPoint => {
    await cache();
    for (var i = 0; i < stopPoints.length; i++) {
        if (stopPoints[i].id == stopPoint) {
            return stopPoints[i];
        }
    }
    return false;
};

export const around = async (lon, lat, radius) => {
    await cache();
    var dist = 0;
    var stopPointsAround = [];

    for (var i = 0; i < stopPoints.length; i++) {
        dist = distance(
            parseFloat(lon),
            parseFloat(lat),
            stopPoints[i].longitude,
            stopPoints[i].latitude
        );

        if (dist <= radius) {
            var temp = stopPoints[i];
            temp.distance = parseFloat(dist.toFixed(2));
            stopPointsAround.push(temp);
        }
    }
    return stopPointsAround;
};

export const box = async (swlon, swlat, nelon, nelat) => {
    await cache();
    var whybox = false;
    return stopPoints.filter(function(stopPoint) {
        whybox = inbox(
            swlon, swlat, nelon, nelat,
            stopPoint.longitude,
            stopPoint.latitude
        );
        return whybox;
    });
};
<commit_msg>removed console log<commit_after>import request  from 'request-promise-native';
import config   from '../../config';
import distance from '../../helper/distance';
import inbox    from '../../helper/inbox';
var cron = require('node-cron');

var stopPoints = [];

const getRaw = () => {
    return request(config('MOBILITEIT_STOPPOINTS', true));
};

cron.schedule(config('MOBILITEIT_REFRESH_CRON', true), function(){
    loadStoppoints();
});

const loadStoppoints = async () => {
    stopPoints = await load();
};

export const load = async () => {

    var raw = await getRaw();
    var stations = raw.trim().split('\n');
    var newStopPoints = [];

    for (var i = 0; i < stations.length; i++) {
        var paramParts = stations[i].split('@');
        var params = {};
        for (var j = 0; j < paramParts.length; j++) {
            var keyVal = paramParts[j].split('=', 2);
            params[keyVal[0]] = keyVal[1];
        }
        newStopPoints.push({
            id: parseInt(params.L, 10),
            name: params.O,
            longitude: parseFloat(params.X.replace(',', '.')),
            latitude: parseFloat(params.Y.replace(',', '.'))
        });
    }

    return newStopPoints;

};

const cache = async () => {
    if (stopPoints.length === 0) {
        await loadStoppoints();
    }
};

export const all = async () => {
    await cache();
    return stopPoints;
};

export const get = async stopPoint => {
    await cache();
    for (var i = 0; i < stopPoints.length; i++) {
        if (stopPoints[i].id == stopPoint) {
            return stopPoints[i];
        }
    }
    return false;
};

export const around = async (lon, lat, radius) => {
    await cache();
    var dist = 0;
    var stopPointsAround = [];

    for (var i = 0; i < stopPoints.length; i++) {
        dist = distance(
            parseFloat(lon),
            parseFloat(lat),
            stopPoints[i].longitude,
            stopPoints[i].latitude
        );

        if (dist <= radius) {
            var temp = stopPoints[i];
            temp.distance = parseFloat(dist.toFixed(2));
            stopPointsAround.push(temp);
        }
    }
    return stopPointsAround;
};

export const box = async (swlon, swlat, nelon, nelat) => {
    await cache();
    var whybox = false;
    return stopPoints.filter(function(stopPoint) {
        whybox = inbox(
            swlon, swlat, nelon, nelat,
            stopPoint.longitude,
            stopPoint.latitude
        );
        return whybox;
    });
};
<|endoftext|>


<commit_before>var crypto = require("crypto");
var SimpleThumbnailGenerator = require("./simple-thumbnail-generator");
var Logger = require("./logger");
var express = require('express');
var bodyParser = require('body-parser');
var nullLogger = require("./null-logger");
var utils = require("./utils");

/**
 * Start a web service which will allow thumbnail generation to be controlled by web requests.
 * Requests are
 * - POST /v1/start
 *   - start generating thumbnails
 *   - manifest will be at "thumbnails-[id].json"
 *   - fields
 *     - url
 *   - response
 *     - {id: <id used for further communication>}
 * - GET /v1/generators/{id}
 *     - {ended: <true or false>}
 * - DELETE /v1/generators/{id}
 *   - stop generating and remove thumbnails for stream
 * @constructor
 * @param {Object} options
 * @param {Number} [options.port] The port to listen on. Defaults to 8080.
 * @param {String} [options.secret] A secret which is required with requests. Defaults to null which disables this. If enabled secret should be procided in "x-secret" header for api requests.
 * @param {String} [options.pingInterval] If a ping request isn't made every 'pingInterval' seconds then thumbnail generation will stop. Defaults to disabled.
 * @param {Number} [options.logger] An object with `debug`, `info`, `warn` and `error` functions, or null, to disable logging.
 * @param {Object} [simpleThumbnailGeneratorOptions] Default configuraton for `ThumbnailGenerator`.
 * @param {Object} [thumbnailGeneratorOptions] Default configuraton for `SimpleThumbnailGenerator`. Note the temp directory will be automatically generated and managed if not provided.
 */
function ThumbnailGeneratorService(options, simpleThumbnailGeneratorOptions, thumbnailGeneratorOptions) {
	options = options || {};

	this._simpleThumbnailGeneratorOptions = simpleThumbnailGeneratorOptions || {};
	this._thumbnailGeneratorOptions = thumbnailGeneratorOptions || {};

	if (typeof(options.logger) === "undefined") {
		this._logger = Logger.get('ThumbnailGeneratorService');
	}
	else {
		if (options.logger === null) {
			this._logger = nullLogger;
		}
		else {
			this._logger = options.logger;
		}
	}

	this._port = options.port || 8080;
	this._secret = options.secret || null;
	this._pingInterval = options.pingInterval || null;
	this._outputDir = this._thumbnailGeneratorOptions.outputDir;
	this._tempDir = this._thumbnailGeneratorOptions.tempDir || null;
	this._destroyed = false;
	// generators by id
	this._generators = {};
	this._app = null;
	this._pingTimeoutIds = {};
	this._initTempDir().then(() => {
		if (this._destroyed) {
			return;
		}
		this._createServer();
		this._logger.debug("Loaded and listening on "+this._port+".");
	}).catch((err) => {
		throw err;
	});
}

/**
 * Destroy the server.
 */
ThumbnailGeneratorService.prototype.destroy = function() {
	if (this._destroyed) {
		return;
	}
	this._destroyed = true;
	Object.keys(this._generators).forEach((id) => {
		this._generators[id].destroy();
	});
	this._generators = {};
	this._app && this._app.close();
};

ThumbnailGeneratorService.prototype._initTempDir = function() {
	if (this._tempDir) {
		// temp dir provided. Don't touch, use as is.
		return Promise.resolve();
	}

	var tempDir = utils.getTempDir();
	this._logger.debug("Initializing temp directory.");

	return utils.ensureExists(tempDir).then(() => {
		return utils.emptyDir(tempDir);
	}).then(() => {
		this._tempDir = tempDir;
		this._logger.debug("Temp directory initialized.", tempDir);
	});
};

ThumbnailGeneratorService.prototype._createServer = function() {
	var app = express();
	app.use(bodyParser.urlencoded({ extended: false }));

	app.all('*', (req, res, next) => {
		if (!this._secret || (req.headers["x-secret"] && req.headers["x-secret"] === this._secret)) {
			next();
		}
		else {
			this._logger.debug("Request denied. Invalid secret.");
			res.status(403).send({ error: 'Invalid secret.' });
		}
	});

	app.post('/v1/start', (req, res) => {
		var url = req.body.url || null;
		if (!url) {
			throw new Error("URL required.");
		}
		var options = {
			playlistUrl: url
		};
		req.body.width && (options.width = parseInt(req.body.width));
		req.body.height && (options.height = parseInt(req.body.height));
		req.body.interval && (options.interval = parseInt(req.body.interval));
		req.body.initialThumbnailCount && (options.initialThumbnailCount = parseInt(req.body.initialThumbnailCount));
		req.body.targetThumbnailCount && (options.targetThumbnailCount = parseInt(req.body.targetThumbnailCount));

		var id = this._createGenerator(options);
		res.send({
			id: id
		});
	});

	app.get('/v1/generators/:id', (req, res) => {
		var id = req.params.id;
		var generator = this._generators[id];
		if (!generator) {
			res.status(404).send({ error: 'Generator does not exist.' });
			return;
		}
		this._schedulePingTimeout(id, generator);
		res.send({
			ended: generator.hasPlaylistEnded()
		});
	});

	app.delete('/v1/generators/:id', (req, res) => {
		var id = req.params.id;
		var generator = this._generators[id];
		if (!generator) {
			res.status(404).send({ error: 'Generator does not exist.' });
			return;
		}
		generator.destroy();
		delete this._generators[id];
		if (this._pingInterval) {
			clearTimeout(this._pingTimeoutIds[id]);
			delete this._pingTimeoutIds[id];
		}
		res.send("Deleted.");
	});

	app.listen(this._port);
	this._app = app;
};

ThumbnailGeneratorService.prototype._createGenerator = function(options) {
	var id = this._generateId();
	var thumbnailGeneratorOptions = Object.assign({}, this._thumbnailGeneratorOptions, options, {
		tempDir: this._tempDir,
		outputNamePrefix: id
	});
	var simpleThumbnailGeneratorOptions = Object.assign({}, this._simpleThumbnailGeneratorOptions, {
		manifestFileName: this._generateManifestFileName(id)
	});
	var generator = new SimpleThumbnailGenerator(simpleThumbnailGeneratorOptions, thumbnailGeneratorOptions);
	this._addListeners(id, generator);
	this._generators[id] = generator;
	this._schedulePingTimeout(id, generator);
	return id;
};

ThumbnailGeneratorService.prototype._schedulePingTimeout = function(id, generator) {
	if (!this._pingInterval) {
		// disabled
		return;
	}
	if (typeof(this._pingTimeoutIds[id]) !== "undefined") {
		// reset ping timeout
		clearTimeout(this._pingTimeoutIds[id]);
	}
	this._pingTimeoutIds[id] = setTimeout(() => {
		generator.destroy();
		delete this._generators[id];
		delete this._pingTimeoutIds[id];
		this._logger.debug("Generator destroyed because ping missed.", id);
	}, this._pingInterval * 1000);
};

ThumbnailGeneratorService.prototype._addListeners = function(id, generator) {
	var emitter = generator.getEmitter();

	emitter.on("error", (err) => {
		this._logger.error("Generator error.", id, err.stack);
		delete this._generators[id];
	});

	emitter.on("finished", () => {
		this._logger.debug("Generator finished.", id);
		delete this._generators[id];
	});
};

ThumbnailGeneratorService.prototype._generateManifestFileName = function(id) {
	return "thumbnails-"+id+".json";
};

ThumbnailGeneratorService.prototype._generateId = function() {
	var id = null;
	do {
		id = crypto.createHash("sha1").update(crypto.randomBytes(256).toString()).digest("hex");
	} while(this._generators[id]);
	return id;
};

module.exports = ThumbnailGeneratorService;
<commit_msg>Listen on all interfaces<commit_after>var crypto = require("crypto");
var SimpleThumbnailGenerator = require("./simple-thumbnail-generator");
var Logger = require("./logger");
var express = require('express');
var bodyParser = require('body-parser');
var nullLogger = require("./null-logger");
var utils = require("./utils");

/**
 * Start a web service which will allow thumbnail generation to be controlled by web requests.
 * Requests are
 * - POST /v1/start
 *   - start generating thumbnails
 *   - manifest will be at "thumbnails-[id].json"
 *   - fields
 *     - url
 *   - response
 *     - {id: <id used for further communication>}
 * - GET /v1/generators/{id}
 *     - {ended: <true or false>}
 * - DELETE /v1/generators/{id}
 *   - stop generating and remove thumbnails for stream
 * @constructor
 * @param {Object} options
 * @param {Number} [options.port] The port to listen on. Defaults to 8080.
 * @param {String} [options.secret] A secret which is required with requests. Defaults to null which disables this. If enabled secret should be procided in "x-secret" header for api requests.
 * @param {String} [options.pingInterval] If a ping request isn't made every 'pingInterval' seconds then thumbnail generation will stop. Defaults to disabled.
 * @param {Number} [options.logger] An object with `debug`, `info`, `warn` and `error` functions, or null, to disable logging.
 * @param {Object} [simpleThumbnailGeneratorOptions] Default configuraton for `ThumbnailGenerator`.
 * @param {Object} [thumbnailGeneratorOptions] Default configuraton for `SimpleThumbnailGenerator`. Note the temp directory will be automatically generated and managed if not provided.
 */
function ThumbnailGeneratorService(options, simpleThumbnailGeneratorOptions, thumbnailGeneratorOptions) {
	options = options || {};

	this._simpleThumbnailGeneratorOptions = simpleThumbnailGeneratorOptions || {};
	this._thumbnailGeneratorOptions = thumbnailGeneratorOptions || {};

	if (typeof(options.logger) === "undefined") {
		this._logger = Logger.get('ThumbnailGeneratorService');
	}
	else {
		if (options.logger === null) {
			this._logger = nullLogger;
		}
		else {
			this._logger = options.logger;
		}
	}

	this._port = options.port || 8080;
	this._secret = options.secret || null;
	this._pingInterval = options.pingInterval || null;
	this._outputDir = this._thumbnailGeneratorOptions.outputDir;
	this._tempDir = this._thumbnailGeneratorOptions.tempDir || null;
	this._destroyed = false;
	// generators by id
	this._generators = {};
	this._app = null;
	this._pingTimeoutIds = {};
	this._initTempDir().then(() => {
		if (this._destroyed) {
			return;
		}
		this._createServer();
		this._logger.debug("Loaded and listening on "+this._port+".");
	}).catch((err) => {
		throw err;
	});
}

/**
 * Destroy the server.
 */
ThumbnailGeneratorService.prototype.destroy = function() {
	if (this._destroyed) {
		return;
	}
	this._destroyed = true;
	Object.keys(this._generators).forEach((id) => {
		this._generators[id].destroy();
	});
	this._generators = {};
	this._app && this._app.close();
};

ThumbnailGeneratorService.prototype._initTempDir = function() {
	if (this._tempDir) {
		// temp dir provided. Don't touch, use as is.
		return Promise.resolve();
	}

	var tempDir = utils.getTempDir();
	this._logger.debug("Initializing temp directory.");

	return utils.ensureExists(tempDir).then(() => {
		return utils.emptyDir(tempDir);
	}).then(() => {
		this._tempDir = tempDir;
		this._logger.debug("Temp directory initialized.", tempDir);
	});
};

ThumbnailGeneratorService.prototype._createServer = function() {
	var app = express();
	app.use(bodyParser.urlencoded({ extended: false }));

	app.all('*', (req, res, next) => {
		if (!this._secret || (req.headers["x-secret"] && req.headers["x-secret"] === this._secret)) {
			next();
		}
		else {
			this._logger.debug("Request denied. Invalid secret.");
			res.status(403).send({ error: 'Invalid secret.' });
		}
	});

	app.post('/v1/start', (req, res) => {
		var url = req.body.url || null;
		if (!url) {
			throw new Error("URL required.");
		}
		var options = {
			playlistUrl: url
		};
		req.body.width && (options.width = parseInt(req.body.width));
		req.body.height && (options.height = parseInt(req.body.height));
		req.body.interval && (options.interval = parseInt(req.body.interval));
		req.body.initialThumbnailCount && (options.initialThumbnailCount = parseInt(req.body.initialThumbnailCount));
		req.body.targetThumbnailCount && (options.targetThumbnailCount = parseInt(req.body.targetThumbnailCount));

		var id = this._createGenerator(options);
		res.send({
			id: id
		});
	});

	app.get('/v1/generators/:id', (req, res) => {
		var id = req.params.id;
		var generator = this._generators[id];
		if (!generator) {
			res.status(404).send({ error: 'Generator does not exist.' });
			return;
		}
		this._schedulePingTimeout(id, generator);
		res.send({
			ended: generator.hasPlaylistEnded()
		});
	});

	app.delete('/v1/generators/:id', (req, res) => {
		var id = req.params.id;
		var generator = this._generators[id];
		if (!generator) {
			res.status(404).send({ error: 'Generator does not exist.' });
			return;
		}
		generator.destroy();
		delete this._generators[id];
		if (this._pingInterval) {
			clearTimeout(this._pingTimeoutIds[id]);
			delete this._pingTimeoutIds[id];
		}
		res.send("Deleted.");
	});

	app.listen(this._port, "0.0.0.0");
	this._app = app;
};

ThumbnailGeneratorService.prototype._createGenerator = function(options) {
	var id = this._generateId();
	var thumbnailGeneratorOptions = Object.assign({}, this._thumbnailGeneratorOptions, options, {
		tempDir: this._tempDir,
		outputNamePrefix: id
	});
	var simpleThumbnailGeneratorOptions = Object.assign({}, this._simpleThumbnailGeneratorOptions, {
		manifestFileName: this._generateManifestFileName(id)
	});
	var generator = new SimpleThumbnailGenerator(simpleThumbnailGeneratorOptions, thumbnailGeneratorOptions);
	this._addListeners(id, generator);
	this._generators[id] = generator;
	this._schedulePingTimeout(id, generator);
	return id;
};

ThumbnailGeneratorService.prototype._schedulePingTimeout = function(id, generator) {
	if (!this._pingInterval) {
		// disabled
		return;
	}
	if (typeof(this._pingTimeoutIds[id]) !== "undefined") {
		// reset ping timeout
		clearTimeout(this._pingTimeoutIds[id]);
	}
	this._pingTimeoutIds[id] = setTimeout(() => {
		generator.destroy();
		delete this._generators[id];
		delete this._pingTimeoutIds[id];
		this._logger.debug("Generator destroyed because ping missed.", id);
	}, this._pingInterval * 1000);
};

ThumbnailGeneratorService.prototype._addListeners = function(id, generator) {
	var emitter = generator.getEmitter();

	emitter.on("error", (err) => {
		this._logger.error("Generator error.", id, err.stack);
		delete this._generators[id];
	});

	emitter.on("finished", () => {
		this._logger.debug("Generator finished.", id);
		delete this._generators[id];
	});
};

ThumbnailGeneratorService.prototype._generateManifestFileName = function(id) {
	return "thumbnails-"+id+".json";
};

ThumbnailGeneratorService.prototype._generateId = function() {
	var id = null;
	do {
		id = crypto.createHash("sha1").update(crypto.randomBytes(256).toString()).digest("hex");
	} while(this._generators[id]);
	return id;
};

module.exports = ThumbnailGeneratorService;
<|endoftext|>


<commit_before>/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;

var orientations = ['v', 'h'];

function crossTraceCalc(gd, plotinfo) {
    var calcdata = gd.calcdata;
    var xa = plotinfo.xaxis;
    var ya = plotinfo.yaxis;

    for(var i = 0; i < orientations.length; i++) {
        var orientation = orientations[i];
        var posAxis = orientation === 'h' ? ya : xa;
        var boxList = [];

        // make list of boxes / candlesticks
        // For backward compatibility, candlesticks are treated as if they *are* box traces here
        for(var j = 0; j < calcdata.length; j++) {
            var cd = calcdata[j];
            var t = cd[0].t;
            var trace = cd[0].trace;

            if(trace.visible === true &&
                    (trace.type === 'box' || trace.type === 'candlestick') &&
                    !t.empty &&
                    (trace.orientation || 'v') === orientation &&
                    trace.xaxis === xa._id &&
                    trace.yaxis === ya._id
              ) {
                boxList.push(j);
            }
        }

        setPositionOffset('box', gd, boxList, posAxis);
    }
}

function setPositionOffset(traceType, gd, boxList, posAxis) {
    var calcdata = gd.calcdata;
    var fullLayout = gd._fullLayout;
    var axId = posAxis._id;
    var axLetter = axId.charAt(0);

    var i, j, calcTrace;
    var pointList = [];
    var shownPts = 0;

    // make list of box points
    for(i = 0; i < boxList.length; i++) {
        calcTrace = calcdata[boxList[i]];
        for(j = 0; j < calcTrace.length; j++) {
            pointList.push(posAxis.c2l(calcTrace[j].pos, true));
            shownPts += (calcTrace[j].pts2 || []).length;
        }
    }

    if(!pointList.length) return;

    // box plots - update dPos based on multiple traces
    var boxdv = Lib.distinctVals(pointList);
    var dPos0 = boxdv.minDiff / 2;

    // check for forced minimum dtick
    Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);

    var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
    var numTotal = fullLayout[numKey];
    var group = fullLayout[traceType + 'mode'] === 'group' && numTotal > 1;
    var groupFraction = 1 - fullLayout[traceType + 'gap'];
    var groupGapFraction = 1 - fullLayout[traceType + 'groupgap'];

    for(i = 0; i < boxList.length; i++) {
        calcTrace = calcdata[boxList[i]];

        var trace = calcTrace[0].trace;
        var t = calcTrace[0].t;
        var width = trace.width;
        var side = trace.side;

        // position coordinate delta
        var dPos;
        // box half width;
        var bdPos;
        // box center offset
        var bPos;
        // half-width within which to accept hover for this box/violin
        // always split the distance to the closest box/violin
        var wHover;

        if(width) {
            dPos = bdPos = wHover = width / 2;
            bPos = 0;
        } else {
            dPos = dPos0;

            if(group) {
                var groupId = getAxisGroup(fullLayout, posAxis._id) + trace.orientation;
                var alignmentGroups = fullLayout._alignmentOpts[groupId] || {};
                var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {};
                var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length;
                var num = nOffsetGroups || numTotal;
                var shift = nOffsetGroups ? trace._offsetIndex : t.num;

                bdPos = dPos * groupFraction * groupGapFraction / num;
                bPos = 2 * dPos * (-0.5 + (shift + 0.5) / num) * groupFraction;
                wHover = dPos * groupFraction / num;
            } else {
                bdPos = dPos * groupFraction * groupGapFraction;
                bPos = 0;
                wHover = dPos;
            }
        }
        t.dPos = dPos;
        t.bPos = bPos;
        t.bdPos = bdPos;
        t.wHover = wHover;

        // box/violin-only value-space push value
        var pushplus;
        var pushminus;
        // edge of box/violin
        var edge = bPos + bdPos;
        var edgeplus;
        var edgeminus;
        // value-space padding
        var vpadplus;
        var vpadminus;
        // pixel-space padding
        var ppadplus;
        var ppadminus;
        // do we add 5% of both sides (more logic for points beyond box/violin below)
        var padded = Boolean(width);
        // does this trace show points?
        var hasPts = (trace.boxpoints || trace.points) && (shownPts > 0);

        if(side === 'positive') {
            pushplus = dPos * (width ? 1 : 0.5);
            edgeplus = edge;
            pushminus = edgeplus = bPos;
        } else if(side === 'negative') {
            pushplus = edgeplus = bPos;
            pushminus = dPos * (width ? 1 : 0.5);
            edgeminus = edge;
        } else {
            pushplus = pushminus = dPos;
            edgeplus = edgeminus = edge;
        }

        if(hasPts) {
            var pointpos = trace.pointpos;
            var jitter = trace.jitter;
            var ms = trace.marker.size / 2;

            var pp = 0;
            if((pointpos + jitter) >= 0) {
                pp = edge * (pointpos + jitter);
                if(pp > pushplus) {
                    // (++) beyond plus-value, use pp
                    padded = true;
                    ppadplus = ms;
                    vpadplus = pp;
                } else if(pp > edgeplus) {
                    // (+), use push-value (it's bigger), but add px-pad
                    ppadplus = ms;
                    vpadplus = pushplus;
                }
            }
            if(pp <= pushplus) {
                // (->) fallback to push value
                vpadplus = pushplus;
            }

            var pm = 0;
            if((pointpos - jitter) <= 0) {
                pm = -edge * (pointpos - jitter);
                if(pm > pushminus) {
                    // (--) beyond plus-value, use pp
                    padded = true;
                    ppadminus = ms;
                    vpadminus = pm;
                } else if(pm > edgeminus) {
                    // (-), use push-value (it's bigger), but add px-pad
                    ppadminus = ms;
                    vpadminus = pushminus;
                }
            }
            if(pm <= pushminus) {
                // (<-) fallback to push value
                vpadminus = pushminus;
            }
        } else {
            vpadplus = pushplus;
            vpadminus = pushminus;
        }

        var pos = new Array(calcTrace.length);
        for(j = 0; j < calcTrace.length; j++) {
            pos[j] = calcTrace[j].pos;
        }

        trace._extremes[axId] = Axes.findExtremes(posAxis, pos, {
            padded: padded,
            vpadminus: vpadminus,
            vpadplus: vpadplus,
            vpadLinearized: true,
            // N.B. SVG px-space positive/negative
            ppadminus: {x: ppadminus, y: ppadplus}[axLetter],
            ppadplus: {x: ppadplus, y: ppadminus}[axLetter],
        });
    }
}

module.exports = {
    crossTraceCalc: crossTraceCalc,
    setPositionOffset: setPositionOffset
};
<commit_msg>fix issue 4510 for box plots<commit_after>/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
var getAxisGroup = require('../../plots/cartesian/axis_ids').getAxisGroup;

var orientations = ['v', 'h'];

function crossTraceCalc(gd, plotinfo) {
    var calcdata = gd.calcdata;
    var xa = plotinfo.xaxis;
    var ya = plotinfo.yaxis;

    for(var i = 0; i < orientations.length; i++) {
        var orientation = orientations[i];
        var posAxis = orientation === 'h' ? ya : xa;
        var boxList = [];

        // make list of boxes / candlesticks
        // For backward compatibility, candlesticks are treated as if they *are* box traces here
        for(var j = 0; j < calcdata.length; j++) {
            var cd = calcdata[j];
            var t = cd[0].t;
            var trace = cd[0].trace;

            if(trace.visible === true &&
                    (trace.type === 'box' || trace.type === 'candlestick') &&
                    !t.empty &&
                    (trace.orientation || 'v') === orientation &&
                    trace.xaxis === xa._id &&
                    trace.yaxis === ya._id
              ) {
                boxList.push(j);
            }
        }

        setPositionOffset('box', gd, boxList, posAxis);
    }
}

function setPositionOffset(traceType, gd, boxList, posAxis) {
    var calcdata = gd.calcdata;
    var fullLayout = gd._fullLayout;
    var axId = posAxis._id;
    var axLetter = axId.charAt(0);

    var i, j, calcTrace;
    var pointList = [];
    var shownPts = 0;

    // make list of box points
    for(i = 0; i < boxList.length; i++) {
        calcTrace = calcdata[boxList[i]];
        for(j = 0; j < calcTrace.length; j++) {
            pointList.push(posAxis.c2l(calcTrace[j].pos, true));
            shownPts += (calcTrace[j].pts2 || []).length;
        }
    }

    if(!pointList.length) return;

    // box plots - update dPos based on multiple traces
    var boxdv = Lib.distinctVals(pointList, {
        unitMinDiff: posAxis.type === 'category' || posAxis.type === 'multicategory'
    });

    var dPos0 = boxdv.minDiff / 2;

    // check for forced minimum dtick
    Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);

    var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
    var numTotal = fullLayout[numKey];
    var group = fullLayout[traceType + 'mode'] === 'group' && numTotal > 1;
    var groupFraction = 1 - fullLayout[traceType + 'gap'];
    var groupGapFraction = 1 - fullLayout[traceType + 'groupgap'];

    for(i = 0; i < boxList.length; i++) {
        calcTrace = calcdata[boxList[i]];

        var trace = calcTrace[0].trace;
        var t = calcTrace[0].t;
        var width = trace.width;
        var side = trace.side;

        // position coordinate delta
        var dPos;
        // box half width;
        var bdPos;
        // box center offset
        var bPos;
        // half-width within which to accept hover for this box/violin
        // always split the distance to the closest box/violin
        var wHover;

        if(width) {
            dPos = bdPos = wHover = width / 2;
            bPos = 0;
        } else {
            dPos = dPos0;

            if(group) {
                var groupId = getAxisGroup(fullLayout, posAxis._id) + trace.orientation;
                var alignmentGroups = fullLayout._alignmentOpts[groupId] || {};
                var alignmentGroupOpts = alignmentGroups[trace.alignmentgroup] || {};
                var nOffsetGroups = Object.keys(alignmentGroupOpts.offsetGroups || {}).length;
                var num = nOffsetGroups || numTotal;
                var shift = nOffsetGroups ? trace._offsetIndex : t.num;

                bdPos = dPos * groupFraction * groupGapFraction / num;
                bPos = 2 * dPos * (-0.5 + (shift + 0.5) / num) * groupFraction;
                wHover = dPos * groupFraction / num;
            } else {
                bdPos = dPos * groupFraction * groupGapFraction;
                bPos = 0;
                wHover = dPos;
            }
        }
        t.dPos = dPos;
        t.bPos = bPos;
        t.bdPos = bdPos;
        t.wHover = wHover;

        // box/violin-only value-space push value
        var pushplus;
        var pushminus;
        // edge of box/violin
        var edge = bPos + bdPos;
        var edgeplus;
        var edgeminus;
        // value-space padding
        var vpadplus;
        var vpadminus;
        // pixel-space padding
        var ppadplus;
        var ppadminus;
        // do we add 5% of both sides (more logic for points beyond box/violin below)
        var padded = Boolean(width);
        // does this trace show points?
        var hasPts = (trace.boxpoints || trace.points) && (shownPts > 0);

        if(side === 'positive') {
            pushplus = dPos * (width ? 1 : 0.5);
            edgeplus = edge;
            pushminus = edgeplus = bPos;
        } else if(side === 'negative') {
            pushplus = edgeplus = bPos;
            pushminus = dPos * (width ? 1 : 0.5);
            edgeminus = edge;
        } else {
            pushplus = pushminus = dPos;
            edgeplus = edgeminus = edge;
        }

        if(hasPts) {
            var pointpos = trace.pointpos;
            var jitter = trace.jitter;
            var ms = trace.marker.size / 2;

            var pp = 0;
            if((pointpos + jitter) >= 0) {
                pp = edge * (pointpos + jitter);
                if(pp > pushplus) {
                    // (++) beyond plus-value, use pp
                    padded = true;
                    ppadplus = ms;
                    vpadplus = pp;
                } else if(pp > edgeplus) {
                    // (+), use push-value (it's bigger), but add px-pad
                    ppadplus = ms;
                    vpadplus = pushplus;
                }
            }
            if(pp <= pushplus) {
                // (->) fallback to push value
                vpadplus = pushplus;
            }

            var pm = 0;
            if((pointpos - jitter) <= 0) {
                pm = -edge * (pointpos - jitter);
                if(pm > pushminus) {
                    // (--) beyond plus-value, use pp
                    padded = true;
                    ppadminus = ms;
                    vpadminus = pm;
                } else if(pm > edgeminus) {
                    // (-), use push-value (it's bigger), but add px-pad
                    ppadminus = ms;
                    vpadminus = pushminus;
                }
            }
            if(pm <= pushminus) {
                // (<-) fallback to push value
                vpadminus = pushminus;
            }
        } else {
            vpadplus = pushplus;
            vpadminus = pushminus;
        }

        var pos = new Array(calcTrace.length);
        for(j = 0; j < calcTrace.length; j++) {
            pos[j] = calcTrace[j].pos;
        }

        trace._extremes[axId] = Axes.findExtremes(posAxis, pos, {
            padded: padded,
            vpadminus: vpadminus,
            vpadplus: vpadplus,
            vpadLinearized: true,
            // N.B. SVG px-space positive/negative
            ppadminus: {x: ppadminus, y: ppadplus}[axLetter],
            ppadplus: {x: ppadplus, y: ppadminus}[axLetter],
        });
    }
}

module.exports = {
    crossTraceCalc: crossTraceCalc,
    setPositionOffset: setPositionOffset
};
<|endoftext|>


<commit_before>if(typeof define!=='function'){var define=require('amdefine')(module);}

define(function () {
  return function (Controller, collectionName, itemName, itemController) {

    Controller
      .onModel(collectionName, 'itemsAdded', 'addItemChildren')
      .onModel(collectionName, 'itemsRemoved', 'removeItemChildren')
      .onModel(collectionName, 'set', 'setItemChildren')

      .proto({

        setItemChildren: function () {
          this.destroyChild(collectionName)
          this.addItemChildren(this.models[collectionName])
        },

        addItemChildren: function (models) {
          models.forEach(this.addItemChild, this)
        },

        addItemChild: function (model) {
          var extraModels = {}
          extraModels[itemName] = model
          this.setChild([collectionName, model.mid()], itemController, extraModels)
        },

        removeItemChildren: function (models) {
          models.forEach(this.removeItemChild, this)
        },

        removeItemChild: function (model) {
          this.destroyChild([collectionName, model.mid()])
        }

      })

  }

})
<commit_msg>updated collectionController as per new interface<commit_after>if(typeof define!=='function'){var define=require('amdefine')(module);}

define(function () {
  return function (Controller, collectionName, itemName, ItemController) {

    Controller
      .onModel(collectionName, 'itemsAdded', 'addItemChildren')
      .onModel(collectionName, 'itemsRemoved', 'removeItemChildren')
      .onModel(collectionName, 'set', 'setItemChildren')

      .proto({

        setItemChildren: function () {
          this.destroyChildren(type: ItemController)
          this.addItemChildren(this.models[collectionName])
        },

        addItemChildren: function (models) {
          models.forEach(this.addItemChild, this)
        },

        addItemChild: function (model) {
          this.spawnWithModel(ItemController, itemName, model)
        },

        removeItemChildren: function (models) {
          models.forEach(this.removeItemChild, this)
        },

        removeItemChild: function (model) {
          this.destroyChildWithModel(itemName, model)
        }

      })

  }

})
<|endoftext|>


<commit_before>/**
 * Provides interface elements to display and review likes.
 *
 * @author	Alexander Ebert
 * @copyright	2001-2015 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLab/WCF/Ui/Like/Handler
 */
define(
	[
		'Ajax',      'Core',                     'Dictionary',         'Language',
		'ObjectMap', 'StringUtil',               'Dom/ChangeListener', 'Dom/Util',
		'Ui/Dialog', 'WoltLab/WCF/Ui/User/List'
	],
	function(
		Ajax,        Core,                        Dictionary,           Language,
		ObjectMap,   StringUtil,                  DomChangeListener,    DomUtil,
		UiDialog,    UiUserList
	)
{
	"use strict";

	var _isBusy = false;

	/**
	 * @constructor
	 */
	function UiLikeHandler(objectType, options) { this.init(objectType, options); }
	UiLikeHandler.prototype = {
		/**
		 * Initializes the like handler.
		 *
		 * @param	{string}	objectType	object type
		 * @param	{object}	options		initialization options
		 */
		init: function(objectType, options) {
			if (options.containerSelector === '') {
				throw new Error("[WoltLab/WCF/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
			}

			this._containers = new ObjectMap();
			this._details = new ObjectMap();
			this._objectType = objectType;
			this._options = Core.extend({
				// settings
				badgeClassNames: '',
				isSingleItem: false,
				markListItemAsActive: false,
				renderAsButton: true,
				summaryPrepend: true,
				summaryUseIcon: true,

				// permissions
				canDislike: false,
				canLike: false,
				canLikeOwnContent: false,
				canViewSummary: false,

				// selectors
				badgeContainerSelector: '.messageHeader .messageStatus',
				buttonAppendToSelector: '.messageFooter .messageFooterButtons',
				buttonBeforeSelector: '',
				containerSelector: '',
				summarySelector: '.messageFooterGroup'
			}, options);

			this.initContainers(options, objectType);

			DomChangeListener.add('WoltLab/WCF/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
		},

		/**
		 * Initializes all applicable containers.
		 */
		initContainers: function() {
			var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
			for (var i = 0, length = elements.length; i < length; i++) {
				element = elements[i];
				if (this._containers.has(element)) {
					continue;
				}

				elementData = {
					badge: null,
					dislikeButton: null,
					likeButton: null,
					summary: null,

					dislikes: ~~elData(element, 'like-dislikes'),
					liked: ~~elData(element, 'like-liked'),
					likes: ~~elData(element, 'like-likes'),
					objectId: ~~elData(element, 'object-id'),
					users: JSON.parse(elData(element, 'like-users'))
				};

				this._containers.set(element, elementData);
				this._buildWidget(element, elementData);

				triggerChange = true;
			}

			if (triggerChange) {
				DomChangeListener.trigger();
			}
		},

		/**
		 * Creates the interface elements.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	elementData	like data
		 */
		_buildWidget: function(element, elementData) {
			// build summary
			if (this._options.canViewSummary) {
				var summary, summaryContent, summaryIcon;
				var summaryContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
				if (summaryContainer !== null) {
					summary = elCreate('div');
					summary.className = 'likesSummary';

					if (this._options.summaryUseIcon) {
						summaryIcon = elCreate('span');
						summaryIcon.className = 'icon icon16 fa-thumbs-o-up';
						summary.appendChild(summaryIcon);
					}

					summaryContent = elCreate('span');
					summaryContent.className = 'likesSummaryContent';
					summaryContent.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
					summary.appendChild(summaryContent);

					if (this._options.summaryPrepend) {
						DomUtil.prepend(summary, summaryContainer);
					}
					else {
						summaryContainer.appendChild(summary);
					}

					elementData.summary = summaryContent;

					this._updateSummary(element);
				}
			}

			// cumulative likes
			var badge, listItem;
			var badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
			if (badgeContainer !== null) {
				badge = elCreate('a');
				badge.href = '#';
				badge.className = 'wcfLikeCounter jsTooltip' + (this._options.badgeClassNames ? ' ' + this._options.badgeClassNames : '');
				badge.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));

				if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
					listItem = elCreate('li');
					listItem.appendChild(badge);
					badgeContainer.appendChild(listItem);
				}
				else {
					badgeContainer.appendChild(badge);
				}

				elementData.badge = badge;

				this._updateBadge(element);
			}

			if (this._options.canLike && (WCF.User.userID != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
				var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
				var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
				if (insertPosition === null && appendTo === null) {
					throw new Error("Unable to find insert location for like/dislike buttons.");
				}
				else {
					// like button
					elementData.likeButton = this._createButton(element, true, insertPosition, appendTo);

					// dislike button
					if (this._options.canDislike) {
						elementData.dislikeButton = this._createButton(element, false, insertPosition, appendTo);
					}

					this._updateActiveState(element);
				}
			}
		},

		/**
		 * Creates a like or dislike button.
		 *
		 * @param	{Element}	element		container element
		 * @param	{boolean}	isLike		false if this is a dislike button
		 * @param	{Element?}	insertBefore	insert button before given element
		 * @param       {Element?}      appendTo        append button to given element
		 * @return	{Element}	button element
		 */
		_createButton: function(element, isLike, insertBefore, appendTo) {
			var title = Language.get('wcf.like.button.' + (isLike ? 'like' : 'dislike'));

			var listItem = elCreate('li');
			listItem.className = 'wcf' + (isLike ? 'Like' : 'Dislike') + 'Button';

			var button = elCreate('a');
			button.className = 'jsTooltip' + (this._options.renderAsButton ? ' button' : '');
			button.href = '#';
			button.title = title;
			button.innerHTML = ' ' + title + '';
			button.addEventListener(WCF_CLICK_EVENT, this._like.bind(this, element));
			elData(button, 'type', (isLike ? 'like' : 'dislike'));

			listItem.appendChild(button);

			if (insertBefore) {
				insertBefore.parentNode.insertBefore(listItem, insertBefore);
			}
			else {
				appendTo.appendChild(listItem);
			}

			return button;
		},

		/**
		 * Shows the summary of likes/dislikes.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	event		event object
		 */
		_showSummary: function(element, event) {
			event.preventDefault();

			if (!this._details.has(element)) {
				this._details.set(element, new UiUserList({
					className: 'wcf\\data\\like\\LikeAction',
					dialogTitle: Language.get('wcf.like.details'),
					parameters: {
						data: {
							containerID: DomUtil.identify(element),
							objectID: this._containers.get(element).objectId,
							objectType: this._objectType
						}
					}
				}));
			}

			this._details.get(element).open();
		},

		/**
		 * Updates the display of cumulative likes.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateBadge: function(element) {
			var data = this._containers.get(element);

			if (data.likes === 0 && data.dislikes === 0) {
				elHide(data.badge);
			}
			else {
				elShow(data.badge);

				// update like counter
				var cumulativeLikes = data.likes - data.dislikes;
				var content = '<span class="icon icon16 fa-thumbs-o-' + (cumulativeLikes < 0 ? 'down' : 'up' ) + '">';
				if (cumulativeLikes > 0) {
					content += '+' + StringUtil.addThousandsSeparator(cumulativeLikes);
					data.badge.classList.add('likeCounterLiked');
				}
				else if (cumulativeLikes < 0) {
					// U+2212 = minus sign
					content += '\u2212' + StringUtil.addThousandsSeparator(Math.abs(cumulativeLikes));
					data.badge.classList.add('likeCounterDisliked');
				}
				else {
					// U+00B1 = plus-minus sign
					content += '\u00B1' + '0';
				}

				data.badge.innerHTML = content + '';
				data.badge.setAttribute('data-tooltip', Language.get('wcf.like.tooltip', {
					dislikes: data.dislikes,
					likes: data.likes
				}));
			}
		},

		/**
		 * Updates the like summary.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateSummary: function(element) {
			var data = this._containers.get(element);

			if (data.likes) {
				elShow(data.summary.parentNode);

				var usernames = [];
				var keys = Object.keys(data.users);
				for (var i = 0, length = keys.length; i < length; i++) {
					usernames.push(data.users[keys[i]]);
				}

				var others = data.likes - usernames.length;
				data.summary.innerHTML = Language.get('wcf.like.summary', { users: usernames, others: others });
			}
			else {
				elHide(data.summary.parentNode);
			}
		},

		/**
		 * Updates the active like/dislike button state.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateActiveState: function(element) {
			var data = this._containers.get(element);

			var dislikeTarget = (this._options.markListItemAsActive) ? data.dislikeButton.parentNode : data.dislikeButton;
			var likeTarget = (this._options.markListItemAsActive) ? data.likeButton.parentNode : data.likeButton;

			if (data.dislikeButton !== null) dislikeTarget.classList.remove('active');
			likeTarget.classList.remove('active');

			if (data.liked === 1) {
				likeTarget.classList.add('active');
			}
			else if (data.liked === -1 && data.dislikeButton !== null) {
				dislikeTarget.classList.add('active');
			}
		},

		/**
		 * Likes or dislikes an element.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	event		event object
		 */
		_like: function(element, event) {
			event.preventDefault();

			if (_isBusy) {
				return;
			}

			_isBusy = true;

			Ajax.api(this, {
				actionName: elData(event.currentTarget, 'type'),
				parameters: {
					data: {
						containerID: DomUtil.identify(element),
						objectID: this._containers.get(element).objectId,
						objectType: this._objectType
					}
				}
			});
		},

		_ajaxSuccess: function(data) {
			var element = elById(data.returnValues.containerID);
			var elementData = this._containers.get(element);
			if (elementData === undefined) {
				return;
			}

			elementData.dislikes = ~~data.returnValues.dislikes;
			elementData.likes = ~~data.returnValues.likes;

			var users = data.returnValues.users;
			elementData.users = [];
			var keys = Object.keys(users);
			for (var i = 0, length = keys.length; i < length; i++) {
				elementData.users.push(StringUtil.escapeHTML(users[keys[i]].username));
			}

			if (data.returnValues.isLiked == 1) elementData.liked = 1;
			else if (data.returnValues.isDisliked == 1) elementData.liked = -1;
			else elementData.liked = 0;

			// update label
			this._updateBadge(element);

			// update summary
			if (this._options.canViewSummary) this._updateSummary(element);

			// mark button as active
			this._updateActiveState(element);

			// invalidate cache for like details
			this._details['delete'](element);

			_isBusy = false;
		},

		_ajaxSetup: function() {
			return {
				data: {
					className: 'wcf\\data\\like\\LikeAction'
				}
			};
		}
	};

	return UiLikeHandler;
});
<commit_msg>Replace usage of WCF.User with User<commit_after>/**
 * Provides interface elements to display and review likes.
 *
 * @author	Alexander Ebert
 * @copyright	2001-2015 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLab/WCF/Ui/Like/Handler
 */
define(
	[
		'Ajax',      'Core',                     'Dictionary',         'Language',
		'ObjectMap', 'StringUtil',               'Dom/ChangeListener', 'Dom/Util',
		'Ui/Dialog', 'WoltLab/WCF/Ui/User/List', 'User'
	],
	function(
		Ajax,        Core,                        Dictionary,           Language,
		ObjectMap,   StringUtil,                  DomChangeListener,    DomUtil,
		UiDialog,    UiUserList,                  User
	)
{
	"use strict";

	var _isBusy = false;

	/**
	 * @constructor
	 */
	function UiLikeHandler(objectType, options) { this.init(objectType, options); }
	UiLikeHandler.prototype = {
		/**
		 * Initializes the like handler.
		 *
		 * @param	{string}	objectType	object type
		 * @param	{object}	options		initialization options
		 */
		init: function(objectType, options) {
			if (options.containerSelector === '') {
				throw new Error("[WoltLab/WCF/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
			}

			this._containers = new ObjectMap();
			this._details = new ObjectMap();
			this._objectType = objectType;
			this._options = Core.extend({
				// settings
				badgeClassNames: '',
				isSingleItem: false,
				markListItemAsActive: false,
				renderAsButton: true,
				summaryPrepend: true,
				summaryUseIcon: true,

				// permissions
				canDislike: false,
				canLike: false,
				canLikeOwnContent: false,
				canViewSummary: false,

				// selectors
				badgeContainerSelector: '.messageHeader .messageStatus',
				buttonAppendToSelector: '.messageFooter .messageFooterButtons',
				buttonBeforeSelector: '',
				containerSelector: '',
				summarySelector: '.messageFooterGroup'
			}, options);

			this.initContainers(options, objectType);

			DomChangeListener.add('WoltLab/WCF/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
		},

		/**
		 * Initializes all applicable containers.
		 */
		initContainers: function() {
			var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
			for (var i = 0, length = elements.length; i < length; i++) {
				element = elements[i];
				if (this._containers.has(element)) {
					continue;
				}

				elementData = {
					badge: null,
					dislikeButton: null,
					likeButton: null,
					summary: null,

					dislikes: ~~elData(element, 'like-dislikes'),
					liked: ~~elData(element, 'like-liked'),
					likes: ~~elData(element, 'like-likes'),
					objectId: ~~elData(element, 'object-id'),
					users: JSON.parse(elData(element, 'like-users'))
				};

				this._containers.set(element, elementData);
				this._buildWidget(element, elementData);

				triggerChange = true;
			}

			if (triggerChange) {
				DomChangeListener.trigger();
			}
		},

		/**
		 * Creates the interface elements.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	elementData	like data
		 */
		_buildWidget: function(element, elementData) {
			// build summary
			if (this._options.canViewSummary) {
				var summary, summaryContent, summaryIcon;
				var summaryContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
				if (summaryContainer !== null) {
					summary = elCreate('div');
					summary.className = 'likesSummary';

					if (this._options.summaryUseIcon) {
						summaryIcon = elCreate('span');
						summaryIcon.className = 'icon icon16 fa-thumbs-o-up';
						summary.appendChild(summaryIcon);
					}

					summaryContent = elCreate('span');
					summaryContent.className = 'likesSummaryContent';
					summaryContent.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
					summary.appendChild(summaryContent);

					if (this._options.summaryPrepend) {
						DomUtil.prepend(summary, summaryContainer);
					}
					else {
						summaryContainer.appendChild(summary);
					}

					elementData.summary = summaryContent;

					this._updateSummary(element);
				}
			}

			// cumulative likes
			var badge, listItem;
			var badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
			if (badgeContainer !== null) {
				badge = elCreate('a');
				badge.href = '#';
				badge.className = 'wcfLikeCounter jsTooltip' + (this._options.badgeClassNames ? ' ' + this._options.badgeClassNames : '');
				badge.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));

				if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
					listItem = elCreate('li');
					listItem.appendChild(badge);
					badgeContainer.appendChild(listItem);
				}
				else {
					badgeContainer.appendChild(badge);
				}

				elementData.badge = badge;

				this._updateBadge(element);
			}

			if (this._options.canLike && (User.userId != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
				var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
				var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
				if (insertPosition === null && appendTo === null) {
					throw new Error("Unable to find insert location for like/dislike buttons.");
				}
				else {
					// like button
					elementData.likeButton = this._createButton(element, true, insertPosition, appendTo);

					// dislike button
					if (this._options.canDislike) {
						elementData.dislikeButton = this._createButton(element, false, insertPosition, appendTo);
					}

					this._updateActiveState(element);
				}
			}
		},

		/**
		 * Creates a like or dislike button.
		 *
		 * @param	{Element}	element		container element
		 * @param	{boolean}	isLike		false if this is a dislike button
		 * @param	{Element?}	insertBefore	insert button before given element
		 * @param       {Element?}      appendTo        append button to given element
		 * @return	{Element}	button element
		 */
		_createButton: function(element, isLike, insertBefore, appendTo) {
			var title = Language.get('wcf.like.button.' + (isLike ? 'like' : 'dislike'));

			var listItem = elCreate('li');
			listItem.className = 'wcf' + (isLike ? 'Like' : 'Dislike') + 'Button';

			var button = elCreate('a');
			button.className = 'jsTooltip' + (this._options.renderAsButton ? ' button' : '');
			button.href = '#';
			button.title = title;
			button.innerHTML = ' ' + title + '';
			button.addEventListener(WCF_CLICK_EVENT, this._like.bind(this, element));
			elData(button, 'type', (isLike ? 'like' : 'dislike'));

			listItem.appendChild(button);

			if (insertBefore) {
				insertBefore.parentNode.insertBefore(listItem, insertBefore);
			}
			else {
				appendTo.appendChild(listItem);
			}

			return button;
		},

		/**
		 * Shows the summary of likes/dislikes.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	event		event object
		 */
		_showSummary: function(element, event) {
			event.preventDefault();

			if (!this._details.has(element)) {
				this._details.set(element, new UiUserList({
					className: 'wcf\\data\\like\\LikeAction',
					dialogTitle: Language.get('wcf.like.details'),
					parameters: {
						data: {
							containerID: DomUtil.identify(element),
							objectID: this._containers.get(element).objectId,
							objectType: this._objectType
						}
					}
				}));
			}

			this._details.get(element).open();
		},

		/**
		 * Updates the display of cumulative likes.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateBadge: function(element) {
			var data = this._containers.get(element);

			if (data.likes === 0 && data.dislikes === 0) {
				elHide(data.badge);
			}
			else {
				elShow(data.badge);

				// update like counter
				var cumulativeLikes = data.likes - data.dislikes;
				var content = '<span class="icon icon16 fa-thumbs-o-' + (cumulativeLikes < 0 ? 'down' : 'up' ) + '">';
				if (cumulativeLikes > 0) {
					content += '+' + StringUtil.addThousandsSeparator(cumulativeLikes);
					data.badge.classList.add('likeCounterLiked');
				}
				else if (cumulativeLikes < 0) {
					// U+2212 = minus sign
					content += '\u2212' + StringUtil.addThousandsSeparator(Math.abs(cumulativeLikes));
					data.badge.classList.add('likeCounterDisliked');
				}
				else {
					// U+00B1 = plus-minus sign
					content += '\u00B1' + '0';
				}

				data.badge.innerHTML = content + '';
				data.badge.setAttribute('data-tooltip', Language.get('wcf.like.tooltip', {
					dislikes: data.dislikes,
					likes: data.likes
				}));
			}
		},

		/**
		 * Updates the like summary.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateSummary: function(element) {
			var data = this._containers.get(element);

			if (data.likes) {
				elShow(data.summary.parentNode);

				var usernames = [];
				var keys = Object.keys(data.users);
				for (var i = 0, length = keys.length; i < length; i++) {
					usernames.push(data.users[keys[i]]);
				}

				var others = data.likes - usernames.length;
				data.summary.innerHTML = Language.get('wcf.like.summary', { users: usernames, others: others });
			}
			else {
				elHide(data.summary.parentNode);
			}
		},

		/**
		 * Updates the active like/dislike button state.
		 *
		 * @param	{Element}	element		container element
		 */
		_updateActiveState: function(element) {
			var data = this._containers.get(element);

			var dislikeTarget = (this._options.markListItemAsActive) ? data.dislikeButton.parentNode : data.dislikeButton;
			var likeTarget = (this._options.markListItemAsActive) ? data.likeButton.parentNode : data.likeButton;

			if (data.dislikeButton !== null) dislikeTarget.classList.remove('active');
			likeTarget.classList.remove('active');

			if (data.liked === 1) {
				likeTarget.classList.add('active');
			}
			else if (data.liked === -1 && data.dislikeButton !== null) {
				dislikeTarget.classList.add('active');
			}
		},

		/**
		 * Likes or dislikes an element.
		 *
		 * @param	{Element}	element		container element
		 * @param	{object}	event		event object
		 */
		_like: function(element, event) {
			event.preventDefault();

			if (_isBusy) {
				return;
			}

			_isBusy = true;

			Ajax.api(this, {
				actionName: elData(event.currentTarget, 'type'),
				parameters: {
					data: {
						containerID: DomUtil.identify(element),
						objectID: this._containers.get(element).objectId,
						objectType: this._objectType
					}
				}
			});
		},

		_ajaxSuccess: function(data) {
			var element = elById(data.returnValues.containerID);
			var elementData = this._containers.get(element);
			if (elementData === undefined) {
				return;
			}

			elementData.dislikes = ~~data.returnValues.dislikes;
			elementData.likes = ~~data.returnValues.likes;

			var users = data.returnValues.users;
			elementData.users = [];
			var keys = Object.keys(users);
			for (var i = 0, length = keys.length; i < length; i++) {
				elementData.users.push(StringUtil.escapeHTML(users[keys[i]].username));
			}

			if (data.returnValues.isLiked == 1) elementData.liked = 1;
			else if (data.returnValues.isDisliked == 1) elementData.liked = -1;
			else elementData.liked = 0;

			// update label
			this._updateBadge(element);

			// update summary
			if (this._options.canViewSummary) this._updateSummary(element);

			// mark button as active
			this._updateActiveState(element);

			// invalidate cache for like details
			this._details['delete'](element);

			_isBusy = false;
		},

		_ajaxSetup: function() {
			return {
				data: {
					className: 'wcf\\data\\like\\LikeAction'
				}
			};
		}
	};

	return UiLikeHandler;
});
<|endoftext|>


<commit_before>/**
 * Wrapper around the web browser's various clipboard APIs.
 *
 * @author	Tim Duesterhus
 * @copyright	2001-2019 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLabSuite/Core/Clipboard
 */
define(['Environment', 'Ui/Screen'], function(Environment, UiScreen) {
	"use strict";

	return {
		copyTextToClipboard: function (text) {
			if (navigator.clipboard) {
				return navigator.clipboard.writeText(text);
			}
			else if (window.getSelection) {
				var textarea = elCreate('textarea');
				textarea.contentEditable = true;
				textarea.readOnly = false;

				// iOS has some implicit restrictions that, if crossed, cause the browser to scroll to the top.
				var scrollDisabled = false;
				if (Environment.platform() === 'ios') {
					scrollDisabled = true;
					UiScreen.scrollDisable();

					var topPx = (~~(window.innerHeight / 4) + window.pageYOffset);
					textarea.style.cssText = 'font-size: 16px; position: absolute; left: 1px; top: ' + topPx + 'px; width: 50px; height: 50px; overflow: hidden;border: 5px solid red;';
				}
				else {
					textarea.style.cssText = 'position: absolute; left: -9999px; top: -9999px; width: 0; height: 0;';
				}

				document.body.appendChild(textarea);
				try {
					// see: https://stackoverflow.com/a/34046084/782822
					textarea.value = text;
					var range = document.createRange();
					range.selectNodeContents(textarea);
					var selection = window.getSelection();
					selection.removeAllRanges();
					selection.addRange(range);
					textarea.setSelectionRange(0, 999999);
					if (!document.execCommand('copy')) {
						return Promise.reject(new Error("execCommand('copy') failed"));
					}
					return Promise.resolve();
				}
				finally {
					elRemove(textarea);

					if (scrollDisabled) {
						UiScreen.scrollEnable();
					}
				}
			}

			return Promise.reject(new Error('Neither navigator.clipboard, nor window.getSelection is supported.'));
		},

		copyElementTextToClipboard: function (element) {
			return this.copyTextToClipboard(element.textContent);
		}
	};
});
<commit_msg>Remove/replace nbsp when copying code to the clipboard<commit_after>/**
 * Wrapper around the web browser's various clipboard APIs.
 *
 * @author	Tim Duesterhus
 * @copyright	2001-2019 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLabSuite/Core/Clipboard
 */
define(['Environment', 'Ui/Screen'], function(Environment, UiScreen) {
	"use strict";

	return {
		copyTextToClipboard: function (text) {
			if (navigator.clipboard) {
				return navigator.clipboard.writeText(text);
			}
			else if (window.getSelection) {
				var textarea = elCreate('textarea');
				textarea.contentEditable = true;
				textarea.readOnly = false;

				// iOS has some implicit restrictions that, if crossed, cause the browser to scroll to the top.
				var scrollDisabled = false;
				if (Environment.platform() === 'ios') {
					scrollDisabled = true;
					UiScreen.scrollDisable();

					var topPx = (~~(window.innerHeight / 4) + window.pageYOffset);
					textarea.style.cssText = 'font-size: 16px; position: absolute; left: 1px; top: ' + topPx + 'px; width: 50px; height: 50px; overflow: hidden;border: 5px solid red;';
				}
				else {
					textarea.style.cssText = 'position: absolute; left: -9999px; top: -9999px; width: 0; height: 0;';
				}

				document.body.appendChild(textarea);
				try {
					// see: https://stackoverflow.com/a/34046084/782822
					textarea.value = text;
					var range = document.createRange();
					range.selectNodeContents(textarea);
					var selection = window.getSelection();
					selection.removeAllRanges();
					selection.addRange(range);
					textarea.setSelectionRange(0, 999999);
					if (!document.execCommand('copy')) {
						return Promise.reject(new Error("execCommand('copy') failed"));
					}
					return Promise.resolve();
				}
				finally {
					elRemove(textarea);

					if (scrollDisabled) {
						UiScreen.scrollEnable();
					}
				}
			}

			return Promise.reject(new Error('Neither navigator.clipboard, nor window.getSelection is supported.'));
		},

		copyElementTextToClipboard: function (element) {
			return this.copyTextToClipboard(element.textContent.replace(/\u200B/g, '').replace(/\u00A0/g, ' '));
		}
	};
});
<|endoftext|>