<commit_before>/**
 * Provides Form and input features.
 *
 * @module furnace
 * @submodule furnace-forms
 */
import Ember from 'ember';
import Lookup from 'furnace-forms/utils/lookup-class';
import Actions from 'furnace-forms/mixins/controls/control-actions';
import I18n from 'furnace-i18n';
/**
 * Abstract control component proxy
 *
 * @class Abstract
 * @namespace Furnace.Forms.Controls
 * @extends Ember.ObjectProxy
 * @private
 */
export default Ember.Object.extend(Ember.ActionHandler,{

	_name: null,

	_form: null,

	_panel: null,

	_decoratorName: null,

	_decoratorType: null,

	_decorator: Ember.computed('_decoratorName,_decoratorType', {
		get : function() {
			Ember.assert('furnace-forms: '+this+' ('+this._name+') does not specify a decorator type or name',this.get('_decoratorName') && this.get('_decoratorType'));
			return this.get('_decoratorName')+'.'+this.get('_decoratorType')+'-decorator';
		},
		set : function(key,value) {
			return value;
		}
	}),

	decorator : Ember.computed.alias('_decorator').readOnly(),

	_for : null,

	caption: undefined,

	hasPrerequisites: null,

	_messagesSilent: false,

	actions: {
	},

	isEnabled: true,

	_enabled: true,

	setEnabled: function(enabled) {
		if(enabled!==this._enabled) {
			this.set('_enabled',enabled);
		}
		var isEnabled = this._enabled && (!this._panel || this._panel.get('isEnabled'));

		if(isEnabled!==this.get('isEnabled')) {
			if(isEnabled) {
				this.send('onEnabled');
			} else {
				this.send('onDisabled');
			}
			this.setFlag('isEnabled',isEnabled);
		}
	},

	_panelEnabledObserver:Ember.observer('_panel.isEnabled',function() {
		this.setEnabled(this._enabled);
	}),

	_valid: null,

	isValid: null,

	setValid: function(valid) {
		if(valid!==this._valid) {
			this.setFlag('isValid',valid);
			if(valid) {
				this.send('onValid');
			} else {
				this.send('onInvalid');
			}
			this.set('_valid',valid);
			this.notifyChange();
		}
	},

	store: Ember.computed.alias('target.store'),

	_dirty: false,

	isDirty: false,

	setDirty: function(dirty) {
		this._didReset=false;
		if(dirty!==this._dirty) {
			this.setFlag('isDirty',dirty);
			this.set('_dirty',dirty);
			this.notifyChange();
		}
	},

	_flags: null,

	setFlag:function(property,value) {
		if(this.get(property)!==value) {
			this.set(property,value);
			this._flags[property]=value;
			Ember.run.scheduleOnce('sync',this,this._updateFlags);
		}
	},

	_updateFlags: function() {
		for(var flag in this._flags) {
			this._components.invoke('set',flag,this._flags[flag]);
			delete this._flags[flag];
		}
	},

	init:function() {
		this._super();
		this._flags={};
		this.set('_controlMessages',Ember.A());
		this.set('_components',Ember.A());
		var control=this;
		// Initialize computed attributes
		this.constructor.eachComputedProperty(function(name, meta) {
			if (meta.type==='form-attr') {
				control.get(name);
			}
		});
		if(this._form) {
			this.set('_path',this._getPath());
			this.set('target',this._panel);
		}

		this._panelEnabledObserver();
		this._registerControl ? this._registerControl(this) : this._form._registerControl(this);
	},

	_apply: function() {

	},

	_didReset : false,

	_reset: function(modelChanged) {
// This breaks already set validation, our value might not change by a reset so may not trigger a new validation
		if(modelChanged) {
			this.setFlag('isValid',null);
			this.set('_valid',null);
		}
		this._didReset=true;
	},

	notifyChange: function() {
		if(this._panel) {
			Ember.run.scheduleOnce('sync',this,function() {
				this._panel.propertyDidChange(this._name);
			});
		}
	},

	_getPath: function() {
		// Temporary work arround to give lists a proper path
		// Temporary work arround to make sure forms dont get an absolute path
		if(!this['_model'] || this._isList || (this.get('_model')!==this.get('_panel._model') && !this._isForm)) {
			return (this.get('_panel._path') ? this.get('_panel._path')+ "." : '')+this.get('_name');
		} else if(this._panel && this.get('_model')===this.get('_panel._model')) {
			return this.get('_panel._path');
		}

	},

	hasModel : function() {
		var model=null;
		if(this['_model'])
			model = this.get('_model');
		else {
			if(this.getForm()) {
				model = this.getForm().get('_model');
			}
		}
		if(!model) {
			return false;
		}
		return true;
	},

	getModel : function(path) {
		var model=null;
		if(this['_model'])
			model = this.get('_model');
		else {
			if(this.getForm()) {
				model = this.getForm().get('_model');
			}
		}
		if(!model) {
			Ember.warn('Control '+this.toString()+' is trying to access the (form)model but it is not defined!',model,{id:'furnace-forms:control.model-missing'});
			return undefined;
		}
		if(path) {
			return model.get(path);
		}
		return model;
	},

	getFor : function(path) {
		return this.getModel(path);
	},

	getTarget : function() {
		if(this._form)
			return this.getForm().get('targetObject');
		return this.get('targetObject');
	},

	getPanel : function() {
		return this._panel;
	},

	getForm : function(path) {
		if(path) {
			return this._form.get(path);
		}
		return this._form;
	},

	_controlMessages : null,

	setMessages: function(messages,silent) {
		this._updateMessages(messages,this._controlMessages);
		if(this.get('hasPrerequisites')===false || this._components.length===0 || this._didReset)
			silent=true;
		this._didReset=false;
		this._messagesSilent=silent;
//		console.log("New messages for "+this+" Components: "+this._components.length);
		if(!silent) {
			Ember.run.once(this,function() {
				this._components.invoke('_controlMessageObserver');
			});
		}
	},

	_updateMessages: function(source,target) {
		var remove=Ember.A();
		target.forEach(function(item) {
			if(!source) {
				remove.pushObject(item);
			} else {
				var _messages=source.filterBy('type',item.type).filterBy('message',item.message);
				if(!_messages.length) {
					remove.pushObject(item);
				} else {

					_messages.forEach(function(message) {
						Ember.set(item,'attributes',message.attributes);
						source.removeObject(message);
					});
				}
			}
		});
		target.removeObjects(remove);
		if(source) {
			source.forEach(function(message) {
				target.pushObject(message);
			});
		}
	},

	_components : null,

	registerComponent:function(component) {
		this._components.pushObject(component);
		component.set('isValid',this.get('isValid'));
		component.set('isEnabled',this.get('isEnabled'));
		component.set('isDirty',this.get('isDirty'));
		if(!this._messagesSilent && this._controlMessages.length) {
//			console.log(component+" component registered to "+this+", updating messages");
			component._controlMessageObserver();
		}
		this.send('onComponent',component);

	},

	unregisterComponent:function(component) {
		this._components.removeObject(component);
	},

	getComponentClass : function() {
		Ember.assert('getComponentClass is no longer support',false);
		var component=this._decorator;
		if(typeof component ==="string") {
			component = Lookup.call(this,this._decoratorName,this._decoratorType);
		}
		return component;
	},

	getComponent : function() {
		var component=this.getComponentClass();
		if(!component)
			return null;
		return component.create({container:this.container,
								control:this,
								_debugContainerKey : component + this.get('_name')
								});
	},

	toString: function() {
		var string=this._super();
		return string.substring(0,string.length-1)+':'+this.get('_name')+'>';
	},

	willDestroy : function() {
	    if(!this.isDestroyed) {
		//this._unregisterControl ? this._unregisterControl(this) : this._form._unregisterControl(this);
	}
		this._super();
	},

	destroy: function() {
		this._super();
		this._unregisterControl ? this._unregisterControl(this) : this._form._unregisterControl(this);
		this.set('target',null);
		this.set('_panel',null);
		this.set('_form',null);
//		Ember.warn('Destroying control '+this.toString()+ ' while there are still components attached! '+ (this._components.length),this._components.length===0);
//		if(this._components.length!==0)
//			console.trace();
	},

}).reopenClass({
	decorator : function(decorator) {
		return this.reopen({
			_decoratorName : decorator
		});
	}
}).extend(Actions);<commit_msg>Fixed abstract control notifyChange scheduling<commit_after>/**
 * Provides Form and input features.
 *
 * @module furnace
 * @submodule furnace-forms
 */
import Ember from 'ember';
import Lookup from 'furnace-forms/utils/lookup-class';
import Actions from 'furnace-forms/mixins/controls/control-actions';
import I18n from 'furnace-i18n';
/**
 * Abstract control component proxy
 *
 * @class Abstract
 * @namespace Furnace.Forms.Controls
 * @extends Ember.ObjectProxy
 * @private
 */
export default Ember.Object.extend(Ember.ActionHandler,{

	_name: null,

	_form: null,

	_panel: null,

	_decoratorName: null,

	_decoratorType: null,

	_decorator: Ember.computed('_decoratorName,_decoratorType', {
		get : function() {
			Ember.assert('furnace-forms: '+this+' ('+this._name+') does not specify a decorator type or name',this.get('_decoratorName') && this.get('_decoratorType'));
			return this.get('_decoratorName')+'.'+this.get('_decoratorType')+'-decorator';
		},
		set : function(key,value) {
			return value;
		}
	}),

	decorator : Ember.computed.alias('_decorator').readOnly(),

	_for : null,

	caption: undefined,

	hasPrerequisites: null,

	_messagesSilent: false,

	actions: {
	},

	isEnabled: true,

	_enabled: true,

	setEnabled: function(enabled) {
		if(enabled!==this._enabled) {
			this.set('_enabled',enabled);
		}
		var isEnabled = this._enabled && (!this._panel || this._panel.get('isEnabled'));

		if(isEnabled!==this.get('isEnabled')) {
			if(isEnabled) {
				this.send('onEnabled');
			} else {
				this.send('onDisabled');
			}
			this.setFlag('isEnabled',isEnabled);
		}
	},

	_panelEnabledObserver:Ember.observer('_panel.isEnabled',function() {
		this.setEnabled(this._enabled);
	}),

	_valid: null,

	isValid: null,

	setValid: function(valid) {
		if(valid!==this._valid) {
			this.setFlag('isValid',valid);
			if(valid) {
				this.send('onValid');
			} else {
				this.send('onInvalid');
			}
			this.set('_valid',valid);
			this.notifyChange();
		}
	},

	store: Ember.computed.alias('target.store'),

	_dirty: false,

	isDirty: false,

	setDirty: function(dirty) {
		this._didReset=false;
		if(dirty!==this._dirty) {
			this.setFlag('isDirty',dirty);
			this.set('_dirty',dirty);
			this.notifyChange();
		}
	},

	_flags: null,

	setFlag:function(property,value) {
		if(this.get(property)!==value) {
			this.set(property,value);
			this._flags[property]=value;
			Ember.run.scheduleOnce('sync',this,this._updateFlags);
		}
	},

	_updateFlags: function() {
		for(var flag in this._flags) {
			this._components.invoke('set',flag,this._flags[flag]);
			delete this._flags[flag];
		}
	},

	init:function() {
		this._super();
		this._flags={};
		this.set('_controlMessages',Ember.A());
		this.set('_components',Ember.A());
		var control=this;
		// Initialize computed attributes
		this.constructor.eachComputedProperty(function(name, meta) {
			if (meta.type==='form-attr') {
				control.get(name);
			}
		});
		if(this._form) {
			this.set('_path',this._getPath());
			this.set('target',this._panel);
		}

		this._panelEnabledObserver();
		this._registerControl ? this._registerControl(this) : this._form._registerControl(this);
	},

	_apply: function() {

	},

	_didReset : false,

	_reset: function(modelChanged) {
// This breaks already set validation, our value might not change by a reset so may not trigger a new validation
		if(modelChanged) {
			this.setFlag('isValid',null);
			this.set('_valid',null);
		}
		this._didReset=true;
	},

	notifyChange: function() {
		if(this._panel) {
			Ember.run.scheduleOnce('sync',this,this._notifyPanel);
		}
	},

	_notifyPanel: function(){
		if(this._panel)
			this._panel.propertyDidChange(this._name);
	},

	_getPath: function() {
		// Temporary work arround to give lists a proper path
		// Temporary work arround to make sure forms dont get an absolute path
		if(!this['_model'] || this._isList || (this.get('_model')!==this.get('_panel._model') && !this._isForm)) {
			return (this.get('_panel._path') ? this.get('_panel._path')+ "." : '')+this.get('_name');
		} else if(this._panel && this.get('_model')===this.get('_panel._model')) {
			return this.get('_panel._path');
		}

	},

	hasModel : function() {
		var model=null;
		if(this['_model'])
			model = this.get('_model');
		else {
			if(this.getForm()) {
				model = this.getForm().get('_model');
			}
		}
		if(!model) {
			return false;
		}
		return true;
	},

	getModel : function(path) {
		var model=null;
		if(this['_model'])
			model = this.get('_model');
		else {
			if(this.getForm()) {
				model = this.getForm().get('_model');
			}
		}
		if(!model) {
			Ember.warn('Control '+this.toString()+' is trying to access the (form)model but it is not defined!',model,{id:'furnace-forms:control.model-missing'});
			return undefined;
		}
		if(path) {
			return model.get(path);
		}
		return model;
	},

	getFor : function(path) {
		return this.getModel(path);
	},

	getTarget : function() {
		if(this._form)
			return this.getForm().get('targetObject');
		return this.get('targetObject');
	},

	getPanel : function() {
		return this._panel;
	},

	getForm : function(path) {
		if(path) {
			return this._form.get(path);
		}
		return this._form;
	},

	_controlMessages : null,

	setMessages: function(messages,silent) {
		this._updateMessages(messages,this._controlMessages);
		if(this.get('hasPrerequisites')===false || this._components.length===0 || this._didReset)
			silent=true;
		this._didReset=false;
		this._messagesSilent=silent;
//		console.log("New messages for "+this+" Components: "+this._components.length);
		if(!silent) {
			Ember.run.once(this,function() {
				this._components.invoke('_controlMessageObserver');
			});
		}
	},

	_updateMessages: function(source,target) {
		var remove=Ember.A();
		target.forEach(function(item) {
			if(!source) {
				remove.pushObject(item);
			} else {
				var _messages=source.filterBy('type',item.type).filterBy('message',item.message);
				if(!_messages.length) {
					remove.pushObject(item);
				} else {

					_messages.forEach(function(message) {
						Ember.set(item,'attributes',message.attributes);
						source.removeObject(message);
					});
				}
			}
		});
		target.removeObjects(remove);
		if(source) {
			source.forEach(function(message) {
				target.pushObject(message);
			});
		}
	},

	_components : null,

	registerComponent:function(component) {
		this._components.pushObject(component);
		component.set('isValid',this.get('isValid'));
		component.set('isEnabled',this.get('isEnabled'));
		component.set('isDirty',this.get('isDirty'));
		if(!this._messagesSilent && this._controlMessages.length) {
//			console.log(component+" component registered to "+this+", updating messages");
			component._controlMessageObserver();
		}
		this.send('onComponent',component);

	},

	unregisterComponent:function(component) {
		this._components.removeObject(component);
	},

	getComponentClass : function() {
		Ember.assert('getComponentClass is no longer support',false);
		var component=this._decorator;
		if(typeof component ==="string") {
			component = Lookup.call(this,this._decoratorName,this._decoratorType);
		}
		return component;
	},

	getComponent : function() {
		var component=this.getComponentClass();
		if(!component)
			return null;
		return component.create({container:this.container,
								control:this,
								_debugContainerKey : component + this.get('_name')
								});
	},

	toString: function() {
		var string=this._super();
		return string.substring(0,string.length-1)+':'+this.get('_name')+'>';
	},

	willDestroy : function() {
	    if(!this.isDestroyed) {
		//this._unregisterControl ? this._unregisterControl(this) : this._form._unregisterControl(this);
	}
		this._super();
	},

	destroy: function() {
		this._super();
		this._unregisterControl ? this._unregisterControl(this) : this._form._unregisterControl(this);
		this.set('target',null);
		this.set('_panel',null);
		this.set('_form',null);
//		Ember.warn('Destroying control '+this.toString()+ ' while there are still components attached! '+ (this._components.length),this._components.length===0);
//		if(this._components.length!==0)
//			console.trace();
	},

}).reopenClass({
	decorator : function(decorator) {
		return this.reopen({
			_decoratorName : decorator
		});
	}
}).extend(Actions);<|endoftext|>


<commit_before>function Valinator() {
    var self = this;

    /**
     * The CSS class to add to an input element that contains an error
     * @type String
     */
    self.inputErrorClass = 'valinator-error';

    /**
     * The HTML tag to use for the error message element
     * @type String
     */
    self.errorMessageTag = 'span';

    /**
     * The CSS class of the error message element that will be added to an input element
     * @type String
     */
    self.errorMessageClass = 'valinator-error-message';

    /**
     * The CSS selector for the error message element
     * @type String
     */
    self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;

    /**
     * All the validation checks, in name:function pairs
     * @type Object
     */
    self.checks = {};

    /**
     * Set a custom CSS class for input fields that contain an error
     * @param String  cssClass
     */
    self.setInputErrorClass = function(cssClass) {
        self.inputErrorClass = cssClass;
    };

    /**
     * Set a custom HTML tag to use for adding error message elements
     * @param String  htmlTag
     */
    self.setErrorMessageTag = function(htmlTag) {
        self.errorMessageTag = htmlTag;

        // Also update the error message selector accordingly
        self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;
    };

    /**
     * Set a custom CSS class for the error message elements
     * @param String  cssClass
     */
    self.setErrorMessageClass = function(cssClass) {
        self.errorMessageClass = cssClass;

        // Also update the error message selector accordingly
        self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;
    };

    /**
     * Set a validation check (new or replace an existing one), that can be used by self.validate
     * @param String    name      The name of the check
     * @param Function  callback  The checking function, needs to return a boolean (true = passed, false = failed)
     *                              This function gets 2 arguments: value and option (option can be null)
     */
    self.newCheck = function(name, callback) {
        // An object with name:function pairs can be given,
        // which will be merged with the existing
        if($.type(name) == 'object')
            self.checks = $.extend(true, self.checks, name);

        // Otherwise use name and callback to add it
        else
            self.checks[name] = callback;
    };

    /**
     * Make sure the given value is/becomes a jQuery element
     * @param  String|jQuery  element
     * @return jQuery
     */
    self.ensureJquery = function(element) {
        if($.type(element) == 'string')
            element = $('[name="' + rule.element + '"]');

        return element;
    };

    /**
     * Remove any errors that are below an input field
     * @param jQuery  $element
     */
    self.removeError = function($element) {
        // Remove the error class from the input element
        $element.removeClass(self.inputErrorClass)
            // Then remove the error message element
            .next(self.errorMessageSelector)
            .remove();
    };

    /**
     * Show an error message below an input field
     * @param jQuery  $element
     * @param String  message
     */
    self.setError = function($element, message) {
        // Add an error message element, if it's not there yet
        if($element.next(self.errorMessageSelector).length < 1) {
            $('<' + self.errorMessageTag + '/>')
                .addClass(self.errorMessageClass)
                .insertAfter($element);
        }

        // Add the error class to the input element
        $element.addClass(self.inputErrorClass)
            // Then set the error message in the error message element
            .next(self.errorMessageSelector)
            .text(message);

        // When the value of the element changes, remove the error
        $element
            // Only 1 binding, so remove it first (event namespace is to prevent turning off (all) other event handlers)
            .off('change.validation keyup.validation paste.validation')
            .on('change.validation keyup.validation paste.validation', function(event) {
                self.removeError($element);
            });
    };

    /**
     * Validate given input fields
     * @param  Array  rules  Rules as objects (will be converted to array, if an object is given)
     *                         element: input name or jQuery element
     *                         rules: validation rules with error message, can have an option, with pipe character
     *                         Example:
     *                         {
     *                             element: $('input[name="password"]'),
     *                             rules: {
     *                                 required: 'Please fill in a password',
     *                                 min: '8|Password needs to be least 8 characters long',
     *                                 matchWith: 'password_check|Passwords need to match'
     *                             }
     *                         }
     *                         To make a value nullable, provide the rule nullable:true
     *                         The other validation rules will then only be applied if the value is not empty, example:
     *                         rules: {
     *                             nullable: true,
     *                             min: '5|The value needs to be at least 5 characters long'
     *                         }
     *                         To make a validation conditional, provide a function instead of a message
     *                         In this example, the field is only required if a particular select option is chosen
     *                         rules: {
     *                             required: function() {
     *                                 if($('select[name="hobby"]').val() == 'other')
     *                                     return 'Please tell us what your hobby is';
     *                             }
     *                         }
     * @return  Array  Collection of error elements as objects: {$element, errormessage}
     */
    self.validate = function(rules) {
        // Make sure the rules are an array
        if($.type(rules) != 'array')
            rules = [rules];

        // Keep track of the elements that have errors
        // Will contain objects: {$element, message}
        var errorElements = [];

        // Loop over the validation rules
        for(var i=-1;  ++i<rules.length;) {
            var rule = rules[i];

            // Make sure that the element is a jQuery element
            var $element = self.ensureJquery(rule.element);

            // Clear any current error message
            self.removeError($element);

            // Get the (trimmed) value, and update it inside the input field
            var value = $.trim($element.val());
            $element.val(value);

            // Only validate if the value is not nullable,
            // or if the value is nullable, and the value is not empty
            if( ! rule.rules.nullable  ||  (rule.rules.nullable  &&  value != '')) {
                // Loop over the validation rules
                for(var ruleName in rule.rules) {
                    // Don't validate the 'nullable' rule
                    if(ruleName != 'nullable') {
                        var message = rule.rules[ruleName];
                        var option = null;

                        // If the message is a function, it means it's conditional
                        // First execute the function, to see if validation is needed
                        if($.type(message) == 'function')
                            message = message();

                        // Only validate, if there is a valid message (which includes a possible option)
                        if($.type(message) == 'string') {
                            // Split the message to option + message by pipe character
                            // If it has a pipe character, the part before it is the option
                            var messageParts = message.split('|', 2);
                            if(messageParts.length == 2) {
                                option = messageParts[0];
                                message = messageParts[1];
                            }

                            // Check the value for errors (if the check exists)
                            if(self.checks[ruleName]  &&  ! self.checks[ruleName](value, option)) {
                                // Set an error on the element, and keep a reference
                                self.setError($element, message);
                                errorElements.push({
                                    $element: $element,
                                    message: message
                                });
                                // In case of error, don't do another validation on the same element
                                break;
                            }
                        }
                    }
                }
            }
        }

        return errorElements;
    };

    /**
     * Set some validation checks, to be used by self.validate
     */
    (self.setChecks = function() {
        self.newCheck({
            required: function(value, option) {
                // Value can't be empty
                return (value != '');
            },
            email: function(value, option) {
                // Value needs to be a valid email address
                return (value.match(/.+?@.+?\..+/));
            },
            phone: function(value, option) {
                // Value needs to be a valid phone number
                return (value.match(/^[\d\(\) \+-]+$/));
            },
            name: function(value, option) {
                // Value needs to be a valid name
                // Remove accents, before validating the alphabetical characters
                var nameNoAccents = App.characters.removeAccents(value);
                return (nameNoAccents.match(/^[a-z0-9 /,.-]+$/i));
            },
            min: function(value, option) {
                // Value needs to have a minimum length
                return (value.length >= parseInt(option, 10));
            },
            max: function(value, option) {
                // Value needs to have a maximum length
                return (value.length <= parseInt(option, 10));
            },
            length: function(value, option) {
                // Value needs to have a fixed length
                return (value.length == parseInt(option, 10));
            },
            match: function(value, option) {
                // Value needs to match the option
                return (value == option);
            },
            matchWith: function(value, option) {
                // Value needs to match the value of another element
                return (value == $('[name="' + option + '"]').val());
            },
            numeric: function(value, option) {
                // Value needs to be a number
                // From: https://github.com/angular/angular/blob/4.3.x/packages/common/src/pipes/number_pipe.ts#L172
                return ( ! isNaN(value - parseFloat(value)));
            },
            alphanumeric: function(value, option) {
                // Value needs to be alphanumeric
                return (value.match(/^[a-z\d]+$/i));
            }
        });
    })();
}
<commit_msg>Fixed a small comment typo<commit_after>function Valinator() {
    var self = this;

    /**
     * The CSS class to add to an input element that contains an error
     * @type String
     */
    self.inputErrorClass = 'valinator-error';

    /**
     * The HTML tag to use for the error message element
     * @type String
     */
    self.errorMessageTag = 'span';

    /**
     * The CSS class of the error message element that will be added to an input element
     * @type String
     */
    self.errorMessageClass = 'valinator-error-message';

    /**
     * The CSS selector for the error message element
     * @type String
     */
    self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;

    /**
     * All the validation checks, in name:function pairs
     * @type Object
     */
    self.checks = {};

    /**
     * Set a custom CSS class for input fields that contain an error
     * @param String  cssClass
     */
    self.setInputErrorClass = function(cssClass) {
        self.inputErrorClass = cssClass;
    };

    /**
     * Set a custom HTML tag to use for adding error message elements
     * @param String  htmlTag
     */
    self.setErrorMessageTag = function(htmlTag) {
        self.errorMessageTag = htmlTag;

        // Also update the error message selector accordingly
        self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;
    };

    /**
     * Set a custom CSS class for the error message elements
     * @param String  cssClass
     */
    self.setErrorMessageClass = function(cssClass) {
        self.errorMessageClass = cssClass;

        // Also update the error message selector accordingly
        self.errorMessageSelector = self.errorMessageTag + '.' + self.errorMessageClass;
    };

    /**
     * Set a validation check (new or replace an existing one), that can be used by self.validate
     * @param String    name      The name of the check
     * @param Function  callback  The checking function, needs to return a boolean (true = passed, false = failed)
     *                              This function gets 2 arguments: value and option (option can be null)
     */
    self.newCheck = function(name, callback) {
        // An object with name:function pairs can be given,
        // which will be merged with the existing
        if($.type(name) == 'object')
            self.checks = $.extend(true, self.checks, name);

        // Otherwise use name and callback to add it
        else
            self.checks[name] = callback;
    };

    /**
     * Make sure the given value is/becomes a jQuery element
     * @param  String|jQuery  element
     * @return jQuery
     */
    self.ensureJquery = function(element) {
        if($.type(element) == 'string')
            element = $('[name="' + rule.element + '"]');

        return element;
    };

    /**
     * Remove any errors that are below an input field
     * @param jQuery  $element
     */
    self.removeError = function($element) {
        // Remove the error class from the input element
        $element.removeClass(self.inputErrorClass)
            // Then remove the error message element
            .next(self.errorMessageSelector)
            .remove();
    };

    /**
     * Show an error message below an input field
     * @param jQuery  $element
     * @param String  message
     */
    self.setError = function($element, message) {
        // Add an error message element, if it's not there yet
        if($element.next(self.errorMessageSelector).length < 1) {
            $('<' + self.errorMessageTag + '/>')
                .addClass(self.errorMessageClass)
                .insertAfter($element);
        }

        // Add the error class to the input element
        $element.addClass(self.inputErrorClass)
            // Then set the error message in the error message element
            .next(self.errorMessageSelector)
            .text(message);

        // When the value of the element changes, remove the error
        $element
            // Only 1 binding, so remove it first (event namespace is to prevent turning off (all) other event handlers)
            .off('change.validation keyup.validation paste.validation')
            .on('change.validation keyup.validation paste.validation', function(event) {
                self.removeError($element);
            });
    };

    /**
     * Validate given input fields
     * @param  Array  rules  Rules as objects (will be converted to array, if an object is given)
     *                         element: input name or jQuery element
     *                         rules: validation rules with error message, can have an option, with pipe character
     *                         Example:
     *                         {
     *                             element: $('input[name="password"]'),
     *                             rules: {
     *                                 required: 'Please fill in a password',
     *                                 min: '8|Password needs to be least 8 characters long',
     *                                 matchWith: 'password_check|Passwords need to match'
     *                             }
     *                         }
     *                         To make a value nullable, provide the rule nullable:true
     *                         The other validation rules will then only be applied if the value is not empty, example:
     *                         rules: {
     *                             nullable: true,
     *                             min: '5|The value needs to be at least 5 characters long'
     *                         }
     *                         To make a validation conditional, provide a function instead of a message
     *                         In this example, the field is only required if a particular select option is chosen
     *                         rules: {
     *                             required: function() {
     *                                 if($('select[name="hobby"]').val() == 'other')
     *                                     return 'Please tell us what your hobby is';
     *                             }
     *                         }
     * @return  Array  Collection of error elements as objects: {$element, message}
     */
    self.validate = function(rules) {
        // Make sure the rules are an array
        if($.type(rules) != 'array')
            rules = [rules];

        // Keep track of the elements that have errors
        // Will contain objects: {$element, message}
        var errorElements = [];

        // Loop over the validation rules
        for(var i=-1;  ++i<rules.length;) {
            var rule = rules[i];

            // Make sure that the element is a jQuery element
            var $element = self.ensureJquery(rule.element);

            // Clear any current error message
            self.removeError($element);

            // Get the (trimmed) value, and update it inside the input field
            var value = $.trim($element.val());
            $element.val(value);

            // Only validate if the value is not nullable,
            // or if the value is nullable, and the value is not empty
            if( ! rule.rules.nullable  ||  (rule.rules.nullable  &&  value != '')) {
                // Loop over the validation rules
                for(var ruleName in rule.rules) {
                    // Don't validate the 'nullable' rule
                    if(ruleName != 'nullable') {
                        var message = rule.rules[ruleName];
                        var option = null;

                        // If the message is a function, it means it's conditional
                        // First execute the function, to see if validation is needed
                        if($.type(message) == 'function')
                            message = message();

                        // Only validate, if there is a valid message (which includes a possible option)
                        if($.type(message) == 'string') {
                            // Split the message to option + message by pipe character
                            // If it has a pipe character, the part before it is the option
                            var messageParts = message.split('|', 2);
                            if(messageParts.length == 2) {
                                option = messageParts[0];
                                message = messageParts[1];
                            }

                            // Check the value for errors (if the check exists)
                            if(self.checks[ruleName]  &&  ! self.checks[ruleName](value, option)) {
                                // Set an error on the element, and keep a reference
                                self.setError($element, message);
                                errorElements.push({
                                    $element: $element,
                                    message: message
                                });
                                // In case of error, don't do another validation on the same element
                                break;
                            }
                        }
                    }
                }
            }
        }

        return errorElements;
    };

    /**
     * Set some validation checks, to be used by self.validate
     */
    (self.setChecks = function() {
        self.newCheck({
            required: function(value, option) {
                // Value can't be empty
                return (value != '');
            },
            email: function(value, option) {
                // Value needs to be a valid email address
                return (value.match(/.+?@.+?\..+/));
            },
            phone: function(value, option) {
                // Value needs to be a valid phone number
                return (value.match(/^[\d\(\) \+-]+$/));
            },
            name: function(value, option) {
                // Value needs to be a valid name
                // Remove accents, before validating the alphabetical characters
                var nameNoAccents = App.characters.removeAccents(value);
                return (nameNoAccents.match(/^[a-z0-9 /,.-]+$/i));
            },
            min: function(value, option) {
                // Value needs to have a minimum length
                return (value.length >= parseInt(option, 10));
            },
            max: function(value, option) {
                // Value needs to have a maximum length
                return (value.length <= parseInt(option, 10));
            },
            length: function(value, option) {
                // Value needs to have a fixed length
                return (value.length == parseInt(option, 10));
            },
            match: function(value, option) {
                // Value needs to match the option
                return (value == option);
            },
            matchWith: function(value, option) {
                // Value needs to match the value of another element
                return (value == $('[name="' + option + '"]').val());
            },
            numeric: function(value, option) {
                // Value needs to be a number
                // From: https://github.com/angular/angular/blob/4.3.x/packages/common/src/pipes/number_pipe.ts#L172
                return ( ! isNaN(value - parseFloat(value)));
            },
            alphanumeric: function(value, option) {
                // Value needs to be alphanumeric
                return (value.match(/^[a-z\d]+$/i));
            }
        });
    })();
}
<|endoftext|>


<commit_before>import React from "react";
import {
    requireNativeComponent,
    StyleSheet,
    findNodeHandle,
    NativeModules,
} from "react-native";
import extractResponder from "../lib/extract/extractResponder";
import extractViewBox from "../lib/extract/extractViewBox";
import Shape from "./Shape";
import G from "./G";

const RNSVGSvgViewManager = NativeModules.RNSVGSvgViewManager;

// Svg - Root node of all Svg elements
let id = 0;

const styles = StyleSheet.create({
    svg: {
        backgroundColor: "transparent",
        borderWidth: 0,
    },
});

class Svg extends Shape {
    static displayName = "Svg";

    static defaultProps = {
        preserveAspectRatio: "xMidYMid meet",
    };

    constructor() {
        super(...arguments);
        this.id = ++id;
    }
    measureInWindow = (...args) => {
        this.root.measureInWindow(...args);
    };

    measure = (...args) => {
        this.root.measure(...args);
    };

    measureLayout = (...args) => {
        this.root.measureLayout(...args);
    };

    setNativeProps = props => {
        const { width, height } = props;
        if (width) {
            props.bbWidth = String(width);
        }
        if (height) {
            props.bbHeight = String(height);
        }
        this.root.setNativeProps(props);
    };

    toDataURL = callback => {
        callback &&
            RNSVGSvgViewManager.toDataURL(findNodeHandle(this.root), callback);
    };

    render() {
        const {
            opacity,
            viewBox,
            preserveAspectRatio,
            style,
            children,
            onLayout,
            ...props
        } = this.props;
        const stylesAndProps = {
            ...(style && style.length ? Object.assign({}, ...style) : style),
            ...props,
        };
        const {
            color,
            width,
            height,

            // Inherited G properties
            font,
            transform,
            fill,
            fillOpacity,
            fillRule,
            stroke,
            strokeWidth,
            strokeOpacity,
            strokeDasharray,
            strokeDashoffset,
            strokeLinecap,
            strokeLinejoin,
            strokeMiterlimit,
        } = stylesAndProps;

        const dimensions = width && height ? {
            width: width[width.length - 1] === "%" ? width : +width,
            height: height[height.length - 1] === "%" ? height : +height,
            flex: 0,
        } : null;

        const w = String(width);
        const h = String(height);

        return (
            <NativeSvgView
                {...props}
                bbWidth={w}
                bbHeight={h}
                tintColor={color}
                onLayout={onLayout}
                {...extractResponder(props, this)}
                {...extractViewBox({ viewBox, preserveAspectRatio })}
                ref={ele => {
                    this.root = ele;
                }}
                style={[
                    styles.svg,
                    style,
                    !isNaN(+opacity) && {
                        opacity: +opacity,
                    },
                    dimensions,
                ]}
            >
                <G
                    style={style}
                    {...{
                        font,
                        transform,
                        fill,
                        fillOpacity,
                        fillRule,
                        stroke,
                        strokeWidth,
                        strokeOpacity,
                        strokeDasharray,
                        strokeDashoffset,
                        strokeLinecap,
                        strokeLinejoin,
                        strokeMiterlimit,
                    }}
                >
                    {children}
                </G>
            </NativeSvgView>
        );
    }
}

const NativeSvgView = requireNativeComponent("RNSVGSvgView");

export default Svg;
<commit_msg>Refactor svg bounds extraction<commit_after>import React from "react";
import {
    requireNativeComponent,
    StyleSheet,
    findNodeHandle,
    NativeModules,
} from "react-native";
import extractResponder from "../lib/extract/extractResponder";
import extractViewBox from "../lib/extract/extractViewBox";
import Shape from "./Shape";
import G from "./G";

const RNSVGSvgViewManager = NativeModules.RNSVGSvgViewManager;

// Svg - Root node of all Svg elements
let id = 0;

const styles = StyleSheet.create({
    svg: {
        backgroundColor: "transparent",
        borderWidth: 0,
    },
});

class Svg extends Shape {
    static displayName = "Svg";

    static defaultProps = {
        preserveAspectRatio: "xMidYMid meet",
    };

    constructor() {
        super(...arguments);
        this.id = ++id;
    }
    measureInWindow = (...args) => {
        this.root.measureInWindow(...args);
    };

    measure = (...args) => {
        this.root.measure(...args);
    };

    measureLayout = (...args) => {
        this.root.measureLayout(...args);
    };

    setNativeProps = props => {
        const { width, height } = props;
        if (width) {
            props.bbWidth = String(width);
        }
        if (height) {
            props.bbHeight = String(height);
        }
        this.root.setNativeProps(props);
    };

    toDataURL = callback => {
        callback &&
            RNSVGSvgViewManager.toDataURL(findNodeHandle(this.root), callback);
    };

    render() {
        const {
            opacity,
            viewBox,
            preserveAspectRatio,
            style,
            children,
            onLayout,
            ...props
        } = this.props;
        const stylesAndProps = {
            ...(style && style.length ? Object.assign({}, ...style) : style),
            ...props,
        };
        const {
            color,
            width,
            height,

            // Inherited G properties
            font,
            transform,
            fill,
            fillOpacity,
            fillRule,
            stroke,
            strokeWidth,
            strokeOpacity,
            strokeDasharray,
            strokeDashoffset,
            strokeLinecap,
            strokeLinejoin,
            strokeMiterlimit,
        } = stylesAndProps;

        const dimensions = width && height ? {
            width: width[width.length - 1] === "%" ? width : +width,
            height: height[height.length - 1] === "%" ? height : +height,
            flex: 0,
        } : null;

        return (
            <NativeSvgView
                {...props}
                tintColor={color}
                onLayout={onLayout}
                bbWidth={String(width)}
                bbHeight={String(height)}
                {...extractResponder(props, this)}
                {...extractViewBox({ viewBox, preserveAspectRatio })}
                ref={ele => {
                    this.root = ele;
                }}
                style={[
                    styles.svg,
                    style,
                    !isNaN(+opacity) && {
                        opacity: +opacity,
                    },
                    dimensions,
                ]}
            >
                <G
                    style={style}
                    {...{
                        font,
                        transform,
                        fill,
                        fillOpacity,
                        fillRule,
                        stroke,
                        strokeWidth,
                        strokeOpacity,
                        strokeDasharray,
                        strokeDashoffset,
                        strokeLinecap,
                        strokeLinejoin,
                        strokeMiterlimit,
                    }}
                >
                    {children}
                </G>
            </NativeSvgView>
        );
    }
}

const NativeSvgView = requireNativeComponent("RNSVGSvgView");

export default Svg;
<|endoftext|>


<commit_before>/*	SWFObject v2.0 beta3 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
var swfobject=function(){var _1=[];var _2=[];var _3=null;var _4=null;var _5=false;var _6=false;var ua=function(){var _8=typeof document.getElementById!="undefined"&&typeof document.getElementsByTagName!="undefined"&&typeof document.createElement!="undefined"&&typeof document.appendChild!="undefined"&&typeof document.replaceChild!="undefined"&&typeof document.removeChild!="undefined"&&typeof document.cloneNode!="undefined";var _9=[0,0,0];var d=null;if(typeof navigator.plugins!="undefined"&&typeof navigator.plugins["Shockwave Flash"]=="object"){d=navigator.plugins["Shockwave Flash"].description;if(d){d=d.replace(/^.*\s+(\S+\s+\S+$)/,"$1");_9[0]=parseInt(d.replace(/^(.*)\..*$/,"$1"),10);_9[1]=parseInt(d.replace(/^.*\.(.*)\s.*$/,"$1"),10);_9[2]=/r/.test(d)?parseInt(d.replace(/^.*r(.*)$/,"$1"),10):0;}}else{if(typeof window.ActiveXObject!="undefined"){var a=null;var _c=false;try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");_9=[6,0,21];a.AllowScriptAccess="always";}catch(e){if(_9[0]==6){_c=true;}}if(!_c){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(e){}}}if(!_c&&typeof a=="object"){try{d=a.GetVariable("$version");if(d){d=d.split(" ")[1].split(",");_9=[parseInt(d[0],10),parseInt(d[1],10),parseInt(d[2],10)];}}catch(e){}}}}var u=navigator.userAgent.toLowerCase();var p=navigator.platform.toLowerCase();var _f=/webkit/.test(u);var _10=_f?parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):0;var ie=false;var win=p?/win/.test(p):/win/.test(u);var mac=p?/mac/.test(p):/mac/.test(u);/*@cc_on ie=true;@if(@_win32)win=true;@elif(@_mac)mac=true;@end@*/return {w3cdom:_8,playerVersion:_9,webkit:_f,webkitVersion:_10,ie:ie,win:win,mac:mac};}();var _14=function(){if(!ua.w3cdom){return;}addDomLoadEvent(main);if(ua.ie&&ua.win){try{document.write("<script id=__ie_ondomload defer=true src=//:></script>");var s=document.getElementById("__ie_ondomload");if(s){s.onreadystatechange=function(){if(this.readyState=="complete"){this.parentNode.removeChild(this);callDomLoadFunctions();}};}}catch(e){}}if(ua.webkit&&typeof document.readyState!="undefined"){_3=setInterval(function(){if(/loaded|complete/.test(document.readyState)){callDomLoadFunctions();}},10);}if(typeof document.addEventListener!="undefined"){document.addEventListener("DOMContentLoaded",callDomLoadFunctions,null);}addLoadEvent(callDomLoadFunctions);}();function callDomLoadFunctions(){if(_5){return;}if(ua.ie&&ua.win){var s=document.createElement("span");try{var t=document.getElementsByTagName("body")[0].appendChild(s);t.parentNode.removeChild(t);}catch(e){return;}}_5=true;if(_3){clearInterval(_3);_3=null;}var dl=_1.length;for(var i=0;i<dl;i++){_1[i]();}}function addDomLoadEvent(fn){_1[_1.length]=fn;}function addLoadEvent(fn){if(typeof window.addEventListener!="undefined"){window.addEventListener("load",fn,false);}else{if(typeof document.addEventListener!="undefined"){document.addEventListener("load",fn,false);}else{if(typeof window.attachEvent!="undefined"){window.attachEvent("onload",fn);}else{if(typeof window.onload=="function"){var _1c=window.onload;window.onload=function(){_1c();fn();};}else{window.onload=fn;}}}}}function main(){fixOutOfMemoryError();var rl=_2.length;for(var i=0;i<rl;i++){var id=_2[i].id;if(ua.playerVersion[0]>0){var obj=document.getElementById(id);if(obj){if(hasPlayerVersion(_2[i].swfVersion)){if(ua.webkit&&ua.webkitVersion<312){fixParams(obj);}}else{if(_2[i].expressInstall&&!_6&&hasPlayerVersion([6,0,65])&&(ua.win||ua.mac)){showExpressInstall(_2[i]);}else{displayAltContent(obj);}}}}createCSS("#"+id,"visibility:visible");}}function fixOutOfMemoryError(){if(ua.ie&&ua.win&&hasPlayerVersion([9,0,0])){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};});}}function fixParams(obj){var _22=obj.getElementsByTagName("object")[0];if(_22){var e=document.createElement("embed");var a=_22.attributes;if(a){var al=a.length;for(var i=0;i<al;i++){if(a[i].nodeName.toLowerCase()=="data"){e.setAttribute("src",a[i].nodeValue);}else{e.setAttribute(a[i].nodeName,a[i].nodeValue);}}}var c=_22.childNodes;if(c){var cl=c.length;for(var j=0;j<cl;j++){if(c[j].nodeType==1&&c[j].nodeName.toLowerCase()=="param"){e.setAttribute(c[j].getAttribute("name"),c[j].getAttribute("value"));}}}obj.parentNode.replaceChild(e,obj);}}function fixObjectLeaks(){if(ua.ie&&ua.win&&hasPlayerVersion([8,0,0])){window.attachEvent("onunload",function(){var o=document.getElementsByTagName("object");if(o){var ol=o.length;for(var i=0;i<ol;i++){o[i].style.display="none";for(var x in o[i]){if(typeof o[i][x]=="function"){o[i][x]=function(){};}}}}});}}function showExpressInstall(_2e){_6=true;var obj=document.getElementById(_2e.id);if(obj){if(_2e.altContentId){var ac=document.getElementById(_2e.altContentId);if(ac){_4=ac;}}else{_4=abstractAltContent(obj);}var w=_2e.width?_2e.width:(obj.getAttribute("width")?obj.getAttribute("width"):0);if(parseInt(w,10)<310){w="310";}var h=_2e.height?_2e.height:(obj.getAttribute("height")?obj.getAttribute("height"):0);if(parseInt(h,10)<137){h="137";}var pt=ua.ie&&ua.win?"ActiveX":"PlugIn";document.title=document.title.slice(0,47)+" - Flash Player Installation";var dt=document.title;var fv="MMredirectURL="+window.location+"&MMplayerType="+pt+"&MMdoctitle="+dt;var el=obj;if(ua.ie&&ua.win&&obj.readyState!=4){el=document.createElement("div");obj.parentNode.insertBefore(el,obj);obj.style.display="none";window.attachEvent("onload",function(){obj.parentNode.removeChild(obj);});}createSWF({data:_2e.expressInstall,id:"SWFObjectExprInst",width:w,height:h},{flashvars:fv},el);}}function displayAltContent(obj){if(ua.ie&&ua.win&&obj.readyState!=4){var el=document.createElement("div");obj.parentNode.insertBefore(el,obj);el.parentNode.replaceChild(abstractAltContent(obj),el);obj.style.display="none";window.attachEvent("onload",function(){obj.parentNode.removeChild(obj);});}else{obj.parentNode.replaceChild(abstractAltContent(obj),obj);}}function abstractAltContent(obj){var ac=document.createElement("div");if(ua.win&&ua.ie){ac.innerHTML=obj.innerHTML;}else{var _3b=obj.getElementsByTagName("object")[0];if(_3b){var c=_3b.childNodes;if(c){var cl=c.length;for(var i=0;i<cl;i++){if(!(c[i].nodeType==1&&c[i].nodeName.toLowerCase()=="param")&&!(c[i].nodeType==8)){ac.appendChild(c[i].cloneNode(true));}}}}}return ac;}function createSWF(_3f,_40,el){if(ua.ie&&ua.win){var att="";for(var i in _3f){if(i=="data"){_40.movie=_3f[i];}else{if(i!="classid"){att+=" "+i+"=\""+_3f[i]+"\"";}}}var par="";for(var j in _40){par+="<param name=\""+j+"\" value=\""+_40[j]+"\" />";}el.outerHTML="<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\""+att+">"+par+"</object>";fixObjectLeaks();}else{if(ua.webkit&&ua.webkitVersion<312){var e=document.createElement("embed");e.setAttribute("type","application/x-shockwave-flash");for(var k in _3f){if(k=="data"){e.setAttribute("src",_3f[k]);}else{if(k!="classid"){e.setAttribute(k,_3f[k]);}}}for(var l in _40){if(l!="movie"){e.setAttribute(l,_40[l]);}}el.parentNode.replaceChild(e,el);}else{var o=document.createElement("object");o.setAttribute("type","application/x-shockwave-flash");for(var m in _3f){if(m!="classid"){o.setAttribute(m,_3f[m]);}}for(var n in _40){if(n!="movie"){createObjParam(o,n,_40[n]);}}el.parentNode.replaceChild(o,el);}}}function createObjParam(el,_4d,_4e){var p=document.createElement("param");p.setAttribute("name",_4d);p.setAttribute("value",_4e);el.appendChild(p);}function hasPlayerVersion(rv){return (ua.playerVersion[0]>rv[0]||(ua.playerVersion[0]==rv[0]&&ua.playerVersion[1]>rv[1])||(ua.playerVersion[0]==rv[0]&&ua.playerVersion[1]==rv[1]&&ua.playerVersion[2]>=rv[2]))?true:false;}function createCSS(sel,_52){if(ua.ie&&ua.mac){return;}var h=document.getElementsByTagName("head")[0];var s=document.createElement("style");s.setAttribute("type","text/css");s.setAttribute("media","screen");if(!(ua.ie&&ua.win)&&typeof document.createTextNode!="undefined"){s.appendChild(document.createTextNode(sel+" {"+_52+"}"));}h.appendChild(s);if(ua.ie&&ua.win&&typeof document.styleSheets!="undefined"&&document.styleSheets.length>0){var ls=document.styleSheets[document.styleSheets.length-1];if(typeof ls.addRule=="object"){ls.addRule(sel,_52);}}}return {registerObject:function(_56,_57,_58){if(!ua.w3cdom||!_56||!_57){return;}var _59={};_59.id=_56;var v=_57.split(".");_59.swfVersion=[parseInt(v[0],10),parseInt(v[1],10),parseInt(v[2],10)];_59.expressInstall=_58?_58:false;_2[_2.length]=_59;createCSS("#"+_56,"visibility:hidden");},embedSWF:function(_5b,_5c,_5d,_5e,_5f,_60,_61,_62,_63){if(!ua.w3cdom||!_5b||!_5c||!_5d||!_5e||!_5f){return;}if(hasPlayerVersion(_5f.split("."))){createCSS("#"+_5c,"visibility:hidden");var att=(typeof _63=="object")?_63:{};att.data=_5b;att.width=_5d;att.height=_5e;var par=(typeof _62=="object")?_62:{};if(typeof _61=="object"){for(var i in _61){if(typeof par.flashvars!="undefined"){par.flashvars+="&"+i+"="+_61[i];}else{par.flashvars=i+"="+_61[i];}}}addDomLoadEvent(function(){createSWF(att,par,document.getElementById(_5c));createCSS("#"+_5c,"visibility:visible");});}else{if(_60&&!_6&&hasPlayerVersion([6,0,65])&&(ua.win||ua.mac)){createCSS("#"+_5c,"visibility:hidden");addDomLoadEvent(function(){var _67={};_67.id=_67.altContentId=_5c;_67.width=_5d;_67.height=_5e;_67.expressInstall=_60;showExpressInstall(_67);createCSS("#"+_5c,"visibility:visible");});}}},getFlashPlayerVersion:function(){return {major:ua.playerVersion[0],minor:ua.playerVersion[1],release:ua.playerVersion[2]};},hasFlashPlayerVersion:function(_68){return hasPlayerVersion(_68.split("."));},expressInstallCallback:function(){if(_6&&_4){var obj=document.getElementById("SWFObjectExprInst");if(obj){obj.parentNode.replaceChild(_4,obj);_4=null;_6=false;}}}};}();<commit_msg>SWFObject 2.0 beta4<commit_after>/*	SWFObject v2.0 beta4 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
var swfobject=function(){var _1=[];var _2=[];var _3=null;var _4=null;var _5=false;var _6=false;var ua=function(){var _8=typeof document.getElementById!="undefined"&&typeof document.getElementsByTagName!="undefined"&&typeof document.createElement!="undefined"&&typeof document.appendChild!="undefined"&&typeof document.replaceChild!="undefined"&&typeof document.removeChild!="undefined"&&typeof document.cloneNode!="undefined";var _9=[0,0,0];var d=null;if(typeof navigator.plugins!="undefined"&&typeof navigator.plugins["Shockwave Flash"]=="object"){d=navigator.plugins["Shockwave Flash"].description;if(d){d=d.replace(/^.*\s+(\S+\s+\S+$)/,"$1");_9[0]=parseInt(d.replace(/^(.*)\..*$/,"$1"),10);_9[1]=parseInt(d.replace(/^.*\.(.*)\s.*$/,"$1"),10);_9[2]=/r/.test(d)?parseInt(d.replace(/^.*r(.*)$/,"$1"),10):0;}}else{if(typeof window.ActiveXObject!="undefined"){var a=null;var _c=false;try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");_9=[6,0,21];a.AllowScriptAccess="always";}catch(e){if(_9[0]==6){_c=true;}}if(!_c){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(e){}}}if(!_c&&typeof a=="object"){try{d=a.GetVariable("$version");if(d){d=d.split(" ")[1].split(",");_9=[parseInt(d[0],10),parseInt(d[1],10),parseInt(d[2],10)];}}catch(e){}}}}var u=navigator.userAgent.toLowerCase();var p=navigator.platform.toLowerCase();var _f=/webkit/.test(u);var _10=_f?parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):0;var ie=false;var win=p?/win/.test(p):/win/.test(u);var mac=p?/mac/.test(p):/mac/.test(u);/*@cc_on ie=true;@if(@_win32)win=true;@elif(@_mac)mac=true;@end@*/return {w3cdom:_8,playerVersion:_9,webkit:_f,webkitVersion:_10,ie:ie,win:win,mac:mac};}();var _14=function(){if(!ua.w3cdom){return;}addDomLoadEvent(main);if(ua.ie&&ua.win){try{document.write("<script id=__ie_ondomload defer=true src=//:></script>");var s=document.getElementById("__ie_ondomload");if(s){s.onreadystatechange=function(){if(this.readyState=="complete"){this.parentNode.removeChild(this);callDomLoadFunctions();}};}}catch(e){}}if(ua.webkit&&typeof document.readyState!="undefined"){_3=setInterval(function(){if(/loaded|complete/.test(document.readyState)){callDomLoadFunctions();}},10);}if(typeof document.addEventListener!="undefined"){document.addEventListener("DOMContentLoaded",callDomLoadFunctions,null);}addLoadEvent(callDomLoadFunctions);}();function callDomLoadFunctions(){if(_5){return;}if(ua.ie&&ua.win){var s=document.createElement("span");try{var t=document.getElementsByTagName("body")[0].appendChild(s);t.parentNode.removeChild(t);}catch(e){return;}}_5=true;if(_3){clearInterval(_3);_3=null;}var dl=_1.length;for(var i=0;i<dl;i++){_1[i]();}}function addDomLoadEvent(fn){_1[_1.length]=fn;}function addLoadEvent(fn){if(typeof window.addEventListener!="undefined"){window.addEventListener("load",fn,false);}else{if(typeof document.addEventListener!="undefined"){document.addEventListener("load",fn,false);}else{if(typeof window.attachEvent!="undefined"){window.attachEvent("onload",fn);}else{if(typeof window.onload=="function"){var _1c=window.onload;window.onload=function(){_1c();fn();};}else{window.onload=fn;}}}}}function main(){fixOutOfMemoryError();var rl=_2.length;for(var i=0;i<rl;i++){var id=_2[i].id;if(ua.playerVersion[0]>0){var obj=document.getElementById(id);if(obj){if(hasPlayerVersion(_2[i].swfVersion)){if(ua.webkit&&ua.webkitVersion<312){fixParams(obj);}}else{if(_2[i].expressInstall&&!_6&&hasPlayerVersion([6,0,65])&&(ua.win||ua.mac)){showExpressInstall(_2[i]);}else{displayAltContent(obj);}}}}createCSS("#"+id,"visibility:visible");}}function fixOutOfMemoryError(){if(ua.ie&&ua.win&&hasPlayerVersion([9,0,0])){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};});}}function fixParams(obj){var _22=obj.getElementsByTagName("object")[0];if(_22){var e=document.createElement("embed");var a=_22.attributes;if(a){var al=a.length;for(var i=0;i<al;i++){if(a[i].nodeName.toLowerCase()=="data"){e.setAttribute("src",a[i].nodeValue);}else{e.setAttribute(a[i].nodeName,a[i].nodeValue);}}}var c=_22.childNodes;if(c){var cl=c.length;for(var j=0;j<cl;j++){if(c[j].nodeType==1&&c[j].nodeName.toLowerCase()=="param"){e.setAttribute(c[j].getAttribute("name"),c[j].getAttribute("value"));}}}obj.parentNode.replaceChild(e,obj);}}function fixObjectLeaks(){if(ua.ie&&ua.win&&hasPlayerVersion([8,0,0])){window.attachEvent("onunload",function(){var o=document.getElementsByTagName("object");if(o){var ol=o.length;for(var i=0;i<ol;i++){o[i].style.display="none";for(var x in o[i]){if(typeof o[i][x]=="function"){o[i][x]=function(){};}}}}});}}function showExpressInstall(_2e){_6=true;var obj=document.getElementById(_2e.id);if(obj){if(_2e.altContentId){var ac=document.getElementById(_2e.altContentId);if(ac){_4=ac;}}else{_4=abstractAltContent(obj);}var w=_2e.width?_2e.width:(obj.getAttribute("width")?obj.getAttribute("width"):0);if(parseInt(w,10)<310){w="310";}var h=_2e.height?_2e.height:(obj.getAttribute("height")?obj.getAttribute("height"):0);if(parseInt(h,10)<137){h="137";}var pt=ua.ie&&ua.win?"ActiveX":"PlugIn";document.title=document.title.slice(0,47)+" - Flash Player Installation";var dt=document.title;var fv="MMredirectURL="+window.location+"&MMplayerType="+pt+"&MMdoctitle="+dt;var el=obj;if(ua.ie&&ua.win&&obj.readyState!=4){el=document.createElement("div");obj.parentNode.insertBefore(el,obj);obj.style.display="none";window.attachEvent("onload",function(){obj.parentNode.removeChild(obj);});}createSWF({data:_2e.expressInstall,id:"SWFObjectExprInst",width:w,height:h},{flashvars:fv},el);}}function displayAltContent(obj){if(ua.ie&&ua.win&&obj.readyState!=4){var el=document.createElement("div");obj.parentNode.insertBefore(el,obj);el.parentNode.replaceChild(abstractAltContent(obj),el);obj.style.display="none";window.attachEvent("onload",function(){obj.parentNode.removeChild(obj);});}else{obj.parentNode.replaceChild(abstractAltContent(obj),obj);}}function abstractAltContent(obj){var ac=document.createElement("div");if(ua.win&&ua.ie){ac.innerHTML=obj.innerHTML;}else{var _3b=obj.getElementsByTagName("object")[0];if(_3b){var c=_3b.childNodes;if(c){var cl=c.length;for(var i=0;i<cl;i++){if(!(c[i].nodeType==1&&c[i].nodeName.toLowerCase()=="param")&&!(c[i].nodeType==8)){ac.appendChild(c[i].cloneNode(true));}}}}}return ac;}function createSWF(_3f,_40,el){if(ua.ie&&ua.win){var att="";for(var i in _3f){if(typeof _3f[i]=="string"){if(i=="data"){_40.movie=_3f[i];}else{if(i.toLowerCase()=="styleclass"){att+=" class=\""+_3f[i]+"\"";}else{if(i!="classid"){att+=" "+i+"=\""+_3f[i]+"\"";}}}}}var par="";for(var j in _40){if(typeof _40[j]=="string"){par+="<param name=\""+j+"\" value=\""+_40[j]+"\" />";}}el.outerHTML="<object classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\""+att+">"+par+"</object>";fixObjectLeaks();}else{if(ua.webkit&&ua.webkitVersion<312){var e=document.createElement("embed");e.setAttribute("type","application/x-shockwave-flash");for(var k in _3f){if(typeof _3f[k]=="string"){if(k=="data"){e.setAttribute("src",_3f[k]);}else{if(k.toLowerCase()=="styleclass"){e.setAttribute("class",_3f[k]);}else{if(k!="classid"){e.setAttribute(k,_3f[k]);}}}}}for(var l in _40){if(typeof _40[l]=="string"){if(l!="movie"){e.setAttribute(l,_40[l]);}}}el.parentNode.replaceChild(e,el);}else{var o=document.createElement("object");o.setAttribute("type","application/x-shockwave-flash");for(var m in _3f){if(typeof _3f[m]=="string"){if(m.toLowerCase()=="styleclass"){o.setAttribute("class",_3f[m]);}else{if(m!="classid"){o.setAttribute(m,_3f[m]);}}}}for(var n in _40){if(typeof _40[n]=="string"&&n!="movie"){createObjParam(o,n,_40[n]);}}el.parentNode.replaceChild(o,el);}}}function createObjParam(el,_4d,_4e){var p=document.createElement("param");p.setAttribute("name",_4d);p.setAttribute("value",_4e);el.appendChild(p);}function hasPlayerVersion(rv){return (ua.playerVersion[0]>rv[0]||(ua.playerVersion[0]==rv[0]&&ua.playerVersion[1]>rv[1])||(ua.playerVersion[0]==rv[0]&&ua.playerVersion[1]==rv[1]&&ua.playerVersion[2]>=rv[2]))?true:false;}function createCSS(sel,_52){if(ua.ie&&ua.mac){return;}var h=document.getElementsByTagName("head")[0];var s=document.createElement("style");s.setAttribute("type","text/css");s.setAttribute("media","screen");if(!(ua.ie&&ua.win)&&typeof document.createTextNode!="undefined"){s.appendChild(document.createTextNode(sel+" {"+_52+"}"));}h.appendChild(s);if(ua.ie&&ua.win&&typeof document.styleSheets!="undefined"&&document.styleSheets.length>0){var ls=document.styleSheets[document.styleSheets.length-1];if(typeof ls.addRule=="object"){ls.addRule(sel,_52);}}}return {registerObject:function(_56,_57,_58){if(!ua.w3cdom||!_56||!_57){return;}var _59={};_59.id=_56;var v=_57.split(".");_59.swfVersion=[parseInt(v[0],10),parseInt(v[1],10),parseInt(v[2],10)];_59.expressInstall=_58?_58:false;_2[_2.length]=_59;createCSS("#"+_56,"visibility:hidden");},embedSWF:function(_5b,_5c,_5d,_5e,_5f,_60,_61,_62,_63){if(!ua.w3cdom||!_5b||!_5c||!_5d||!_5e||!_5f){return;}if(hasPlayerVersion(_5f.split("."))){createCSS("#"+_5c,"visibility:hidden");var att=(typeof _63=="object")?_63:{};att.data=_5b;att.width=_5d;att.height=_5e;var par=(typeof _62=="object")?_62:{};if(typeof _61=="object"){for(var i in _61){if(typeof _61[i]=="string"){if(typeof par.flashvars!="undefined"){par.flashvars+="&"+i+"="+_61[i];}else{par.flashvars=i+"="+_61[i];}}}}addDomLoadEvent(function(){createSWF(att,par,document.getElementById(_5c));createCSS("#"+_5c,"visibility:visible");});}else{if(_60&&!_6&&hasPlayerVersion([6,0,65])&&(ua.win||ua.mac)){createCSS("#"+_5c,"visibility:hidden");addDomLoadEvent(function(){var _67={};_67.id=_67.altContentId=_5c;_67.width=_5d;_67.height=_5e;_67.expressInstall=_60;showExpressInstall(_67);createCSS("#"+_5c,"visibility:visible");});}}},getFlashPlayerVersion:function(){return {major:ua.playerVersion[0],minor:ua.playerVersion[1],release:ua.playerVersion[2]};},hasFlashPlayerVersion:function(_68){return hasPlayerVersion(_68.split("."));},expressInstallCallback:function(){if(_6&&_4){var obj=document.getElementById("SWFObjectExprInst");if(obj){obj.parentNode.replaceChild(_4,obj);_4=null;_6=false;}}}};}();<|endoftext|>


<commit_before>const _ = require('lodash');
const uid = require('uid');
const update = require('immutability-helper');
const actions = require('./actions');
const utils = require('./utils');

const SWIPE_DELAY_TOLERANCE = 100;

const initialState = {
  clusters: {},
  clients: {},
  swipes: [],
};

function createReducer (config) {
  return (state = initialState, { type, data }) => {
    switch (type) {
      case actions.TYPE.NEXT_STATE:
        return nextState(state);

      case actions.TYPE.CLIENT_ACTION:
        return clientAction(state, data);

      case actions.TYPE.CONNECT:
        return connect(state, data);

      case actions.TYPE.SWIPE:
        return doSwipe(state, data);

      case actions.TYPE.DISCONNECT:
        return disconnect(state, data);

      case actions.TYPE.RECONNECT:
        return reconnect(state, data);

      case actions.TYPE.LEAVE_CLUSTER: {
         return leaveCluster(state, data);
       }
      default:
        return state;
    }
  };

  function nextState (state) {
    const updateClient = config.client.events.update;
    const updateCluster = config.cluster.events.update;
    const changes = {};

    if (_.isFunction(updateCluster)) {
      changes.clusters = getAllChanges(state.clusters, _.partial(getClusterChanges, state, updateCluster));
    }

    if (_.isFunction(updateClient)) {
      changes.clients = getAllChanges(state.clients, _.partial(getClientChanges, state, updateClient));
    }

    return update(state, changes);
  }

  function getAllChanges (entities, update) {
    const changes = _.map(entities, update);
    const ids = _.keys(entities);
    return _.zipObject(ids, changes);
  }

  function getClusterChanges (state, next, cluster) {
    const clusterState = utils.getClusterState(state, cluster.id);
    return { data: next(clusterState) };
  }

  function getClientChanges (state, next, client) {
    const clientState = utils.getClientState(state, client.id);
    return { data: next(clientState) };
  }

  function clientAction (state, { id, type, data }) {
    const handler = config.client.events[type];

    if (!_.isFunction(handler)) {
      throw new Error(`Unhandled event: ${type}`);
    }

    const client = state.clients[id];

    if (!client) {
      return state;
    }

    const clientEventState = utils.getClientState(state, client.id);
    const stateUpdates = handler(clientEventState, data);
    const changes = {};

    if (stateUpdates.cluster) {
      changes.clusters = {
        [client.clusterID]: stateUpdates.cluster,
      };
    }

    if (stateUpdates.client) {
      changes.clients = {
        [client.id]: stateUpdates.client,
      };
    }

    return update(state, changes);
  }

  function connect (state, { id, size }) {
    const clusterID = uid();
    const openings = {
      top: [],
      bottom: [],
      right: [],
      left: [],
    };
    const client = { id, size, transform: { x: 0, y: 0 }, adjacentClientIDs: [], clusterID, openings };

    const clientData = config.client.init(client);
    const clusterData = config.cluster.init(client);

    const changes = {
      clusters: {
        [clusterID]: { $set: { id: clusterID, data: clusterData } },
      },
      clients: {
        [id]: { $set: _.assign({}, client, { data: clientData }) },
      },
    };

    return update(state, changes);
  }

  function doSwipe (state, swipe) {
    const swipes = getCoincidentSwipes(state.swipes);

    if (swipes.length === 0) {
      return addSwipe(state, swipes, swipe);
    }

    const swipeA = swipe;
    const clientA = state.clients[swipeA.id];
    const swipeB = swipes[0];
    const clientB = state.clients[swipeB.id];

    //////////////////////////////////////////////////////////do a functionn
    let updatedState = state;
    if(!clientA || !clientB) {
      return clearSwipes(state);
    }
    const clientACluster = state.clusters[clientA.clusterID];
    const clientBCluster = state.clusters[clientB.clusterID];
    if(clientACluster.data.currentScreenId !== 0 || clientBCluster.data.currentScreenId !== 0) {
      const direction = clientACluster.data.currentScreenId !== swipeA.id ? swipeA.direction : swipeB.direction;
      let originCluster = null;
      let targetCluster = null;
      if(clientACluster.data.maze.getNbMove() > clientBCluster.data.maze.getNbMove()) {
        originCluster = clientACluster;
        targetCluster = clientBCluster;
      } else {
        originCluster = clientBCluster;
        targetCluster = clientACluster;
      }
      const originMaze = originCluster.data.maze;
      //move and check if out of map
      console.log("swipe")
      if(!originMaze.movePosition(direction)) {
        return clearSwipes(state);
      }
      updatedState = copyMaze(state, originCluster, targetCluster);
      console.log(direction)
      console.log(updatedState.clusters[targetCluster.id].data.maze)
    }
    //////////////////////////////////////////////////// end of the the function

    if (clientA.clusterID === clientB.clusterID) {
      return clearSwipes(state);
    }

    const { clients, clusters } = mergeAndRecalculateClusters(updatedState, clientA, swipeA, clientB, swipeB);

    return update(updatedState, {
      swipes: { $set: [] },
      clients: { $set: clients },
      clusters: { $set: clusters },
    });
  }

  function getCoincidentSwipes (swipes) {
    return _.filter(swipes, ({ timestamp }) => (Date.now() - timestamp) < SWIPE_DELAY_TOLERANCE);
  }

  function addSwipe (state, swipes, swipe) {
    const swipeWithTimestamp = update(swipe, { timestamp: { $set: Date.now() } });

    return update(state, {
      swipes: { $set: swipes.concat([swipeWithTimestamp]) },
    });
  }

  function clearSwipes (state) {
    return update(state, { swipes: { $set: [] } });
  }

  function mergeAndRecalculateClusters (state, clientA, swipeA, clientB, swipeB) {
    const transform = getTransform(clientA, swipeA, clientB, swipeB);

    return _.flow([
      _.partial(mergeClusterData, _, transform, clientA.id, clientB.id),
      _.partial(moveClientsToNewCluster, _, transform, clientA.id, clientB.id),
      _.partial(recalculateOpenings, _, clientA.id, clientB.id),
    ])(state);
  }

  function mergeClusterData (state, transform, clientAID, clientBID) {
    const newClusterID = state.clients[clientAID].clusterID;
    const oldClusterID = state.clients[clientBID].clusterID;
    const clusterStateA = utils.getClusterState(state, newClusterID);
    const clusterStateB = utils.getClusterState(state, oldClusterID);
    const clusterDataChanges = config.cluster.events.merge(clusterStateA, clusterStateB, transform);

    return update(state, {
      clusters: {
        [newClusterID]: { data: clusterDataChanges },
      },
    });
  }

  function moveClientsToNewCluster (state, transform, clientAID, clientBID) {
    const oldClusterID = state.clients[clientBID].clusterID;

    return update(state, {
      clusters: { $set: _.omit(state.clusters, oldClusterID) },
      clients: _.assign(
        {
          [clientAID]: {
            adjacentClientIDs: { $push: [clientBID] },
          },
        },
        getClientsBChanges(state, transform, clientAID, clientBID)
      ),
    });
  }

  function getClientsBChanges (state, transform, clientAID, clientBID) {
    const newClusterID = state.clients[clientAID].clusterID;
    const oldClusterID = state.clients[clientBID].clusterID;

    const clientsInCluster = utils.getClientsInCluster(state.clients, oldClusterID);

    return _.reduce(clientsInCluster, (changes, client) => {
      /* eslint-disable no-param-reassign */

      changes[client.id] = {
        clusterID: { $set: newClusterID },
        transform: {
          $set: {
            x: client.transform.x + transform.x,
            y: client.transform.y + transform.y,
          },
        },
      };

      if (client.id === clientBID) {
        changes[client.id].adjacentClientIDs = { $push: [clientAID] };
      }

      return changes;
      /* eslint-enable no-param-reassign */
    }, {});
  }

  function recalculateOpenings (state, clientAID, clientBID) {
    return update(state, {
      clients: {
        [clientAID]: {
          openings: { $set: utils.getOpenings(state.clients, state.clients[clientAID]) },
        },
        [clientBID]: {
          openings: { $set: utils.getOpenings(state.clients, state.clients[clientBID]) },
        },
      },
    });
  }

  function getTransform (clientA, swipeA, clientB, swipeB) {
    switch (swipeA.direction) {
      case 'LEFT':
        return {
          x: (clientA.transform.x - clientB.size.width) - clientB.transform.x,
          y: (clientA.transform.y + (swipeA.position.y - swipeB.position.y)) - clientB.transform.y,
        };
      case 'RIGHT':
        return {
          x: (clientA.transform.x + clientA.size.width) - clientB.transform.x,
          y: (clientA.transform.y + (swipeA.position.y - swipeB.position.y)) - clientB.transform.y,
        };

      case 'UP':
        return {
          x: (clientA.transform.x + (swipeA.position.x - swipeB.position.x)) - clientB.transform.x,
          y: (clientA.transform.y - clientB.size.height) - clientB.transform.y,
        };

      case 'DOWN':
        return {
          x: (clientA.transform.x + (swipeA.position.x - swipeB.position.x)) - clientB.transform.x,
          y: (clientA.transform.y + clientA.size.height) - clientB.transform.y,
        };

      default:
        throw new Error(`Invalid direction: ${swipeA.direction}`);
    }
  }

  function disconnect (state, { id }) {
    const { clients, clusters } = state;
    const client = clients[id];
    if (!client) {
      return state;
    }

    const clusterID = client.clusterID;

    return update(state, {
      clusters: { $set: removeEmptyCluster(clusters, clients, clusterID) },
      clients: { $set: removeClient(clients, client) },
    });
  }

  function removeEmptyCluster (clusters, clients, clusterID) {
    if (utils.getClientsInCluster(clients, clusterID).length > 1) {
      return clusters;
    }

    return _.omit(clusters, [clusterID]);
  }

  function removeClient (clients, client) {
    return _(clients)
      .omit(client.id)
      .mapValues((other) => {
        const newAdjacentClientIDs = _.without(other.adjacentClientIDs, client.id);
        const newClient = update(other, {
          adjacentClientIDs: { $set: newAdjacentClientIDs },
        });

        return update(other, {
          openings: { $set: utils.getOpenings(clients, newClient) },
          adjacentClientIDs: { $set: newAdjacentClientIDs },
        });
      })
      .value();
  }

  function reconnect (state, { id, size }) {
    return _.flow([
      _.partial(disconnect, _, { id }),
      _.partial(connect, _, { id, size }),
    ])(state);
  }

  function leaveCluster(state, { id }) {
    const { clients, clusters } = state;
    const client = clients[id];
    if (!client) {
      return state;
    }
    const { clusterID } = client;
    const newClusterID = uid();
    const clusterData = config.cluster.init(client);
    const clusterDataModified = Object.assign({}, clusterData, { currentScreenId: client.id });
    const newCluster = { [newClusterID]: { id: newClusterID, data: clusterDataModified } };
    const updatedClusters = Object.assign( {}, newCluster, removeEmptyCluster(clusters, clients, clusterID) );
    updatedClustersPendingSplit = {};
    Object.keys(updatedClusters).forEach(key => {
      const data = Object.assign({}, updatedClusters[key].data, { pendingSplit: null });
      updatedClustersPendingSplit[key] = Object.assign({}, updatedClusters[key], { data });
    });

    const clientData = config.client.init(client);
    const openings = {
      top: [],
      bottom: [],
      right: [],
      left: [],
    };
    const updatedClient = Object.assign({}, client, { openings, adjacentClientIDs: [], data: clientData, clusterID: newClusterID });
    const updatedClients = Object.assign({}, { [updatedClient.id]: updatedClient }, removeClient(clients, client));
    const result = update(state, {
      clusters: { $set: updatedClustersPendingSplit },
      clients: { $set: updatedClients },
    });
    return result;
  }

  function copyMaze(state, originCluster, targetCluster ) {
    const updatedData = Object.assign({}, targetCluster.data, { maze: originCluster.data.maze });
    const updatedTargetCluster = { [targetCluster.id] : Object.assign({}, targetCluster, { data: updatedData} ) };
    const { clusters } = state;
    const updatedClusters = Object.assign({}, clusters, updatedTargetCluster);
    return update(state, {
      clusters: { $set: updatedClusters },
    });
  }

  // function changePendingCluster(state, { id }) {
  //   const { clients, clusters } = state;
  //   const { clusterID } = clients[id];
  //   const clusterState = utils.getClusterState(state, clusterID);
  //   const clusterUpdated = Object.assign({}, clusterState, {pendingSplit: false, currentScreenId: clusterID });
  //   const updatedClusters = Object.assign({}, clusters, clusterUpdated );
  //   return update(state, {
  //     clusters: { $set: updatedClusters }
  //   });
  // }
}

module.exports = createReducer;
<commit_msg>save character properties<commit_after>const _ = require('lodash');
const uid = require('uid');
const update = require('immutability-helper');
const actions = require('./actions');
const utils = require('./utils');

const SWIPE_DELAY_TOLERANCE = 100;

const initialState = {
  clusters: {},
  clients: {},
  swipes: [],
};

function createReducer (config) {
  return (state = initialState, { type, data }) => {
    switch (type) {
      case actions.TYPE.NEXT_STATE:
        return nextState(state);

      case actions.TYPE.CLIENT_ACTION:
        return clientAction(state, data);

      case actions.TYPE.CONNECT:
        return connect(state, data);

      case actions.TYPE.SWIPE:
        return doSwipe(state, data);

      case actions.TYPE.DISCONNECT:
        return disconnect(state, data);

      case actions.TYPE.RECONNECT:
        return reconnect(state, data);

      case actions.TYPE.LEAVE_CLUSTER: {
         return leaveCluster(state, data);
       }
      default:
        return state;
    }
  };

  function nextState (state) {
    const updateClient = config.client.events.update;
    const updateCluster = config.cluster.events.update;
    const changes = {};

    if (_.isFunction(updateCluster)) {
      changes.clusters = getAllChanges(state.clusters, _.partial(getClusterChanges, state, updateCluster));
    }

    if (_.isFunction(updateClient)) {
      changes.clients = getAllChanges(state.clients, _.partial(getClientChanges, state, updateClient));
    }

    return update(state, changes);
  }

  function getAllChanges (entities, update) {
    const changes = _.map(entities, update);
    const ids = _.keys(entities);
    return _.zipObject(ids, changes);
  }

  function getClusterChanges (state, next, cluster) {
    const clusterState = utils.getClusterState(state, cluster.id);
    return { data: next(clusterState) };
  }

  function getClientChanges (state, next, client) {
    const clientState = utils.getClientState(state, client.id);
    return { data: next(clientState) };
  }

  function clientAction (state, { id, type, data }) {
    const handler = config.client.events[type];

    if (!_.isFunction(handler)) {
      throw new Error(`Unhandled event: ${type}`);
    }

    const client = state.clients[id];

    if (!client) {
      return state;
    }

    const clientEventState = utils.getClientState(state, client.id);
    const stateUpdates = handler(clientEventState, data);
    const changes = {};

    if (stateUpdates.cluster) {
      changes.clusters = {
        [client.clusterID]: stateUpdates.cluster,
      };
    }

    if (stateUpdates.client) {
      changes.clients = {
        [client.id]: stateUpdates.client,
      };
    }

    return update(state, changes);
  }

  function connect (state, { id, size }) {
    const clusterID = uid();
    const openings = {
      top: [],
      bottom: [],
      right: [],
      left: [],
    };
    const client = { id, size, transform: { x: 0, y: 0 }, adjacentClientIDs: [], clusterID, openings };

    const clientData = config.client.init(client);
    const clusterData = config.cluster.init(client);

    const changes = {
      clusters: {
        [clusterID]: { $set: { id: clusterID, data: clusterData } },
      },
      clients: {
        [id]: { $set: _.assign({}, client, { data: clientData }) },
      },
    };

    return update(state, changes);
  }

  function doSwipe (state, swipe) {
    const swipes = getCoincidentSwipes(state.swipes);

    if (swipes.length === 0) {
      return addSwipe(state, swipes, swipe);
    }

    const swipeA = swipe;
    const clientA = state.clients[swipeA.id];
    const swipeB = swipes[0];
    const clientB = state.clients[swipeB.id];

    //////////////////////////////////////////////////////////do a functionn
    let updatedState = state;
    if(!clientA || !clientB) {
      return clearSwipes(state);
    }
    const clientACluster = state.clusters[clientA.clusterID];
    const clientBCluster = state.clusters[clientB.clusterID];
    if(clientACluster.data.currentScreenId !== 0 || clientBCluster.data.currentScreenId !== 0) {
      const direction = clientACluster.data.currentScreenId !== swipeA.id ? swipeA.direction : swipeB.direction;
      let originCluster = null;
      let targetCluster = null;
      if(clientACluster.data.maze.getNbMove() > clientBCluster.data.maze.getNbMove()) {
        originCluster = clientACluster;
        targetCluster = clientBCluster;
      } else {
        originCluster = clientBCluster;
        targetCluster = clientACluster;
      }
      const originMaze = originCluster.data.maze;
      //move and check if out of map
      console.log("swipe")
      if(!originMaze.movePosition(direction)) {
        return clearSwipes(state);
      }
      updatedState = copyMazeAndCharacter(state, originCluster, targetCluster);
      console.log(direction)
      console.log(updatedState.clusters[targetCluster.id].data.maze)
    }
    //////////////////////////////////////////////////// end of the the function

    if (clientA.clusterID === clientB.clusterID) {
      return clearSwipes(state);
    }

    const { clients, clusters } = mergeAndRecalculateClusters(updatedState, clientA, swipeA, clientB, swipeB);

    return update(updatedState, {
      swipes: { $set: [] },
      clients: { $set: clients },
      clusters: { $set: clusters },
    });
  }

  function getCoincidentSwipes (swipes) {
    return _.filter(swipes, ({ timestamp }) => (Date.now() - timestamp) < SWIPE_DELAY_TOLERANCE);
  }

  function addSwipe (state, swipes, swipe) {
    const swipeWithTimestamp = update(swipe, { timestamp: { $set: Date.now() } });

    return update(state, {
      swipes: { $set: swipes.concat([swipeWithTimestamp]) },
    });
  }

  function clearSwipes (state) {
    return update(state, { swipes: { $set: [] } });
  }

  function mergeAndRecalculateClusters (state, clientA, swipeA, clientB, swipeB) {
    const transform = getTransform(clientA, swipeA, clientB, swipeB);

    return _.flow([
      _.partial(mergeClusterData, _, transform, clientA.id, clientB.id),
      _.partial(moveClientsToNewCluster, _, transform, clientA.id, clientB.id),
      _.partial(recalculateOpenings, _, clientA.id, clientB.id),
    ])(state);
  }

  function mergeClusterData (state, transform, clientAID, clientBID) {
    const newClusterID = state.clients[clientAID].clusterID;
    const oldClusterID = state.clients[clientBID].clusterID;
    const clusterStateA = utils.getClusterState(state, newClusterID);
    const clusterStateB = utils.getClusterState(state, oldClusterID);
    const clusterDataChanges = config.cluster.events.merge(clusterStateA, clusterStateB, transform);

    return update(state, {
      clusters: {
        [newClusterID]: { data: clusterDataChanges },
      },
    });
  }

  function moveClientsToNewCluster (state, transform, clientAID, clientBID) {
    const oldClusterID = state.clients[clientBID].clusterID;

    return update(state, {
      clusters: { $set: _.omit(state.clusters, oldClusterID) },
      clients: _.assign(
        {
          [clientAID]: {
            adjacentClientIDs: { $push: [clientBID] },
          },
        },
        getClientsBChanges(state, transform, clientAID, clientBID)
      ),
    });
  }

  function getClientsBChanges (state, transform, clientAID, clientBID) {
    const newClusterID = state.clients[clientAID].clusterID;
    const oldClusterID = state.clients[clientBID].clusterID;

    const clientsInCluster = utils.getClientsInCluster(state.clients, oldClusterID);

    return _.reduce(clientsInCluster, (changes, client) => {
      /* eslint-disable no-param-reassign */

      changes[client.id] = {
        clusterID: { $set: newClusterID },
        transform: {
          $set: {
            x: client.transform.x + transform.x,
            y: client.transform.y + transform.y,
          },
        },
      };

      if (client.id === clientBID) {
        changes[client.id].adjacentClientIDs = { $push: [clientAID] };
      }

      return changes;
      /* eslint-enable no-param-reassign */
    }, {});
  }

  function recalculateOpenings (state, clientAID, clientBID) {
    return update(state, {
      clients: {
        [clientAID]: {
          openings: { $set: utils.getOpenings(state.clients, state.clients[clientAID]) },
        },
        [clientBID]: {
          openings: { $set: utils.getOpenings(state.clients, state.clients[clientBID]) },
        },
      },
    });
  }

  function getTransform (clientA, swipeA, clientB, swipeB) {
    switch (swipeA.direction) {
      case 'LEFT':
        return {
          x: (clientA.transform.x - clientB.size.width) - clientB.transform.x,
          y: (clientA.transform.y + (swipeA.position.y - swipeB.position.y)) - clientB.transform.y,
        };
      case 'RIGHT':
        return {
          x: (clientA.transform.x + clientA.size.width) - clientB.transform.x,
          y: (clientA.transform.y + (swipeA.position.y - swipeB.position.y)) - clientB.transform.y,
        };

      case 'UP':
        return {
          x: (clientA.transform.x + (swipeA.position.x - swipeB.position.x)) - clientB.transform.x,
          y: (clientA.transform.y - clientB.size.height) - clientB.transform.y,
        };

      case 'DOWN':
        return {
          x: (clientA.transform.x + (swipeA.position.x - swipeB.position.x)) - clientB.transform.x,
          y: (clientA.transform.y + clientA.size.height) - clientB.transform.y,
        };

      default:
        throw new Error(`Invalid direction: ${swipeA.direction}`);
    }
  }

  function disconnect (state, { id }) {
    const { clients, clusters } = state;
    const client = clients[id];
    if (!client) {
      return state;
    }

    const clusterID = client.clusterID;

    return update(state, {
      clusters: { $set: removeEmptyCluster(clusters, clients, clusterID) },
      clients: { $set: removeClient(clients, client) },
    });
  }

  function removeEmptyCluster (clusters, clients, clusterID) {
    if (utils.getClientsInCluster(clients, clusterID).length > 1) {
      return clusters;
    }

    return _.omit(clusters, [clusterID]);
  }

  function removeClient (clients, client) {
    return _(clients)
      .omit(client.id)
      .mapValues((other) => {
        const newAdjacentClientIDs = _.without(other.adjacentClientIDs, client.id);
        const newClient = update(other, {
          adjacentClientIDs: { $set: newAdjacentClientIDs },
        });

        return update(other, {
          openings: { $set: utils.getOpenings(clients, newClient) },
          adjacentClientIDs: { $set: newAdjacentClientIDs },
        });
      })
      .value();
  }

  function reconnect (state, { id, size }) {
    return _.flow([
      _.partial(disconnect, _, { id }),
      _.partial(connect, _, { id, size }),
    ])(state);
  }

  function leaveCluster(state, { id }) {
    const { clients, clusters } = state;
    const client = clients[id];
    if (!client) {
      return state;
    }
    const { clusterID } = client;
    const newClusterID = uid();
    const clusterData = config.cluster.init(client);
    const clusterDataModified = Object.assign({}, clusterData, { currentScreenId: client.id });
    const newCluster = { [newClusterID]: { id: newClusterID, data: clusterDataModified } };
    const updatedClusters = Object.assign( {}, newCluster, removeEmptyCluster(clusters, clients, clusterID) );
    updatedClustersPendingSplit = {};
    Object.keys(updatedClusters).forEach(key => {
      const data = Object.assign({}, updatedClusters[key].data, { pendingSplit: null });
      updatedClustersPendingSplit[key] = Object.assign({}, updatedClusters[key], { data });
    });

    const clientData = config.client.init(client);
    const openings = {
      top: [],
      bottom: [],
      right: [],
      left: [],
    };
    const updatedClient = Object.assign({}, client, { openings, adjacentClientIDs: [], data: clientData, clusterID: newClusterID });
    const updatedClients = Object.assign({}, { [updatedClient.id]: updatedClient }, removeClient(clients, client));
    const result = update(state, {
      clusters: { $set: updatedClustersPendingSplit },
      clients: { $set: updatedClients },
    });
    return result;
  }

  function copyMazeAndCharacter(state, originCluster, targetCluster ) {
    const updatedData = Object.assign({}, targetCluster.data, { maze: originCluster.data.maze, character: originCluster.data.character });
    const updatedTargetCluster = { [targetCluster.id] : Object.assign({}, targetCluster, { data: updatedData} ) };
    const { clusters } = state;
    const updatedClusters = Object.assign({}, clusters, updatedTargetCluster);
    return update(state, {
      clusters: { $set: updatedClusters },
    });
  }

  // function changePendingCluster(state, { id }) {
  //   const { clients, clusters } = state;
  //   const { clusterID } = clients[id];
  //   const clusterState = utils.getClusterState(state, clusterID);
  //   const clusterUpdated = Object.assign({}, clusterState, {pendingSplit: false, currentScreenId: clusterID });
  //   const updatedClusters = Object.assign({}, clusters, clusterUpdated );
  //   return update(state, {
  //     clusters: { $set: updatedClusters }
  //   });
  // }
}

module.exports = createReducer;
<|endoftext|>