<commit_before>// ----------------------------------------------------------------------------
// Boom - A tiny audio library
// v 1.0 alpha
// Dual licensed under the MIT and GPL licenses.
// http://boom.jaysalvat.com/
// ----------------------------------------------------------------------------
// Copyright (C) 2011 Jay Salvat
// http://jaysalvat.com/
// ----------------------------------------------------------------------------
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files ( the "Software" ), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ----------------------------------------------------------------------------
var boom = {
    defaults: {
        preload: 'metadata', // auto, metadata, none
        autoplay: false, // bool
        loop: false, // bool
        volume: 100,
        placeholder: '--'
    },
    sounds: [],
    el: document.createElement( 'audio' ),
    isSupported: function() {
        return  !!( this.el.canPlayType );
    },
    isOGGSupported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/ogg; codecs="vorbis"' );
    },
    isWAVSupported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/wav; codecs="1"' );
    },
    isMP3Supported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/mpeg;' );
    },
    isAACSupported: function() {
        return !!this.el.canPlayType && ( this.el.canPlayType( 'audio/x-m4a;' ) || this.el.canPlayType( 'audio/aac;' ) );
    },
    sound: function( src, options ) {
        var options = options || {},
            events = [],
            supported = boom.isSupported();

        this.load = function() {
            if ( !supported ) return this;

            this.sound.load();
            return this;
        }
        this.play = function() {
            if ( !supported ) return this;

            this.sound.play();
            return this;
        }
        this.stop = function() {
            if ( !supported  ) return this;

            this.setTime(0);
            this.sound.pause();
            return this;
        }
        this.isEnded = function() {
            if ( !supported ) return null;

            return this.sound.ended;
        }
        this.pause = function() {
            if ( !supported ) return this;

            this.sound.pause();
            return this;
        }
        this.isPaused = function() {
            if ( !supported ) return null;

            return this.sound.paused;
        }
        this.mute = function() {
            if ( !supported ) return this;

            this.sound.muted = true;
            return this;
        }
        this.unmute = function() {
            if ( !supported ) return this;

            this.sound.muted = false;
            return this;
        }
        this.toggleMute = function() {
            if ( !supported ) return this;

            this.sound.muted = !this.sound.muted;
            return this;
        }
        this.isMuted = function() {
            if ( !supported ) return null;

            return this.sound.muted;
        }
        this.togglePlay = function() {
            if ( !supported ) return this;

            if ( this.sound.paused ) {
                this.sound.play();
            } else {
                this.sound.pause();
            }
            return this;
        }
        this.setVolume = function( volume ) {
            if ( !supported ) return this;

            if ( volume < 0 ) volume = 0;
            if ( volume > 100 ) volume = 100;
            this.volume = volume;
            this.sound.volume = volume / 100;
            return this;
        },
        this.getVolume = function() {
            if ( !supported ) return this;

            return this.volume;
        }
        this.increaseVolume = function( value ) {
            return this.setVolume( this.volume + ( value || 1 ) );
        }
        this.decreaseVolume = function( value ) {
            return this.setVolume( this.volume - ( value || 1 ) );
        }
        this.setTime = function( time ) {
            if ( !supported ) return this;

            if ( time instanceof String) {
                var splits = time.split( ':' );
                if ( splits && splits.length == 3 ) {
                    time = ( parseInt( splits[0] ) * 3600 ) + ( parseInt(splits[1] ) * 60 ) + parseInt( splits[2] );
                }
                if ( splits && splits.length == 2 ) {
                    time = ( parseInt( splits[0] ) * 60 ) + parseInt( splits[1] );
                }
            }
            try {
                this.sound.currentTime = time;
            } catch(e) {}
            return this;
        }
        this.getTime = function() {
            if ( !supported ) return null;

            var time = Math.round( this.sound.currentTime * 100 ) / 100;
            return isNaN(time) ? boom.defaults.placeholder : time;
        }
        this.getDuration = function() {
            if ( !supported ) return null;

            var duration = Math.round( this.sound.duration * 100 ) / 100;
            return isNaN(duration) ? boom.defaults.placeholder : duration;
        }
        this.setPercent = function( time ) {
            if ( !supported ) return this;

            return this.setTime( this.sound.duration * time / 100 );
        }
        this.getPercent = function() {
            if ( !supported ) return null;

            var percent = Math.round( (this.sound.currentTime / this.sound.duration * 100 ) * 100) / 100;
            return isNaN(percent) ? boom.defaults.placeholder : percent;
        }
        this.set = function( key, value ) {
            if ( !supported ) return this;

            this.sound[ key ] = value;
            return this;
        }
        this.get = function( key ) {
            if ( !supported ) return null;

            return key ? this.sound[ key ] : this.sound;
        }
        this.bind = function( type, func ) {
            if ( !supported ) return this;

            var idx = type;
            if ( type.indexOf( '.' ) > -1 ) {
                type = type.split( '.' )[1];
            }
            events.push( { idx: idx, func: func } );
            this.sound.addEventListener( type, func, true );
            return this;
        }
        this.unbind = function( type ) {
            if ( !supported ) return this;

            var idx = type;
            if ( type.indexOf( '.' ) > -1 ) {
                type = type.split( '.' )[1];
            }
            for( var i in events ) {
                var namespace = events[ i ].idx.match( /\.(.*)/ );
                 if ( events[ i ].idx == idx || ( namespace && namespace[1] == idx.replace('.', '') ) ) {
                    this.sound.removeEventListener( type, events[ i ].func );
                    delete events[ i ];
                }
            }
            return this;
        }
        this.destroy = function() {
            if ( !supported ) return this;

            for( var i in boom.sounds ) {
                if ( boom.sounds[ i ] == this ) {
                    delete boom.sounds[ i ];
                    break;
                }
            }
            return this;
        }

        if ( supported ) {
            for( var i in boom.defaults ) {
                options[ i ] = options[ i ] || boom.defaults[ i ];
            }

            this.sound = document.createElement( 'audio' );
            if ( src instanceof Array ) {
                for( var i in src ) {
                    var source = document.createElement( 'source' );
                    source.src = src[ i ];
                    this.sound.appendChild( source );
                }
            } else {
                this.sound.src = src;
            }
            if ( options.loop ) {
                this.bind('boomloop.ended', function() {
                    this.currentTime = 0;
                    this.play();
                });
            }
            if ( options.autoplay ) {
                this.sound.autoplay = 'autoplay';
            }
            this.sound.preload = options.preload;
            this.volume = options.volume;

            boom.sounds.push( this );
        }
    },
    all: function() {
      return new boom.group( boom.sounds );
    },
    group: function( sounds ) {
        fn = function() {
            var args = Array.prototype.slice.call( arguments ),
                func = args.shift();

            for( var i in sounds ) {
                sounds[ i ][ func ].apply( sounds[ i ], args );
            }
        }
        this.play = function() {
            fn( 'play' );
        }
        this.stop = function() {
            fn( 'stop' );
        }
        this.pause = function( time ) {
            fn( 'pause', time );
        }
        this.togglePlay = function( ) {
            fn( 'togglePlay' );
        }
        this.mute = function() {
            fn( 'mute' );
        }
        this.unmute = function() {
            fn( 'unmute' );
        }
        this.toggleMute = function() {
            fn( 'toggleMute' );
        }
        this.setVolume = function( volume ) {
            fn( 'setVolume', volume );
        }
        this.increaseVolume = function( value ) {
            fn( 'increaseVolume', value );
        }
        this.decreaseVolume = function( value ) {
            fn( 'decreaseVolume', value );
        }
        this.setTime = function( time ) {
            fn( 'setTime', time );
        }
        this.set = function( key, value ) {
            fn( 'set', key, value );
        }
        this.bind = function( type, func ) {
            fn( 'bind', type, func );
        }
        this.unbind = function( type ) {
            fn( 'unbind', type );
        }
        this.destroy = function() {
            fn( 'destroy' );
        }
    },
    formatTime: function( s, withHours ) {
        h = Math.floor( s / 3600 );
        h = isNaN(h) ? '--' : ( h >= 10 ) ? h : '0' + h;
        m = withHours ? Math.floor( s / 60 % 60 ) : Math.floor( s / 60 );
        m = isNaN(m) ? '--' : ( m >= 10 ) ? m : '0' + m;
        s = Math.floor( s % 60 );
        s = isNaN(s) ? '--' : ( s >= 10 ) ? s : '0' + s;
        return withHours ? h + ':' + m + ':' + s : m + ':' + s;
    }
}<commit_msg>Add fadeIn and fadeOut methods<commit_after>// ----------------------------------------------------------------------------
// Boom - A HTML5 audio library
// v 1.0 alpha
// Dual licensed under the MIT and GPL licenses.
// http://boom.jaysalvat.com/
// ----------------------------------------------------------------------------
// Copyright (C) 2011 Jay Salvat
// http://jaysalvat.com/
// ----------------------------------------------------------------------------
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files ( the "Software" ), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// ----------------------------------------------------------------------------
var boom = {
    defaults: {
        preload: 'metadata', // auto, metadata, none
        autoplay: false, // bool
        loop: false, // bool
        volume: 100,
        placeholder: '--',
        fadeSpeed: 5000
    },
    sounds: [],
    el: document.createElement( 'audio' ),
    isSupported: function() {
        return  !!( this.el.canPlayType );
    },
    isOGGSupported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/ogg; codecs="vorbis"' );
    },
    isWAVSupported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/wav; codecs="1"' );
    },
    isMP3Supported: function() {
        return !!this.el.canPlayType && this.el.canPlayType( 'audio/mpeg;' );
    },
    isAACSupported: function() {
        return !!this.el.canPlayType && ( this.el.canPlayType( 'audio/x-m4a;' ) || this.el.canPlayType( 'audio/aac;' ) );
    },
    sound: function( src, options ) {
        var options = options || {},
            events = [],
            supported = boom.isSupported();

        this.load = function() {
            if ( !supported ) return this;

            this.sound.load();
            return this;
        }
        this.play = function() {
            if ( !supported ) return this;

            this.sound.play();
            return this;
        }
        this.stop = function() {
            if ( !supported  ) return this;

            this.setTime(0);
            this.sound.pause();
            return this;
        }
        this.isEnded = function() {
            if ( !supported ) return null;

            return this.sound.ended;
        }
        this.pause = function() {
            if ( !supported ) return this;

            this.sound.pause();
            return this;
        }
        this.isPaused = function() {
            if ( !supported ) return null;

            return this.sound.paused;
        }
        this.mute = function() {
            if ( !supported ) return this;

            this.sound.muted = true;
            return this;
        }
        this.unmute = function() {
            if ( !supported ) return this;

            this.sound.muted = false;
            return this;
        }
        this.toggleMute = function() {
            if ( !supported ) return this;

            this.sound.muted = !this.sound.muted;
            return this;
        }
        this.isMuted = function() {
            if ( !supported ) return null;

            return this.sound.muted;
        }
        this.togglePlay = function() {
            if ( !supported ) return this;

            if ( this.sound.paused ) {
                this.sound.play();
            } else {
                this.sound.pause();
            }
            return this;
        }
        this.setVolume = function( volume ) {
            if ( !supported ) return this;

            if ( volume < 0 ) volume = 0;
            if ( volume > 100 ) volume = 100;
            this.volume = volume;
            this.sound.volume = volume / 100;
            return this;
        },
        this.getVolume = function() {
            if ( !supported ) return this;

            return this.volume;
        }
        this.increaseVolume = function( value ) {
            return this.setVolume( this.volume + ( value || 1 ) );
        }
        this.decreaseVolume = function( value ) {
            return this.setVolume( this.volume - ( value || 1 ) );
        }
        this.setTime = function( time ) {
            if ( !supported ) return this;

            if ( time instanceof String) {
                var splits = time.split( ':' );
                if ( splits && splits.length == 3 ) {
                    time = ( parseInt( splits[0] ) * 3600 ) + ( parseInt(splits[1] ) * 60 ) + parseInt( splits[2] );
                }
                if ( splits && splits.length == 2 ) {
                    time = ( parseInt( splits[0] ) * 60 ) + parseInt( splits[1] );
                }
            }
            try {
                this.sound.currentTime = time;
            } catch(e) {}
            return this;
        }
        this.getTime = function() {
            if ( !supported ) return null;

            var time = Math.round( this.sound.currentTime * 100 ) / 100;
            return isNaN(time) ? boom.defaults.placeholder : time;
        }
        this.getDuration = function() {
            if ( !supported ) return null;

            var duration = Math.round( this.sound.duration * 100 ) / 100;
            return isNaN(duration) ? boom.defaults.placeholder : duration;
        }
        this.setPercent = function( time ) {
            if ( !supported ) return this;

            return this.setTime( this.sound.duration * time / 100 );
        }
        this.getPercent = function() {
            if ( !supported ) return null;

            var percent = Math.round( (this.sound.currentTime / this.sound.duration * 100 ) * 100) / 100;
            return isNaN(percent) ? boom.defaults.placeholder : percent;
        }
        this.set = function( key, value ) {
            if ( !supported ) return this;

            this.sound[ key ] = value;
            return this;
        }
        this.get = function( key ) {
            if ( !supported ) return null;

            return key ? this.sound[ key ] : this.sound;
        }
        this.bind = function( type, func ) {
            if ( !supported ) return this;

            var idx = type;
            if ( type.indexOf( '.' ) > -1 ) {
                type = type.split( '.' )[1];
            }
            events.push( { idx: idx, func: func } );
            this.sound.addEventListener( type, func, true );
            return this;
        }
        this.unbind = function( type ) {
            if ( !supported ) return this;

            var idx = type;
            if ( type.indexOf( '.' ) > -1 ) {
                type = type.split( '.' )[1];
            }
            for( var i in events ) {
                var namespace = events[ i ].idx.match( /\.(.*)/ );
                 if ( events[ i ].idx == idx || ( namespace && namespace[1] == idx.replace('.', '') ) ) {
                    this.sound.removeEventListener( type, events[ i ].func );
                    delete events[ i ];
                }
            }
            return this;
        }
        this.destroy = function() {
            if ( !supported ) return this;

            for( var i in boom.sounds ) {
                if ( boom.sounds[ i ] == this ) {
                    delete boom.sounds[ i ];
                    break;
                }
            }
            return this;
        }
        this.fadeIn = function( speed, callback ) {
            speed = speed || boom.defaults.fadeSpeed;
            var delay = speed / 100,
                that = this;
            this.volume = 0;

            function doFade() {
                setTimeout( function() {
                    if ( that.volume < 100 ) {
                        that.setVolume( that.volume += 1 );
                        doFade();
                    } else if ( callback instanceof Function ) {
                        callback.apply( that );
                    }
                }, delay );
            }
            doFade();
        }
        this.fadeOut = function( speed, callback ) {
            speed = speed || boom.defaults.fadeSpeed;
            var delay = speed / this.volume,
                that = this;

            function doFade() {
                setTimeout( function() {
                    if ( that.volume > 0 ) {
                        that.setVolume( that.volume -= 1 );
                        doFade();
                    } else if ( callback instanceof Function ) {
                        callback.apply( that );
                    }
                }, delay );
            }
            doFade();
        }

        // init
        if ( supported ) {
            for( var i in boom.defaults ) {
                options[ i ] = options[ i ] || boom.defaults[ i ];
            }

            this.sound = document.createElement( 'audio' );
            if ( src instanceof Array ) {
                for( var i in src ) {
                    var source = document.createElement( 'source' );
                    source.src = src[ i ];
                    this.sound.appendChild( source );
                }
            } else {
                this.sound.src = src;
            }
            if ( options.loop ) {
                this.bind('boomloop.ended', function() {
                    this.currentTime = 0;
                    this.play();
                });
            }
            if ( options.autoplay ) {
                this.sound.autoplay = 'autoplay';
            }
            this.sound.preload = options.preload;
            this.volume = options.volume;

            boom.sounds.push( this );
        }
    },
    all: function() {
      return new boom.group( boom.sounds );
    },
    group: function( sounds ) {
        fn = function() {
            var args = Array.prototype.slice.call( arguments ),
                func = args.shift();

            for( var i in sounds ) {
                sounds[ i ][ func ].apply( sounds[ i ], args );
            }
        }
        this.play = function() {
            fn( 'play' );
        }
        this.stop = function() {
            fn( 'stop' );
        }
        this.pause = function( time ) {
            fn( 'pause', time );
        }
        this.togglePlay = function( ) {
            fn( 'togglePlay' );
        }
        this.mute = function() {
            fn( 'mute' );
        }
        this.unmute = function() {
            fn( 'unmute' );
        }
        this.toggleMute = function() {
            fn( 'toggleMute' );
        }
        this.setVolume = function( volume ) {
            fn( 'setVolume', volume );
        }
        this.increaseVolume = function( value ) {
            fn( 'increaseVolume', value );
        }
        this.decreaseVolume = function( value ) {
            fn( 'decreaseVolume', value );
        }
        this.setTime = function( time ) {
            fn( 'setTime', time );
        }
        this.set = function( key, value ) {
            fn( 'set', key, value );
        }
        this.bind = function( type, func ) {
            fn( 'bind', type, func );
        }
        this.unbind = function( type ) {
            fn( 'unbind', type );
        }
        this.destroy = function() {
            fn( 'destroy' );
        }
        this.fadeIn = function( speed, callback ) {
            fn( 'fadeIn', speed, callback );
        }
        this.fadeOut = function( speed, callback ) {
            fn( 'fadeOut', speed, callback );
        }
    },
    formatTime: function( s, withHours ) {
        h = Math.floor( s / 3600 );
        h = isNaN(h) ? '--' : ( h >= 10 ) ? h : '0' + h;
        m = withHours ? Math.floor( s / 60 % 60 ) : Math.floor( s / 60 );
        m = isNaN(m) ? '--' : ( m >= 10 ) ? m : '0' + m;
        s = Math.floor( s % 60 );
        s = isNaN(s) ? '--' : ( s >= 10 ) ? s : '0' + s;
        return withHours ? h + ':' + m + ':' + s : m + ':' + s;
    }
}<|endoftext|>


<commit_before>/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */

/* Magic Mirror
 * Module Blueprint.
 * @typedef {Object} Module
 *
 * By Michael Teeuw https://michaelteeuw.nl
 * MIT Licensed.
 *
 */
var Module = Class.extend({
	/*********************************************************
	 * All methods (and properties) below can be subclassed. *
	 *********************************************************/

	// Set the minimum MagicMirror module version for this module.
	requiresVersion: "2.0.0",

	// Module config defaults.
	defaults: {},

	// Timer reference used for showHide animation callbacks.
	showHideTimer: null,

	// Array to store lockStrings. These strings are used to lock
	// visibility when hiding and showing module.
	lockStrings: [],

	// Storage of the nunjuck Environment,
	// This should not be referenced directly.
	// Use the nunjucksEnvironment() to get it.
	_nunjucksEnvironment: null,

	/**
	 * Called when the module is instantiated.
	 */
	init: function () {
		//Log.log(this.defaults);
	},

	/**
	 * Called when the module is started.
	 */
	start: function () {
		Log.info("Starting module: " + this.name);
	},

	/**
	 * Returns a list of scripts the module requires to be loaded.
	 *
	 * @returns {string[]} An array with filenames.
	 */
	getScripts: function () {
		return [];
	},

	/**
	 * Returns a list of stylesheets the module requires to be loaded.
	 *
	 * @returns {string[]} An array with filenames.
	 */
	getStyles: function () {
		return [];
	},

	/**
	 * Returns a map of translation files the module requires to be loaded.
	 *
	 * return Map<String, String> -
	 *
	 * @returns {*} A map with langKeys and filenames.
	 */
	getTranslations: function () {
		return false;
	},

	/**
	 * Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
	 * This method can to be subclassed if the module wants to display info on the mirror.
	 * Alternatively, the getTemplate method could be subclassed.
	 *
	 * @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
	 */
	getDom: function () {
		var self = this;
		return new Promise(function (resolve) {
			var div = document.createElement("div");
			var template = self.getTemplate();
			var templateData = self.getTemplateData();

			// Check to see if we need to render a template string or a file.
			if (/^.*((\.html)|(\.njk))$/.test(template)) {
				// the template is a filename
				self.nunjucksEnvironment().render(template, templateData, function (err, res) {
					if (err) {
						Log.error(err);
					}

					div.innerHTML = res;

					resolve(div);
				});
			} else {
				// the template is a template string.
				div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);

				resolve(div);
			}
		});
	},

	/**
	 * Generates the header string which needs to be displayed if a user has a header configured for this module.
	 * This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
	 * This method needs to be subclassed if the module wants to display modified headers on the mirror.
	 *
	 * @returns {string} The header to display above the header.
	 */
	getHeader: function () {
		return this.data.header;
	},

	/**
	 * Returns the template for the module which is used by the default getDom implementation.
	 * This method needs to be subclassed if the module wants to use a template.
	 * It can either return a template sting, or a template filename.
	 * If the string ends with '.html' it's considered a file from within the module's folder.
	 *
	 * @returns {string} The template string of filename.
	 */
	getTemplate: function () {
		return '
' + this.name + '

' + this.identifier + "
";
	},

	/**
	 * Returns the data to be used in the template.
	 * This method needs to be subclassed if the module wants to use a custom data.
	 *
	 * @returns {object} The data for the template
	 */
	getTemplateData: function () {
		return {};
	},

	/**
	 * Called by the Magic Mirror core when a notification arrives.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 * @param {Module} sender The module that sent the notification.
	 */
	notificationReceived: function (notification, payload, sender) {
		if (sender) {
			// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
		} else {
			// Log.log(this.name + " received a system notification: " + notification);
		}
	},

	/**
	 * Returns the nunjucks environment for the current module.
	 * The environment is checked in the _nunjucksEnvironment instance variable.
	 *
	 * @returns {object} The Nunjucks Environment
	 */
	nunjucksEnvironment: function () {
		if (this._nunjucksEnvironment !== null) {
			return this._nunjucksEnvironment;
		}

		var self = this;

		this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
			trimBlocks: true,
			lstripBlocks: true
		});

		this._nunjucksEnvironment.addFilter("translate", function (str) {
			return self.translate(str);
		});

		return this._nunjucksEnvironment;
	},

	/**
	 * Called when a socket notification arrives.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	socketNotificationReceived: function (notification, payload) {
		Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
	},

	/*
	 * Called when the module is hidden.
	 */
	suspend: function () {
		Log.log(this.name + " is suspended.");
	},

	/*
	 * Called when the module is shown.
	 */
	resume: function () {
		Log.log(this.name + " is resumed.");
	},

	/*********************************************
	 * The methods below don"t need subclassing. *
	 *********************************************/

	/**
	 * Set the module data.
	 *
	 * @param {Module} data The module data
	 */
	setData: function (data) {
		this.data = data;
		this.name = data.name;
		this.identifier = data.identifier;
		this.hidden = false;

		this.setConfig(data.config, data.configDeepMerge);
	},

	/**
	 * Set the module config and combine it with the module defaults.
	 *
	 * @param {object} config The combined module config.
	 * @param {boolean} config Merge module config in deep.
	 */
	setConfig: function (config, deep) {
		this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
	},

	/**
	 * Returns a socket object. If it doesn't exist, it's created.
	 * It also registers the notification callback.
	 *
	 * @returns {MMSocket} a socket object
	 */
	socket: function () {
		if (typeof this._socket === "undefined") {
			this._socket = new MMSocket(this.name);
		}

		var self = this;
		this._socket.setNotificationCallback(function (notification, payload) {
			self.socketNotificationReceived(notification, payload);
		});

		return this._socket;
	},

	/**
	 * Retrieve the path to a module file.
	 *
	 * @param {string} file Filename
	 * @returns {string} the file path
	 */
	file: function (file) {
		return (this.data.path + "/" + file).replace("//", "/");
	},

	/**
	 * Load all required stylesheets by requesting the MM object to load the files.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadStyles: function (callback) {
		this.loadDependencies("getStyles", callback);
	},

	/**
	 * Load all required scripts by requesting the MM object to load the files.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadScripts: function (callback) {
		this.loadDependencies("getScripts", callback);
	},

	/**
	 * Helper method to load all dependencies.
	 *
	 * @param {string} funcName Function name to call to get scripts or styles.
	 * @param {Function} callback Function called when done.
	 */
	loadDependencies: function (funcName, callback) {
		var self = this;
		var dependencies = this[funcName]();

		var loadNextDependency = function () {
			if (dependencies.length > 0) {
				var nextDependency = dependencies[0];
				Loader.loadFile(nextDependency, self, function () {
					dependencies = dependencies.slice(1);
					loadNextDependency();
				});
			} else {
				callback();
			}
		};

		loadNextDependency();
	},

	/**
	 * Load all translations.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadTranslations: function (callback) {
		var self = this;
		var translations = this.getTranslations();
		var lang = config.language.toLowerCase();

		// The variable `first` will contain the first
		// defined translation after the following line.
		for (var first in translations) {
			break;
		}

		if (translations) {
			var translationFile = translations[lang] || undefined;
			var translationsFallbackFile = translations[first];

			// If a translation file is set, load it and then also load the fallback translation file.
			// Otherwise only load the fallback translation file.
			if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
				Translator.load(self, translationFile, false, function () {
					Translator.load(self, translationsFallbackFile, true, callback);
				});
			} else {
				Translator.load(self, translationsFallbackFile, true, callback);
			}
		} else {
			callback();
		}
	},

	/**
	 * Request the translation for a given key with optional variables and default value.
	 *
	 * @param {string} key The key of the string to translate
	 * @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
	 * @param {string} [defaultValue] The default value with variables.
	 * @returns {string} the translated key
	 */
	translate: function (key, defaultValueOrVariables, defaultValue) {
		if (typeof defaultValueOrVariables === "object") {
			return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
		}
		return Translator.translate(this, key) || defaultValueOrVariables || "";
	},

	/**
	 * Request an (animated) update of the module.
	 *
	 * @param {number} [speed] The speed of the animation.
	 */
	updateDom: function (speed) {
		MM.updateDom(this, speed);
	},

	/**
	 * Send a notification to all modules.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	sendNotification: function (notification, payload) {
		MM.sendNotification(notification, payload, this);
	},

	/**
	 * Send a socket notification to the node helper.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	sendSocketNotification: function (notification, payload) {
		this.socket().sendNotification(notification, payload);
	},

	/**
	 * Hide this module.
	 *
	 * @param {number} speed The speed of the hide animation.
	 * @param {Function} callback Called when the animation is done.
	 * @param {object} [options] Optional settings for the hide method.
	 */
	hide: function (speed, callback, options) {
		if (typeof callback === "object") {
			options = callback;
			callback = function () {};
		}

		callback = callback || function () {};
		options = options || {};

		var self = this;
		MM.hideModule(
			self,
			speed,
			function () {
				self.suspend();
				callback();
			},
			options
		);
	},

	/**
	 * Show this module.
	 *
	 * @param {number} speed The speed of the show animation.
	 * @param {Function} callback Called when the animation is done.
	 * @param {object} [options] Optional settings for the show method.
	 */
	show: function (speed, callback, options) {
		if (typeof callback === "object") {
			options = callback;
			callback = function () {};
		}

		callback = callback || function () {};
		options = options || {};

		var self = this;
		MM.showModule(this, speed, function () {
			self.resume();
			callback();
		}, options);
	}
});

/** Merging MagicMirror (or other) default/config script
 * merge 2 objects or/with array
 * using:
 * -------
 * this.config = configMerge({}, this.defaults, this.config)
 * -------
 * arg1: initial objet
 * arg2: config model
 * arg3: config to merge
 * -------
 * why using it ?
 * Object.assign() function don't to all job
 * it don't merge all thing in deep
 * -> object in object and array is not merging
 * -------
 * @bugsounet
 * @Todo: idea of Mich determinate what do you want to merge or not
 */

function configMerge(result) {
	var stack = Array.prototype.slice.call(arguments, 1);
	var item;
	var key;
	while (stack.length) {
		item = stack.shift();
		for (key in item) {
			if (item.hasOwnProperty(key)) {
				if (typeof result[key] === "object" && result[key] && Object.prototype.toString.call(result[key]) !== "[object Array]") {
					if (typeof item[key] === "object" && item[key] !== null) {
						result[key] = configMerge({}, result[key], item[key]);
					} else {
						result[key] = item[key];
					}
				} else {
					result[key] = item[key];
				}
			}
		}
	}
	return result;
}

Module.definitions = {};

Module.create = function (name) {
	// Make sure module definition is available.
	if (!Module.definitions[name]) {
		return;
	}

	var moduleDefinition = Module.definitions[name];
	var clonedDefinition = cloneObject(moduleDefinition);

	// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
	var ModuleClass = Module.extend(clonedDefinition);

	return new ModuleClass();
};

Module.register = function (name, moduleDefinition) {
	if (moduleDefinition.requiresVersion) {
		Log.log("Check MagicMirror version for module '" + name + "' - Minimum version:  " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
		if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
			Log.log("Version is ok!");
		} else {
			Log.log("Version is incorrect. Skip module: '" + name + "'");
			return;
		}
	}
	Log.log("Module registered: " + name);
	Module.definitions[name] = moduleDefinition;
};

/**
 * Compare two semantic version numbers and return the difference.
 *
 * @param {string} a Version number a.
 * @param {string} b Version number b.
 * @returns {number} A positive number if a is larger than b, a negative
 * number if a is smaller and 0 if they are the same
 */
function cmpVersions(a, b) {
	var i, diff;
	var regExStrip0 = /(\.0+)+$/;
	var segmentsA = a.replace(regExStrip0, "").split(".");
	var segmentsB = b.replace(regExStrip0, "").split(".");
	var l = Math.min(segmentsA.length, segmentsB.length);

	for (i = 0; i < l; i++) {
		diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
		if (diff) {
			return diff;
		}
	}
	return segmentsA.length - segmentsB.length;
}
<commit_msg>Cleanup jsdoc<commit_after>/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */

/* Magic Mirror
 * Module Blueprint.
 * @typedef {Object} Module
 *
 * By Michael Teeuw https://michaelteeuw.nl
 * MIT Licensed.
 *
 */
var Module = Class.extend({
	/*********************************************************
	 * All methods (and properties) below can be subclassed. *
	 *********************************************************/

	// Set the minimum MagicMirror module version for this module.
	requiresVersion: "2.0.0",

	// Module config defaults.
	defaults: {},

	// Timer reference used for showHide animation callbacks.
	showHideTimer: null,

	// Array to store lockStrings. These strings are used to lock
	// visibility when hiding and showing module.
	lockStrings: [],

	// Storage of the nunjuck Environment,
	// This should not be referenced directly.
	// Use the nunjucksEnvironment() to get it.
	_nunjucksEnvironment: null,

	/**
	 * Called when the module is instantiated.
	 */
	init: function () {
		//Log.log(this.defaults);
	},

	/**
	 * Called when the module is started.
	 */
	start: function () {
		Log.info("Starting module: " + this.name);
	},

	/**
	 * Returns a list of scripts the module requires to be loaded.
	 *
	 * @returns {string[]} An array with filenames.
	 */
	getScripts: function () {
		return [];
	},

	/**
	 * Returns a list of stylesheets the module requires to be loaded.
	 *
	 * @returns {string[]} An array with filenames.
	 */
	getStyles: function () {
		return [];
	},

	/**
	 * Returns a map of translation files the module requires to be loaded.
	 *
	 * return Map<String, String> -
	 *
	 * @returns {*} A map with langKeys and filenames.
	 */
	getTranslations: function () {
		return false;
	},

	/**
	 * Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
	 * This method can to be subclassed if the module wants to display info on the mirror.
	 * Alternatively, the getTemplate method could be subclassed.
	 *
	 * @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
	 */
	getDom: function () {
		var self = this;
		return new Promise(function (resolve) {
			var div = document.createElement("div");
			var template = self.getTemplate();
			var templateData = self.getTemplateData();

			// Check to see if we need to render a template string or a file.
			if (/^.*((\.html)|(\.njk))$/.test(template)) {
				// the template is a filename
				self.nunjucksEnvironment().render(template, templateData, function (err, res) {
					if (err) {
						Log.error(err);
					}

					div.innerHTML = res;

					resolve(div);
				});
			} else {
				// the template is a template string.
				div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);

				resolve(div);
			}
		});
	},

	/**
	 * Generates the header string which needs to be displayed if a user has a header configured for this module.
	 * This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
	 * This method needs to be subclassed if the module wants to display modified headers on the mirror.
	 *
	 * @returns {string} The header to display above the header.
	 */
	getHeader: function () {
		return this.data.header;
	},

	/**
	 * Returns the template for the module which is used by the default getDom implementation.
	 * This method needs to be subclassed if the module wants to use a template.
	 * It can either return a template sting, or a template filename.
	 * If the string ends with '.html' it's considered a file from within the module's folder.
	 *
	 * @returns {string} The template string of filename.
	 */
	getTemplate: function () {
		return '
' + this.name + '

' + this.identifier + "
";
	},

	/**
	 * Returns the data to be used in the template.
	 * This method needs to be subclassed if the module wants to use a custom data.
	 *
	 * @returns {object} The data for the template
	 */
	getTemplateData: function () {
		return {};
	},

	/**
	 * Called by the Magic Mirror core when a notification arrives.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 * @param {Module} sender The module that sent the notification.
	 */
	notificationReceived: function (notification, payload, sender) {
		if (sender) {
			// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
		} else {
			// Log.log(this.name + " received a system notification: " + notification);
		}
	},

	/**
	 * Returns the nunjucks environment for the current module.
	 * The environment is checked in the _nunjucksEnvironment instance variable.
	 *
	 * @returns {object} The Nunjucks Environment
	 */
	nunjucksEnvironment: function () {
		if (this._nunjucksEnvironment !== null) {
			return this._nunjucksEnvironment;
		}

		var self = this;

		this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
			trimBlocks: true,
			lstripBlocks: true
		});

		this._nunjucksEnvironment.addFilter("translate", function (str) {
			return self.translate(str);
		});

		return this._nunjucksEnvironment;
	},

	/**
	 * Called when a socket notification arrives.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	socketNotificationReceived: function (notification, payload) {
		Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
	},

	/*
	 * Called when the module is hidden.
	 */
	suspend: function () {
		Log.log(this.name + " is suspended.");
	},

	/*
	 * Called when the module is shown.
	 */
	resume: function () {
		Log.log(this.name + " is resumed.");
	},

	/*********************************************
	 * The methods below don"t need subclassing. *
	 *********************************************/

	/**
	 * Set the module data.
	 *
	 * @param {Module} data The module data
	 */
	setData: function (data) {
		this.data = data;
		this.name = data.name;
		this.identifier = data.identifier;
		this.hidden = false;

		this.setConfig(data.config, data.configDeepMerge);
	},

	/**
	 * Set the module config and combine it with the module defaults.
	 *
	 * @param {object} config The combined module config.
	 * @param {boolean} deep Merge module config in deep.
	 */
	setConfig: function (config, deep) {
		this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
	},

	/**
	 * Returns a socket object. If it doesn't exist, it's created.
	 * It also registers the notification callback.
	 *
	 * @returns {MMSocket} a socket object
	 */
	socket: function () {
		if (typeof this._socket === "undefined") {
			this._socket = new MMSocket(this.name);
		}

		var self = this;
		this._socket.setNotificationCallback(function (notification, payload) {
			self.socketNotificationReceived(notification, payload);
		});

		return this._socket;
	},

	/**
	 * Retrieve the path to a module file.
	 *
	 * @param {string} file Filename
	 * @returns {string} the file path
	 */
	file: function (file) {
		return (this.data.path + "/" + file).replace("//", "/");
	},

	/**
	 * Load all required stylesheets by requesting the MM object to load the files.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadStyles: function (callback) {
		this.loadDependencies("getStyles", callback);
	},

	/**
	 * Load all required scripts by requesting the MM object to load the files.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadScripts: function (callback) {
		this.loadDependencies("getScripts", callback);
	},

	/**
	 * Helper method to load all dependencies.
	 *
	 * @param {string} funcName Function name to call to get scripts or styles.
	 * @param {Function} callback Function called when done.
	 */
	loadDependencies: function (funcName, callback) {
		var self = this;
		var dependencies = this[funcName]();

		var loadNextDependency = function () {
			if (dependencies.length > 0) {
				var nextDependency = dependencies[0];
				Loader.loadFile(nextDependency, self, function () {
					dependencies = dependencies.slice(1);
					loadNextDependency();
				});
			} else {
				callback();
			}
		};

		loadNextDependency();
	},

	/**
	 * Load all translations.
	 *
	 * @param {Function} callback Function called when done.
	 */
	loadTranslations: function (callback) {
		var self = this;
		var translations = this.getTranslations();
		var lang = config.language.toLowerCase();

		// The variable `first` will contain the first
		// defined translation after the following line.
		for (var first in translations) {
			break;
		}

		if (translations) {
			var translationFile = translations[lang] || undefined;
			var translationsFallbackFile = translations[first];

			// If a translation file is set, load it and then also load the fallback translation file.
			// Otherwise only load the fallback translation file.
			if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
				Translator.load(self, translationFile, false, function () {
					Translator.load(self, translationsFallbackFile, true, callback);
				});
			} else {
				Translator.load(self, translationsFallbackFile, true, callback);
			}
		} else {
			callback();
		}
	},

	/**
	 * Request the translation for a given key with optional variables and default value.
	 *
	 * @param {string} key The key of the string to translate
	 * @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
	 * @param {string} [defaultValue] The default value with variables.
	 * @returns {string} the translated key
	 */
	translate: function (key, defaultValueOrVariables, defaultValue) {
		if (typeof defaultValueOrVariables === "object") {
			return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
		}
		return Translator.translate(this, key) || defaultValueOrVariables || "";
	},

	/**
	 * Request an (animated) update of the module.
	 *
	 * @param {number} [speed] The speed of the animation.
	 */
	updateDom: function (speed) {
		MM.updateDom(this, speed);
	},

	/**
	 * Send a notification to all modules.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	sendNotification: function (notification, payload) {
		MM.sendNotification(notification, payload, this);
	},

	/**
	 * Send a socket notification to the node helper.
	 *
	 * @param {string} notification The identifier of the notification.
	 * @param {*} payload The payload of the notification.
	 */
	sendSocketNotification: function (notification, payload) {
		this.socket().sendNotification(notification, payload);
	},

	/**
	 * Hide this module.
	 *
	 * @param {number} speed The speed of the hide animation.
	 * @param {Function} callback Called when the animation is done.
	 * @param {object} [options] Optional settings for the hide method.
	 */
	hide: function (speed, callback, options) {
		if (typeof callback === "object") {
			options = callback;
			callback = function () {};
		}

		callback = callback || function () {};
		options = options || {};

		var self = this;
		MM.hideModule(
			self,
			speed,
			function () {
				self.suspend();
				callback();
			},
			options
		);
	},

	/**
	 * Show this module.
	 *
	 * @param {number} speed The speed of the show animation.
	 * @param {Function} callback Called when the animation is done.
	 * @param {object} [options] Optional settings for the show method.
	 */
	show: function (speed, callback, options) {
		if (typeof callback === "object") {
			options = callback;
			callback = function () {};
		}

		callback = callback || function () {};
		options = options || {};

		var self = this;
		MM.showModule(
			this,
			speed,
			function () {
				self.resume();
				callback();
			},
			options
		);
	}
});

/**
 * Merging MagicMirror (or other) default/config script by @bugsounet
 * Merge 2 objects or/with array
 *
 * Usage:
 * -------
 * this.config = configMerge({}, this.defaults, this.config)
 * -------
 * arg1: initial object
 * arg2: config model
 * arg3: config to merge
 * -------
 * why using it ?
 * Object.assign() function don't to all job
 * it don't merge all thing in deep
 * -> object in object and array is not merging
 * -------
 *
 * Todo: idea of Mich determinate what do you want to merge or not
 *
 * @param {object} result the initial object
 * @returns {object} the merged config
 */
function configMerge(result) {
	var stack = Array.prototype.slice.call(arguments, 1);
	var item;
	var key;
	while (stack.length) {
		item = stack.shift();
		for (key in item) {
			if (item.hasOwnProperty(key)) {
				if (typeof result[key] === "object" && result[key] && Object.prototype.toString.call(result[key]) !== "[object Array]") {
					if (typeof item[key] === "object" && item[key] !== null) {
						result[key] = configMerge({}, result[key], item[key]);
					} else {
						result[key] = item[key];
					}
				} else {
					result[key] = item[key];
				}
			}
		}
	}
	return result;
}

Module.definitions = {};

Module.create = function (name) {
	// Make sure module definition is available.
	if (!Module.definitions[name]) {
		return;
	}

	var moduleDefinition = Module.definitions[name];
	var clonedDefinition = cloneObject(moduleDefinition);

	// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
	var ModuleClass = Module.extend(clonedDefinition);

	return new ModuleClass();
};

Module.register = function (name, moduleDefinition) {
	if (moduleDefinition.requiresVersion) {
		Log.log("Check MagicMirror version for module '" + name + "' - Minimum version:  " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
		if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
			Log.log("Version is ok!");
		} else {
			Log.log("Version is incorrect. Skip module: '" + name + "'");
			return;
		}
	}
	Log.log("Module registered: " + name);
	Module.definitions[name] = moduleDefinition;
};

/**
 * Compare two semantic version numbers and return the difference.
 *
 * @param {string} a Version number a.
 * @param {string} b Version number b.
 * @returns {number} A positive number if a is larger than b, a negative
 * number if a is smaller and 0 if they are the same
 */
function cmpVersions(a, b) {
	var i, diff;
	var regExStrip0 = /(\.0+)+$/;
	var segmentsA = a.replace(regExStrip0, "").split(".");
	var segmentsB = b.replace(regExStrip0, "").split(".");
	var l = Math.min(segmentsA.length, segmentsB.length);

	for (i = 0; i < l; i++) {
		diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
		if (diff) {
			return diff;
		}
	}
	return segmentsA.length - segmentsB.length;
}
<|endoftext|>


<commit_before>/*
This file is part of Kronekeeper, a web based application for
recording and managing wiring frame records.

Copyright (C) 2016 NP Broadcast Limited

Kronekeeper is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Kronekeeper is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Kronekeeper.  If not, see <http://www.gnu.org/licenses/>.

*/

define([
	'block/jumper_select',
	'block/highlight',
	'backbone',
        'jquery',
	'jqueryui'
], function (
	jumper_select,
	highlight
) {
        'use strict';

	var Jumper_Model = Backbone.Model.extend({

		defaults: function() {
			return {
				designation: null,
				id: null,
				is_simple_jumper: false,
				wires: []
			};
		},

		initialize: function(attributes, options) {
			this.circuit = options.circuit;
		},

		parse: function(response, options) {

			var data = response.data;

			/* Sanity check - a jumper should always have at least one wire */
			if(!(data.wires.length >= 1)) {
				alert("ERROR: jumper has no associated wires, therefore no connections!");
			}

			/* Assumption is that all wires lead to the same destination circuit
			 * We've no UI to create a jumper other than this, but still, we trap this
			 * in case we do something crazy in future and forget to update this part of the UI
			 */
			var mismatched_wire = data.wires.find(function(wire) {
				return (wire.b_circuit_id != data.wires[0].b_circuit_id);
			});
			if(mismatched_wire) {
				alert(
					"UNEXPECTED CONDITION: jumper has wires terminating on " +
					"different circuits. Not all connections are shown"
				);
			}

			/* This is the data finally used to construct the new Model */
			return {
				id: data.jumper_id,
				is_simple_jumper: data.is_simple_jumper,
				wires: data.wires,
				designation: data.wires[0].b_circuit_full_designation
			};
		},

		urlRoot: '/api/jumper'
	});

	var Jumper_View = Backbone.View.extend({

		tagName: 'td',
		className: 'jumper',
		template: _.template( $('#active_jumper_cell_template').html() ),

		events: {
			'input' : 'highlight_change',
			'change' : 'jumper_change',
			'keypress' : 'reset_on_escape_key'
		},

		initialize: function(attributes) {
			this.listenTo(
				this.model.circuit.collection,
				"jumper_deleted",
				this.jumper_deleted
			);
			this.listenTo(
				this.model,
				'sync',
				this.model_synced
			);
		},

		render: function() {
			console.log("jumper", this.model.id, "render on circuit", this.model.circuit.id);
			var json = this.model.toJSON();
			this.$el.html(this.template(json));
			return this;
		},

		highlight_change: function(e) {
			if(e.target.value != this.model.get("designation")) {
				e.target.parentNode.classList.add('change_pending');
			}
			else {
				e.target.parentNode.classList.remove('change_pending');
			}
		},

		reset_on_escape_key: function(e) {
			if(e.keyCode == 27) {
				e.target.value = this.model.get("designation");
				e.target.parentNode.classList.remove('change_pending');
			}
		},

		jumper_change: function(e) {
			if(e.target.value == this.model.get("designation")) {
				/* No change - nothing to do */
				console.log("jumper unchanged");
			}
			else if (e.target.value == '') {
				this.jumper_remove();
			}
			else {
				console.log("jumper changed");
				var jumper_view = this;
				jumper_select.display({
					circuit_id: this.model.circuit.id,
					jumper_id: this.model.id,
					destination_designation: e.target.value,
					cancel_action: function() {
						console.log("cancel action");
						e.target.value = jumper_view.model.get("designation");
						e.target.parentNode.classList.remove('change_pending');
						jumper_view.$el.effect("highlight", {}, highlight.duration);
					},
					success_action: function(data) {
						console.log("success action: ", data);

						if(data.deleted_jumper_id) {
							/* Propagate deletion to other jumpers displayed on this block */
							jumper_view.model.circuit.collection.trigger("jumper_deleted", data.deleted_jumper_id);
						}

						/* Update our own model and re-render */
						jumper_view.model.set(
							jumper_view.model.parse({
								data: data.jumper_info
							})
						);
						e.target.parentNode.classList.remove('change_pending');
						jumper_view.render();
						jumper_view.$el.effect("highlight", highlight.green, highlight.duration);

						/* Trigger update and re-render for other affected circuits */
						propagate_circuit_changes(data.jumper_info.wires);
					}
				});

				function propagate_circuit_changes(wires) {

						/* Build list of changed circuits, so we only trigger one event for each */
						var changed_circuits = [];
						wires.forEach(function(wire) {

							/* Don't trigger an event on ourselves */
							if(wire.b_circuit_id != wire.a_circuit_id) {
								changed_circuits[wire.b_circuit_id] = true;
							}
						});

						/* Trigger an event for each affected circuit, so they can reload their jumper models */
						changed_circuits.forEach(function(changed, circuit_id) {
							console.log("propagating jumper change for circuit_id ", circuit_id);
							jumper_view.model.circuit.collection.trigger("circuit_jumper_change",circuit_id);
						});
				}
			}
		},

		jumper_remove: function() {

			this.model.destroy({
				success: function(model, response, options) {
					console.log("jumper removed OK");
					model.circuit.collection.trigger("jumper_deleted", model.id);
				},
				error: function(model, response, options) {
					console.log("ERROR removing jumper");
					alert("ERROR removing jumper");
				}
			});
		},

		jumper_deleted: function(deleted_jumper_id) {

			/* Event handler that responds to another jumper being deleted.
			 * If we are the other end of the deleted jumper, we'll have the
			 * same id, but the text input field won't have been cleared.
			 */
			if(deleted_jumper_id == this.model.id) {
				console.log("jumper_deleted on circuit_id:", this.model.circuit.id, "jumper_id:", this.model.id);
				this.model.set(this.model.defaults());
				this.render();
				this.model_synced();
			}
		},

		model_synced: function(model, response, options) {
			/* Clear field highlighting and flash green to indicate successful save
			 * Server returns the changed fields to confirm which have been updated
			 */
			console.log("jumper model synced");
			this.$el.removeClass('change_pending');
			this.$el.effect("highlight", highlight.green, highlight.duration);
		}

	});

	console.log("loaded block/jumper.js");

	/* Expose public methods */
	return {
		model: Jumper_Model,
		view: Jumper_View
	};
});

<commit_msg>Fix #22 Changing Jumper Destination results in empty Cell<commit_after>/*
This file is part of Kronekeeper, a web based application for
recording and managing wiring frame records.

Copyright (C) 2016 NP Broadcast Limited

Kronekeeper is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Kronekeeper is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Kronekeeper.  If not, see <http://www.gnu.org/licenses/>.

*/

define([
	'block/jumper_select',
	'block/highlight',
	'backbone',
        'jquery',
	'jqueryui'
], function (
	jumper_select,
	highlight
) {
        'use strict';

	var Jumper_Model = Backbone.Model.extend({

		defaults: function() {
			return {
				designation: null,
				id: null,
				is_simple_jumper: false,
				wires: []
			};
		},

		initialize: function(attributes, options) {
			this.circuit = options.circuit;
		},

		parse: function(response, options) {

			var data = response.data;

			/* Sanity check - a jumper should always have at least one wire */
			if(!(data.wires.length >= 1)) {
				alert("ERROR: jumper has no associated wires, therefore no connections!");
			}

			/* Assumption is that all wires lead to the same destination circuit
			 * We've no UI to create a jumper other than this, but still, we trap this
			 * in case we do something crazy in future and forget to update this part of the UI
			 */
			var mismatched_wire = data.wires.find(function(wire) {
				return (wire.b_circuit_id != data.wires[0].b_circuit_id);
			});
			if(mismatched_wire) {
				alert(
					"UNEXPECTED CONDITION: jumper has wires terminating on " +
					"different circuits. Not all connections are shown"
				);
			}

			/* This is the data finally used to construct the new Model */
			return {
				id: data.jumper_id,
				is_simple_jumper: data.is_simple_jumper,
				wires: data.wires,
				designation: data.wires[0].b_circuit_full_designation
			};
		},

		urlRoot: '/api/jumper'
	});

	var Jumper_View = Backbone.View.extend({

		tagName: 'td',
		className: 'jumper',
		template: _.template( $('#active_jumper_cell_template').html() ),

		events: {
			'input' : 'highlight_change',
			'change' : 'jumper_change',
			'keypress' : 'reset_on_escape_key'
		},

		initialize: function(attributes) {
			this.listenTo(
				this.model.circuit.collection,
				"jumper_deleted",
				this.jumper_deleted
			);
			this.listenTo(
				this.model,
				'sync',
				this.model_synced
			);
		},

		render: function() {
			console.log("jumper", this.model.id, "render on circuit", this.model.circuit.id);
			var json = this.model.toJSON();
			this.$el.html(this.template(json));
			return this;
		},

		highlight_change: function(e) {
			if(e.target.value != this.model.get("designation")) {
				e.target.parentNode.classList.add('change_pending');
			}
			else {
				e.target.parentNode.classList.remove('change_pending');
			}
		},

		reset_on_escape_key: function(e) {
			if(e.keyCode == 27) {
				e.target.value = this.model.get("designation");
				e.target.parentNode.classList.remove('change_pending');
			}
		},

		jumper_change: function(e) {
			if(e.target.value == this.model.get("designation")) {
				/* No change - nothing to do */
				console.log("jumper unchanged");
			}
			else if (e.target.value == '') {
				this.jumper_remove();
			}
			else {
				console.log("jumper changed");
				var jumper_view = this;
				jumper_select.display({
					circuit_id: this.model.circuit.id,
					jumper_id: this.model.id,
					destination_designation: e.target.value,
					cancel_action: function() {
						console.log("cancel action");
						e.target.value = jumper_view.model.get("designation");
						jumper_view.el.classList.remove('change_pending');
						jumper_view.$el.effect("highlight", {}, highlight.duration);
					},
					success_action: function(data) {
						console.log("success action: ", data);

						if(data.deleted_jumper_id) {
							/* Propagate deletion to other jumpers displayed on this block */
							jumper_view.model.circuit.collection.trigger("jumper_deleted", data.deleted_jumper_id);
						}

						/* Update our own model and re-render */
						jumper_view.model.set(
							jumper_view.model.parse({
								data: data.jumper_info
							})
						);

						jumper_view.el.classList.remove('change_pending');
						jumper_view.render();
						jumper_view.$el.effect("highlight", highlight.green, highlight.duration);

						/* Trigger update and re-render for other affected circuits */
						propagate_circuit_changes(data.jumper_info.wires);
					}
				});

				function propagate_circuit_changes(wires) {

						/* Build list of changed circuits, so we only trigger one event for each */
						var changed_circuits = [];
						wires.forEach(function(wire) {

							/* Don't trigger an event on ourselves */
							if(wire.b_circuit_id != wire.a_circuit_id) {
								changed_circuits[wire.b_circuit_id] = true;
							}
						});

						/* Trigger an event for each affected circuit, so they can reload their jumper models */
						changed_circuits.forEach(function(changed, circuit_id) {
							console.log("propagating jumper change for circuit_id ", circuit_id);
							jumper_view.model.circuit.collection.trigger("circuit_jumper_change",circuit_id);
						});
				}
			}
		},

		jumper_remove: function() {

			this.model.destroy({
				success: function(model, response, options) {
					console.log("jumper removed OK");
					model.circuit.collection.trigger("jumper_deleted", model.id);
				},
				error: function(model, response, options) {
					console.log("ERROR removing jumper");
					alert("ERROR removing jumper");
				}
			});
		},

		jumper_deleted: function(deleted_jumper_id) {

			/* Event handler that responds to another jumper being deleted.
			 * If we are the other end of the deleted jumper, we'll have the
			 * same id, but the text input field won't have been cleared.
			 */
			if(deleted_jumper_id == this.model.id) {
				console.log("jumper_deleted on circuit_id:", this.model.circuit.id, "jumper_id:", this.model.id);
				this.model.set(this.model.defaults());
				this.render();
				this.model_synced();
			}
		},

		model_synced: function(model, response, options) {
			/* Clear field highlighting and flash green to indicate successful save
			 * Server returns the changed fields to confirm which have been updated
			 */
			console.log("jumper model synced");
			this.$el.removeClass('change_pending');
			this.$el.effect("highlight", highlight.green, highlight.duration);
		}

	});

	console.log("loaded block/jumper.js");

	/* Expose public methods */
	return {
		model: Jumper_Model,
		view: Jumper_View
	};
});

<|endoftext|>


<commit_before>'use strict';

var _ = require('../../../ui-base/_');
var ajax = require('../../../ui-base/ajax');
var Validation = require('../../../util/validation');

var template = require('./index.html');

/**
 * @class UIForm
 * @extend Validation
 * @param {object}      [options.data]                    = 绑定属性
 * @param {string}      [options.data.service]            => 全站异步获取source的接口地址
 * @param {string}      [options.data.class]              => 扩展样式
 * @param {boolean}     [options.data.inline='']          => 如果true,form.item按照inline-block排列
 * @param {string}      [options.data.sourcePath=data]    => 获取到select数据后,读取json数据的路径
 */
var UIForm = Validation.extend({
    name: 'ui.form',
    template: template,
    selectors: [],
    config: function (data) {

        _.extend(data, {
            service: null,
            sourcePath: 'data',
            class: ''
        });

        this.supr(data);
    },
    init: function() {
        this.supr();

        var $outer = this;
        do {
            if ($outer.$outer) {
                $outer = $outer.$outer;
            } else if ($outer.$parent) {
                $outer = $outer.$parent;
        }} while(!($outer instanceof Validation) && ($outer.$outer || $outer.$parent));

        if ($outer && $outer instanceof Validation) {
            $outer.controls.push(this);
        }
        this.initSelectorSource();

        this.adjustInline();
    },
    adjustInline: function() {
        // 如果inline, 那么设置ui.form中的所有form.item的labelCols为0,否则label上会有'g-col g-col-4'的默认样式
        this.$watch('inline', function(newValue) {
            if (!newValue) { return; }
            var controls = this.controls;
            controls.forEach(function($component) {
                if ($component.data.labelCols) {
                    $component.$update('labelCols', 0);
                }
            });
        });
    },
    initSelectorSource: function() {
        var controls = this.controls;
        this.selectors = controls.filter(function($formitem) {
            return !!$formitem.data.sourceKey;
        });

        if (!this.data.service || !this.selectors.length) { return; }

        this.__reqSource();
    },
    __getSourceKeys: function() {
        return this.selectors.map(function($formitem) {
            return $formitem.data.sourceKey;
        })
    },
    __reqSource: function() {
        var keys = this.__getSourceKeys();

        window.NEKSelects = window.NEKSelects || {};
        keys = keys.filter(function(key) { return !window.NEKSelects[key]; });

        this.selectors.forEach(function($formitem) {
            var key = $formitem.data.sourceKey;
            var source = window.NEKSelects[key] || [];
            this.__updateSource($formitem, key, source);
        }.bind(this));

        if (!keys.length) { return; }
        this.keys = keys;
        ajax.request({
            url: this.data.service,
            method: 'get',
            type: 'json',
            data: {
                keys:keys.join(',')
            },
            success: this.__cbReqSource.bind(this)
        })
    },
    __cbReqSource: function(json) {
        var keys = this.keys,
            path = this.data.sourcePath,
            result = path === '' ? json : json[path];

        result = result || {};

        this.selectors.forEach(function($formitem) {
            var key = $formitem.data.sourceKey;
            var source = result[key] || [];
            this.__updateSource($formitem, key, source);
        }.bind(this));
    },
    __updateSource: function($formitem, key, source) {
        var $selectItem = $formitem.controls[0];
        window.NEKSelects[key] = _.clone(source);

        /* 三种情况不给组件赋值:1. form.item下面没有选项组件; 2. source为空 3. 选项组件的source属性已经有值 */
        if (!$selectItem || !source.length || $selectItem.data.source.length) return;
        $selectItem.data.source =  _.clone(source);
        $selectItem.$update();
    }
});

module.exports = UIForm;<commit_msg>fix(ui.select): fix sourceKey invalide<commit_after>'use strict';

var _ = require('../../../ui-base/_');
var ajax = require('../../../ui-base/ajax');
var Validation = require('../../../util/validation');

var template = require('./index.html');

/**
 * @class UIForm
 * @extend Validation
 * @param {object}      [options.data]                    = 绑定属性
 * @param {string}      [options.data.service]            => 全站异步获取source的接口地址
 * @param {string}      [options.data.class]              => 扩展样式
 * @param {boolean}     [options.data.inline='']          => 如果true,form.item按照inline-block排列
 * @param {string}      [options.data.sourcePath=data]    => 获取到select数据后,读取json数据的路径
 */
var UIForm = Validation.extend({
    name: 'ui.form',
    template: template,
    selectors: [],
    config: function (data) {

        _.extend(data, {
            service: null,
            sourcePath: 'data',
            class: ''
        });

        this.supr(data);
    },
    init: function() {
        this.supr();

        var $outer = this;
        do {
            if ($outer.$outer) {
                $outer = $outer.$outer;
            } else if ($outer.$parent) {
                $outer = $outer.$parent;
        }} while(!($outer instanceof Validation) && ($outer.$outer || $outer.$parent));

        if ($outer && $outer instanceof Validation) {
            $outer.controls.push(this);
        }
        this.initSelectorSource();

        this.adjustInline();
    },
    adjustInline: function() {
        // 如果inline, 那么设置ui.form中的所有form.item的labelCols为0,否则label上会有'g-col g-col-4'的默认样式
        this.$watch('inline', function(newValue) {
            if (!newValue) { return; }
            var controls = this.controls;
            controls.forEach(function($component) {
                if ($component.data.labelCols) {
                    $component.$update('labelCols', 0);
                }
            });
        });
    },
    initSelectorSource: function() {
        var controls = this.controls;
        this.selectors = controls.filter(function($formitem) {
            return !!$formitem.data.sourceKey;
        });

        if (!this.data.service || !this.selectors.length) { return; }

        this.__reqSource();
    },
    __getSourceKeys: function() {
        return this.selectors.map(function($formitem) {
            return $formitem.data.sourceKey;
        })
    },
    __reqSource: function() {
        var keys = this.__getSourceKeys();

        window.NEKSelects = window.NEKSelects || {};
        keys = keys.filter(function(key) { return !window.NEKSelects[key]; });

        this.selectors.forEach(function($formitem) {
            var key = $formitem.data.sourceKey;
            var source = window.NEKSelects[key] || [];
            this.__updateSource($formitem, key, source);
        }.bind(this));

        if (!keys.length) { return; }
        this.keys = keys;
        ajax.request({
            url: this.data.service,
            method: 'get',
            type: 'json',
            data: {
                keys:keys.join(',')
            },
            success: this.__cbReqSource.bind(this)
        })
    },
    __cbReqSource: function(json) {
        var keys = this.keys,
            path = this.data.sourcePath,
            result = path === '' ? json : json[path];

        result = result || {};

        this.selectors.forEach(function($formitem) {
            var key = $formitem.data.sourceKey;
            var source = result[key] || [];
            this.__updateSource($formitem, key, source);
        }.bind(this));
    },
    __updateSource: function($formitem, key, source) {
        var $selectItem = $formitem.controls[0];

        /* 三种情况不给组件赋值:1. form.item下面没有选项组件; 2. source为空 3. 选项组件的source属性已经有值 */
        if (!$selectItem || !source.length || $selectItem.data.source.length) return;
        $selectItem.data.source =  _.clone(source);
        window.NEKSelects[key] = _.clone(source);
        $selectItem.$update();
    }
});

module.exports = UIForm;<|endoftext|>