<commit_before>
/* medea - an Open Source, WebGL-based 3d engine for next-generation browser games.
 * (or alternatively, for clumsy and mostly useless tech demos written solely for fun)
 *
 * medea is (c) 2011, Alexander C. Gessler
 * licensed under the terms and conditions of a 3 clause BSD license.
 */

medea._addMod('node',['frustum'],function(undefined) {
	"use strict";

	medea.BB_INFINITE = 'i';
	medea.BB_EMPTY = 'e';

	medea._NODE_FLAG_DIRTY = 0x1;
	medea._NODE_FLAG_DIRTY_BB = 0x2;
	medea._NODE_FLAG_DIRTY_GI = 0x4;

	medea.NODE_FLAG_NO_ROTATION = 0x40;
	medea.NODE_FLAG_NO_SCALING = 0x80;

	medea.NODE_FLAG_USER = 0x100000;

	var id_source = 0;

	this.Node = medea.Class.extend({

		// this is to allow subclasses to have their own flags set when the node's transformation
		// matrix is altered. By default we only set DIRTY.
		trafo_dirty_flag: medea._NODE_FLAG_DIRTY |
			medea._NODE_FLAG_DIRTY_GI |
			medea._NODE_FLAG_DIRTY_BB,

		parent:null,

		init : function(name, flags) {
			this.children = [];
			this.entities = [];
			this.id = id_source++;
			this.name = name || ("UnnamedNode_" + this.id);

			// for culling purposes, saves the index of the frustun plane
			// that caused this node to be culled recently. This exploits
			// temporal coherence in the scene.
			this.plane_hint = [0];

			this.listeners = {
				'OnUpdateGlobalTransform' : {},
				'OnUpdateBB' : {},
			};

			this.lmatrix = mat4.identity(mat4.create());
			this.gmatrix = mat4.create();
			this.gimatrix = mat4.create();

			this.bb = medea.CreateBB();

			this.flags = this.trafo_dirty_flag | (flags || 0);
			this.enabled = true;
		},

		Enabled : function(e) {
			if (e === undefined) {
				return this.enabled;
			}
			this.enabled = e;
		},

		Name : function(n) {
			if (n === undefined) {
				return this.name;
			}
			this.name = n;
		},

		GetEntities: function() {
			return this.entities;
		},

		GetActiveEntities: function(cam) {
			return this.entities;
		},

		AddEntity: function(ent) {
			// #ifdef DEBUG
			medea.DebugAssert(ent instanceof medea.Entity,'need valid entity to attach');
			// #endif

			this.entities.push(ent);
			ent.OnAttach(this);

			this._SetBBDirty();
		},

		// note: when doing this from within Entity.Update(), return medea.ENTITY_UPDATE_WAS_REMOVED
		RemoveEntity: function(ent) {
			var idx = this.entities.indexOf(ent);
			if(idx !== -1) {
				ent.OnDetach(this);

				this._SetBBDirty();
				this.entities.splice(idx,1);
			}
		},

		RemoveAllEntities: function(tag) {
			if(tag === undefined) {
				for (var i = 0; i < this.entities.length; ++i) {
					this.entities[i].OnDetach(this);
				}

				this.entities = [];
				this._SetBBDirty();
				return;
			}
			for (var i = 0; i < this.entities.length; ++i) {
				var ent = this.entities[i];
				if(ent.Tag() !== tag) {
					continue;
				}

				ent.OnDetach(this);

				this._SetBBDirty();
				this.entities.splice(i,1);
			}
		},

		GetChildren: function() {
			return this.children;
		},

		GetParent: function() {
			return this.parent;
		},

		AddChild: function(child) {
			// #ifdef DEBUG
			medea.DebugAssert(child !== this,'cannot attach a node to itself');
			// #endif

			if(typeof child !== 'object' || !( child instanceof medea.Node )) {
				child = new medea.Node(child);
			}

			if(child.parent === this) {
				return;
			}

			this.children.push(child);
			this._SetBBDirty();

			child.OnAttach(this);
			return child;
		},

		RemoveChild: function(child) {
			var idx = this.children.indexOf(child);
			if(idx !== -1) {
				// #ifdef DEBUG
				medea.DebugAssert(child.parent === this,'inconsistent value for child.parent');
				// #endif

				this._SetBBDirty();
				child.OnAttach(null);

				this.children.splice(idx,1);
				child.parent = null;
			}
		},

		OnAttach : function(parent) {
			// #ifdef DEBUG
			medea.DebugAssert(parent !== this,'cannot attach node to itself');
			// #endif

			this.parent = parent;
			this._SetTrafoDirty();
		},

		Update: function(dtime) {
			// all regular updates are carried out lazily, so this is a no-op
		},

		GetWorldBB: function() {
			this._UpdateBB();
			return this.bb;
		},

		GetBB: function() {
			this._UpdateBB();
			return this.bb;
		},

		Cull : function(frustum) {
			return medea.BBInFrustum(frustum, this.GetWorldBB(), this.plane_hint);
		},

		// pure getter, nowadays deprecated
		GetLocalTransform: function() {
			return this.lmatrix;
		},

		LocalTransform: function(l, no_copy) {
			if(l === undefined) {
				return this.lmatrix;
			}

			this._SetTrafoDirty();

			if(no_copy) {
				this.lmatrix = l;
			}
			else {
				mat4.set(l, this.lmatrix);
			}
		},

		GetGlobalTransform: function() {
			this._UpdateGlobalTransform();
			return this.gmatrix;
		},

		GetInverseGlobalTransform: function() {
			this._UpdateInverseGlobalTransform();
			return this.gimatrix;
		},

		TryGetInverseGlobalTransform: function() {
			return this.flags & medea._NODE_FLAG_DIRTY_GI ? null : this.gimatrix;
		},

		Translate: function(vec) {
			mat4.translate(this.lmatrix,vec);
			this._SetTrafoDirty();
			return this;
		},

		Rotate: function(angle,axis) {
			// #ifdef DEBUG
			medea.DebugAssert(!(this.flags & medea.NODE_FLAG_NO_ROTATION),'node cannot be rotated');
			// #endif

			mat4.rotate(this.lmatrix,angle,axis);
			this._SetTrafoDirty();
			return this;
		},

		Scale: function(s) {
			// #ifdef DEBUG
			medea.DebugAssert(!(this.flags & medea.NODE_FLAG_NO_SCALING),'node cannot be scaled');
			// #endif

			mat4.scale(this.lmatrix, typeof s === 'number' ? [s,s,s] : s);
			this._SetTrafoDirty();
			return this;
		},

		ScaleToFit : function(s) {
			var bb = this.GetBB(), m = Math.max, e;
			medea.DebugAssert(bb.length === 2, 'must be AABB');
			e = m(-bb[0][0],bb[1][0]);
			e = m(e,m(-bb[0][1],bb[1][1]));
			e = m(e,m(-bb[0][2],bb[1][2]));
			if(e > 1e-6) {
				this.Scale((s===undefined?1.0:s)/e);
			}
		},

		Center : function(s) {
			var bb = this.GetBB();
			medea.DebugAssert(bb.length === 2, 'must be AABB');
			var x = bb[1][0] - bb[0][0];
			var y = bb[1][1] - bb[0][1];
			var z = bb[1][2] - bb[0][2];
			var vec = [-(x/2 + bb[0][0]),-(y/2 + bb[0][1]),-(z/2 + bb[0][2])];
			this.Translate(vec);
		},

		ResetTransform: function() {
			this.lmatrix = mat4.identity(mat4.create());
			this._SetTrafoDirty();
			return this;
		},

		LocalXAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[0],this.lmatrix[1],this.lmatrix[2]];
			}
			var m = this.lmatrix;
			m[0] = l[0];
			m[1] = l[1];
			m[2] = l[2];
			this._SetTrafoDirty();
		},

		LocalYAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[4],this.lmatrix[5],this.lmatrix[6]];
			}
			var m = this.lmatrix;
			m[4] = l[0];
			m[5] = l[1];
			m[6] = l[2];
			this._SetTrafoDirty();
		},

		LocalZAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[8],this.lmatrix[9],this.lmatrix[10]];
			}
			var m = this.lmatrix;
			m[8] = l[0];
			m[9] = l[1];
			m[10] = l[2];
			this._SetTrafoDirty();
		},

		LocalPos: function(l) {
			if(l === undefined) {
				return [this.lmatrix[12],this.lmatrix[13],this.lmatrix[14]];
			}
			var m = this.lmatrix;
			m[12] = l[0];
			m[13] = l[1];
			m[14] = l[2];
			this._SetTrafoDirty();
		},

		GetWorldPos : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[12],this.gmatrix[13],this.gmatrix[14]];
		},

		GetWorldXAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[0],this.gmatrix[1],this.gmatrix[2]];
		},

		GetWorldYAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[4],this.gmatrix[5],this.gmatrix[6]];
		},

		GetWorldZAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[8],this.gmatrix[9],this.gmatrix[10]];
		},

		AddListener : function(what,l, key) {
			// #ifdef DEBUG
			if(!(what in this.listeners)) {
				medea.DebugAssert('listener not recognized ' + what);
			}
			// #endif
			this.listeners[what][key] = l;
		},

		RemoveListener : function(key) {
			for(var k in this.listeners) {
				try {
					delete this.listeners[k][key];
				}
				catch(e) {
				}
			}
		},

		_UpdateGlobalTransform: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY)) {
				return;
			}
			this.flags &= ~medea._NODE_FLAG_DIRTY;
			if (this.parent) {
				mat4.multiply(this.parent.GetGlobalTransform(),this.lmatrix,this.gmatrix);
			}
			else {
				this.gmatrix = mat4.create( this.lmatrix );
			}

			this._FireListener("OnUpdateGlobalTransform");
		},

		_UpdateInverseGlobalTransform: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY_GI)) {
				return;
			}

			this._UpdateGlobalTransform();

			this.flags &= ~medea._NODE_FLAG_DIRTY_GI;
			mat4.inverse(this.gmatrix,this.gimatrix);
		},

		_SetTrafoDirty : function() {
			this.flags |= this.trafo_dirty_flag;
			this._SetBBDirty();

			for( var i = 0, c = this.children, l = c.length; i < l; ++i) {
				c[i]._SetTrafoDirty();
			}
		},

		_SetBBDirty : function() {
			this.flags |= medea._NODE_FLAG_DIRTY_BB;
			if (this.parent) {
				this.parent._SetBBDirty();
			}
		},

		_UpdateBB: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY_BB)) {
				return;
			}
			this.flags &= ~medea._NODE_FLAG_DIRTY_BB;
			var bbs = [];

			for( var i = 0, c = this.children, l = c.length; i < l; ++i) {
				bbs.push(c[i].GetWorldBB());
			}

			for( var i = 0, c = this.entities, l = c.length; i < l; ++i) {
				bbs.push(medea.TransformBB( c[i].BB(), this.GetGlobalTransform() ));
			}

			this.bb = medea.MergeBBs(bbs);

			// #ifdef DEBUG
			medea.DebugAssert(!!this.bb,"bounding box computation failed, but it shouldn't have");
			// #endif

			this._FireListener("OnUpdateBB");
			return this.bb;
		},

		_FireListener : function(what) {
			var l = this.listeners[what];
			if(l) {
				for(var k in l) {
					l[k].apply(this,arguments);
				}
			}
		},
	});

	//
	medea.CreateNode = function(name, flags) {
		return new medea.Node(name, flags);
	};
});

<commit_msg>node: add FilterEntities() API.<commit_after>
/* medea - an Open Source, WebGL-based 3d engine for next-generation browser games.
 * (or alternatively, for clumsy and mostly useless tech demos written solely for fun)
 *
 * medea is (c) 2011, Alexander C. Gessler
 * licensed under the terms and conditions of a 3 clause BSD license.
 */

medea._addMod('node',['frustum'],function(undefined) {
	"use strict";

	medea.BB_INFINITE = 'i';
	medea.BB_EMPTY = 'e';

	medea._NODE_FLAG_DIRTY = 0x1;
	medea._NODE_FLAG_DIRTY_BB = 0x2;
	medea._NODE_FLAG_DIRTY_GI = 0x4;

	medea.NODE_FLAG_NO_ROTATION = 0x40;
	medea.NODE_FLAG_NO_SCALING = 0x80;

	medea.NODE_FLAG_USER = 0x100000;

	var id_source = 0;

	this.Node = medea.Class.extend({

		// this is to allow subclasses to have their own flags set when the node's transformation
		// matrix is altered. By default we only set DIRTY.
		trafo_dirty_flag: medea._NODE_FLAG_DIRTY |
			medea._NODE_FLAG_DIRTY_GI |
			medea._NODE_FLAG_DIRTY_BB,

		parent:null,

		init : function(name, flags) {
			this.children = [];
			this.entities = [];
			this.id = id_source++;
			this.name = name || ("UnnamedNode_" + this.id);

			// for culling purposes, saves the index of the frustun plane
			// that caused this node to be culled recently. This exploits
			// temporal coherence in the scene.
			this.plane_hint = [0];

			this.listeners = {
				'OnUpdateGlobalTransform' : {},
				'OnUpdateBB' : {},
			};

			this.lmatrix = mat4.identity(mat4.create());
			this.gmatrix = mat4.create();
			this.gimatrix = mat4.create();

			this.bb = medea.CreateBB();

			this.flags = this.trafo_dirty_flag | (flags || 0);
			this.enabled = true;
		},

		Enabled : function(e) {
			if (e === undefined) {
				return this.enabled;
			}
			this.enabled = e;
		},

		Name : function(n) {
			if (n === undefined) {
				return this.name;
			}
			this.name = n;
		},

		GetEntities: function() {
			return this.entities;
		},

		GetActiveEntities: function(cam) {
			return this.entities;
		},

		AddEntity: function(ent) {
			// #ifdef DEBUG
			medea.DebugAssert(ent instanceof medea.Entity,'need valid entity to attach');
			// #endif

			this.entities.push(ent);
			ent.OnAttach(this);

			this._SetBBDirty();
		},

		// note: when doing this from within Entity.Update(), return medea.ENTITY_UPDATE_WAS_REMOVED
		RemoveEntity: function(ent) {
			var idx = this.entities.indexOf(ent);
			if(idx !== -1) {
				ent.OnDetach(this);

				this._SetBBDirty();
				this.entities.splice(idx,1);
			}
		},

		RemoveAllEntities: function(tag) {
			if(tag === undefined) {
				for (var i = 0; i < this.entities.length; ++i) {
					this.entities[i].OnDetach(this);
				}

				this.entities = [];
				this._SetBBDirty();
				return;
			}
			for (var i = 0; i < this.entities.length; ++i) {
				var ent = this.entities[i];
				if(ent.Tag() !== tag) {
					continue;
				}

				ent.OnDetach(this);

				this._SetBBDirty();
				this.entities.splice(i,1);
			}
		},

		FilterEntities : function(classes, callback) {
			if (Array.isArray(classes)) {
				var ce = classes.length;
				for (var i = 0, e = this.entities.length; i < e; ++i) {
					var ent = this.entities[i];
					for (var c = 0; c < ce; ++c) {
						if (ent instanceof classes[c]) {
							callback(ent);
						}
					}
				}
				return;
			}

			for (var i = 0, e = this.entities.length; i < e; ++i) {
				var ent = this.entities[i];
				if (ent instanceof classes) {
					callback(ent);
				}
			}
		},

		GetChildren: function() {
			return this.children;
		},

		GetParent: function() {
			return this.parent;
		},

		AddChild: function(child) {
			// #ifdef DEBUG
			medea.DebugAssert(child !== this,'cannot attach a node to itself');
			// #endif

			if(typeof child !== 'object' || !( child instanceof medea.Node )) {
				child = new medea.Node(child);
			}

			if(child.parent === this) {
				return;
			}

			this.children.push(child);
			this._SetBBDirty();

			child.OnAttach(this);
			return child;
		},

		RemoveChild: function(child) {
			var idx = this.children.indexOf(child);
			if(idx !== -1) {
				// #ifdef DEBUG
				medea.DebugAssert(child.parent === this,'inconsistent value for child.parent');
				// #endif

				this._SetBBDirty();
				child.OnAttach(null);

				this.children.splice(idx,1);
				child.parent = null;
			}
		},

		OnAttach : function(parent) {
			// #ifdef DEBUG
			medea.DebugAssert(parent !== this,'cannot attach node to itself');
			// #endif

			this.parent = parent;
			this._SetTrafoDirty();
		},

		Update: function(dtime) {
			// all regular updates are carried out lazily, so this is a no-op
		},

		GetWorldBB: function() {
			this._UpdateBB();
			return this.bb;
		},

		GetBB: function() {
			this._UpdateBB();
			return this.bb;
		},

		Cull : function(frustum) {
			return medea.BBInFrustum(frustum, this.GetWorldBB(), this.plane_hint);
		},

		// pure getter, nowadays deprecated
		GetLocalTransform: function() {
			return this.lmatrix;
		},

		LocalTransform: function(l, no_copy) {
			if(l === undefined) {
				return this.lmatrix;
			}

			this._SetTrafoDirty();

			if(no_copy) {
				this.lmatrix = l;
			}
			else {
				mat4.set(l, this.lmatrix);
			}
		},

		GetGlobalTransform: function() {
			this._UpdateGlobalTransform();
			return this.gmatrix;
		},

		GetInverseGlobalTransform: function() {
			this._UpdateInverseGlobalTransform();
			return this.gimatrix;
		},

		TryGetInverseGlobalTransform: function() {
			return this.flags & medea._NODE_FLAG_DIRTY_GI ? null : this.gimatrix;
		},

		Translate: function(vec) {
			mat4.translate(this.lmatrix,vec);
			this._SetTrafoDirty();
			return this;
		},

		Rotate: function(angle,axis) {
			// #ifdef DEBUG
			medea.DebugAssert(!(this.flags & medea.NODE_FLAG_NO_ROTATION),'node cannot be rotated');
			// #endif

			mat4.rotate(this.lmatrix,angle,axis);
			this._SetTrafoDirty();
			return this;
		},

		Scale: function(s) {
			// #ifdef DEBUG
			medea.DebugAssert(!(this.flags & medea.NODE_FLAG_NO_SCALING),'node cannot be scaled');
			// #endif

			mat4.scale(this.lmatrix, typeof s === 'number' ? [s,s,s] : s);
			this._SetTrafoDirty();
			return this;
		},

		ScaleToFit : function(s) {
			var bb = this.GetBB(), m = Math.max, e;
			medea.DebugAssert(bb.length === 2, 'must be AABB');
			e = m(-bb[0][0],bb[1][0]);
			e = m(e,m(-bb[0][1],bb[1][1]));
			e = m(e,m(-bb[0][2],bb[1][2]));
			if(e > 1e-6) {
				this.Scale((s===undefined?1.0:s)/e);
			}
		},

		Center : function(s) {
			var bb = this.GetBB();
			medea.DebugAssert(bb.length === 2, 'must be AABB');
			var x = bb[1][0] - bb[0][0];
			var y = bb[1][1] - bb[0][1];
			var z = bb[1][2] - bb[0][2];
			var vec = [-(x/2 + bb[0][0]),-(y/2 + bb[0][1]),-(z/2 + bb[0][2])];
			this.Translate(vec);
		},

		ResetTransform: function() {
			this.lmatrix = mat4.identity(mat4.create());
			this._SetTrafoDirty();
			return this;
		},

		LocalXAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[0],this.lmatrix[1],this.lmatrix[2]];
			}
			var m = this.lmatrix;
			m[0] = l[0];
			m[1] = l[1];
			m[2] = l[2];
			this._SetTrafoDirty();
		},

		LocalYAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[4],this.lmatrix[5],this.lmatrix[6]];
			}
			var m = this.lmatrix;
			m[4] = l[0];
			m[5] = l[1];
			m[6] = l[2];
			this._SetTrafoDirty();
		},

		LocalZAxis: function(l) {
			if(l === undefined) {
				return [this.lmatrix[8],this.lmatrix[9],this.lmatrix[10]];
			}
			var m = this.lmatrix;
			m[8] = l[0];
			m[9] = l[1];
			m[10] = l[2];
			this._SetTrafoDirty();
		},

		LocalPos: function(l) {
			if(l === undefined) {
				return [this.lmatrix[12],this.lmatrix[13],this.lmatrix[14]];
			}
			var m = this.lmatrix;
			m[12] = l[0];
			m[13] = l[1];
			m[14] = l[2];
			this._SetTrafoDirty();
		},

		GetWorldPos : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[12],this.gmatrix[13],this.gmatrix[14]];
		},

		GetWorldXAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[0],this.gmatrix[1],this.gmatrix[2]];
		},

		GetWorldYAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[4],this.gmatrix[5],this.gmatrix[6]];
		},

		GetWorldZAxis : function() {
			this._UpdateGlobalTransform();
			return [this.gmatrix[8],this.gmatrix[9],this.gmatrix[10]];
		},

		AddListener : function(what,l, key) {
			// #ifdef DEBUG
			if(!(what in this.listeners)) {
				medea.DebugAssert('listener not recognized ' + what);
			}
			// #endif
			this.listeners[what][key] = l;
		},

		RemoveListener : function(key) {
			for(var k in this.listeners) {
				try {
					delete this.listeners[k][key];
				}
				catch(e) {
				}
			}
		},

		_UpdateGlobalTransform: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY)) {
				return;
			}
			this.flags &= ~medea._NODE_FLAG_DIRTY;
			if (this.parent) {
				mat4.multiply(this.parent.GetGlobalTransform(),this.lmatrix,this.gmatrix);
			}
			else {
				this.gmatrix = mat4.create( this.lmatrix );
			}

			this._FireListener("OnUpdateGlobalTransform");
		},

		_UpdateInverseGlobalTransform: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY_GI)) {
				return;
			}

			this._UpdateGlobalTransform();

			this.flags &= ~medea._NODE_FLAG_DIRTY_GI;
			mat4.inverse(this.gmatrix,this.gimatrix);
		},

		_SetTrafoDirty : function() {
			this.flags |= this.trafo_dirty_flag;
			this._SetBBDirty();

			for( var i = 0, c = this.children, l = c.length; i < l; ++i) {
				c[i]._SetTrafoDirty();
			}
		},

		_SetBBDirty : function() {
			this.flags |= medea._NODE_FLAG_DIRTY_BB;
			if (this.parent) {
				this.parent._SetBBDirty();
			}
		},

		_UpdateBB: function() {
			if (!(this.flags & medea._NODE_FLAG_DIRTY_BB)) {
				return;
			}
			this.flags &= ~medea._NODE_FLAG_DIRTY_BB;
			var bbs = [];

			for( var i = 0, c = this.children, l = c.length; i < l; ++i) {
				bbs.push(c[i].GetWorldBB());
			}

			for( var i = 0, c = this.entities, l = c.length; i < l; ++i) {
				bbs.push(medea.TransformBB( c[i].BB(), this.GetGlobalTransform() ));
			}

			this.bb = medea.MergeBBs(bbs);

			// #ifdef DEBUG
			medea.DebugAssert(!!this.bb,"bounding box computation failed, but it shouldn't have");
			// #endif

			this._FireListener("OnUpdateBB");
			return this.bb;
		},

		_FireListener : function(what) {
			var l = this.listeners[what];
			if(l) {
				for(var k in l) {
					l[k].apply(this,arguments);
				}
			}
		},
	});

	//
	medea.CreateNode = function(name, flags) {
		return new medea.Node(name, flags);
	};
});

<|endoftext|>


<commit_before>/*
 * showfor.js
 * Scripts for the showfor browser/os detection.
 *
 * Depends on: browserdetect.js
 * Optional for dhtml selects: jquery.selectbox-1.2.js
 */

(function($) {

"use strict";  // following the Ricky kbox awesomeness

var OSES, BROWSERS, VERSIONS, MISSING_MSG;
var ShowFor = {
    // Return the browser and version that appears to be running. Possible
    // values resemble {fx4, fx35, m1, m11}. Return undefined if the currently
    // running browser can't be identified.
    detectBrowser: function() {
        function getVersionGroup(browser, version) {
            if ((browser === undefined) || (version === undefined) || !VERSIONS[browser]) {
                return;
            }

            for (var i = 0; i < VERSIONS[browser].length; i++) {
                if (version < VERSIONS[browser][i][0]) {
                    return browser + VERSIONS[browser][i][1];
                }
            }
        }
        return getVersionGroup(BrowserDetect.browser, BrowserDetect.version);
    },

    // Treat the hash fragment of the URL as a querystring (e.g.
    // #os=this&browser=that), and return an object with a property for each
    // param. May not handle URL escaping yet.
    hashFragment: function() {
        var o = {},
            args = document.location.hash.substr(1).split('&'),
            chunks;
        for (var i = 0; i < args.length; i++) {
            chunks = args[i].split('=');
            o[chunks[0]] = chunks[1];
        }
        return o;
    },

    // Hide/show the proper page sections that are marked with {for} tags as
    // applying to only certain browsers or OSes. Update the table of contents
    // to reflect what was hidden/shown.
    initForTags: function(options, $container) {
        if (!$container) {
            $container = $('body');
        }
        options = $.extend({
            osSelector: '#os',
            browserSelector: '#browser'
        }, options);
        var self = this,
            $osMenu = $container.find(options.osSelector),
            $browserMenu = $container.find(options.browserSelector),
            $origBrowserOptions = $browserMenu.find('option').clone(),
            $body = $('body'),
            hash = self.hashFragment(),
            isSetManually;

        OSES = $osMenu.data('oses');  // {'mac': true, 'win': true, ...}
        BROWSERS = $browserMenu.data('browsers');  // {'fx4': true, ...}
        VERSIONS = $browserMenu.data('version-groups');  // {'fx': [[3.4999, '3'], [3.9999, '35']], 'm': [[1.0999, '1'], [1.9999, '11']]}
        MISSING_MSG = gettext('[missing header]');

        // Make the 'Table of Contents' header localizable.
        $('#toc > h2').text(gettext('Table of Contents'));

        function updateForsAndToc() {
            // Hide and show document sections accordingly:
            showAndHideFors($osMenu.val(), $browserMenu.val());

            // Update the table of contents in case headers were hidden or shown:
            $('#toc > :not(h2)').remove(); // __TOC__ generates <ul/>'s.
            $('#toc').append(self.filteredToc($('#doc-content'), '#toc h2'));

            return false;
        }

        // Set the {for} nodes to the proper visibility for the given OS and
        // browser combination.
        //
        // Hidden are {for}s that {list at least one OS but not the passed-in one}
        // or that {list at least one browser but not the passed-in one}. Also, the
        // entire condition can be inverted by prefixing it with "not ", as in {for
        // not mac,linux}.
        function showAndHideFors(os, browser) {
            $container.find('.for').each(function(index) {
                var osAttrs = {}, browserAttrs = {},
                    foundAnyOses = false, foundAnyBrowsers = false,
                    forData,
                    isInverted,
                    shouldHide;

                // Catch the "not" operator if it's there:
                forData = $(this).data('for');
                if (!forData) {
                    // If the data-for attribute is missing, move on.
                    return;
                }

                isInverted = forData.substring(0, 4) == 'not ';
                if (isInverted) {
                    forData = forData.substring(4);  // strip off "not "
                }

                // Divide {for} attrs into OSes and browsers:
                $(forData.split(',')).each(function(index) {
                    if (OSES[this] != undefined) {
                        osAttrs[this] = true;
                        foundAnyOses = true;
                    } else if (BROWSERS[this] != undefined) {
                        browserAttrs[this] = true;
                        foundAnyBrowsers = true;
                    }
                });

                shouldHide = ((foundAnyOses && osAttrs[os] == undefined) ||
                             (foundAnyBrowsers && browserAttrs[browser] == undefined)) &&
                             // Special cases ):
                             // TODO: make this easier to maintain somehow?
                             // Show android/m4 on desktop selection
                             !(osAttrs['android'] && os !== 'maemo' /* only one mobile browser ATM */) &&
                             !(browserAttrs['m4'] && browser !== 'm4' && (osAttrs['android'] || !foundAnyOses)) &&
                             // Show win/fx4 on mobile selection
                             !(osAttrs['win'] && (os === 'android' || os == 'maemo') && (browserAttrs['fx4'] || !foundAnyBrowsers)) &&
                             !(browserAttrs['fx4'] && browser === 'm4' && (osAttrs['win'] || !foundAnyOses));

                if ((shouldHide && !isInverted) || (!shouldHide && isInverted)) {
                    $(this).hide();  // saves original visibility, which is nice but not necessary
                }
                else {
                    $(this).show();  // restores original visibility
                }
            });
        }

        function updateShowforSelectors() {
            if ($.fn.selectbox) {
                $browserMenu.siblings('input.selectbox, div.selectbox-wrapper').remove();
                $osMenu.siblings('input.selectbox, div.selectbox-wrapper').remove();
                $browserMenu.selectbox();
                $osMenu.selectbox();
            } else {
                $browserMenu.removeAttr('disabled');
                $osMenu.removeAttr('disabled');
            }
        }

        // If there isn't already a hash for purposes of actual navigation,
        // stick our {for} settings in there.
        function updateHashFragment() {
            var hash = self.hashFragment();

            // Kind of a shortcut. What we really want to know is "Is there anything in the hash fragment that isn't a {for} selector?"
            if (!document.location.hash || hash.hasOwnProperty("os") || hash.hasOwnProperty("browser")) {
                var newHash = "os=" + $osMenu.val() + "&browser=" + $browserMenu.val();
                document.location.replace(document.location.href.split('#')[0] + '#' + newHash);
            }
        }

        // Persist the menu selection in a cookie and hash fragment.
        function persistSelection() {
            $.cookie("for_os", $osMenu.val(), {path: '/'});
            $.cookie("for_browser", $browserMenu.val(), {path: '/'});
            updateHashFragment();
        }

        // Clear the menu selection cookies.
        function clearSelectionCookies() {
            $.cookie("for_os", null, {path: '/'});
            $.cookie("for_browser", null, {path: '/'});
        }

        // Get the dependency based on the currently selected OS
        function getCurrentDependency() {
            return $osMenu.find('[value="' + $osMenu.val() + '"]')
                          .data('dependency');
        }

        //Handle OS->Browser dependencies
        function handleDependencies(evt, noRedirect) {
            var currentDependency = getCurrentDependency(),
                currentBrowser, newBrowser, availableBrowsers;

            if (!noRedirect && $body.is('.mobile, .desktop') &&
                !$body.is('.' + currentDependency)) {
                // If we are on the mobile page and select a desktop OS,
                // redirect to the desktop home page. And vice-versa.
                // TODO: maybe use data-* attrs for the URLs?
                persistSelection();
                var url = document.location.href;
                if ($body.is('.mobile')) {
                    document.location = url.replace('/mobile', '/home');
                } else {
                    document.location = url.replace('/home', '/mobile');
                }
            }

            currentBrowser = $browserMenu.val();
            availableBrowsers = $origBrowserOptions.filter(
                '[data-dependency="' + currentDependency + '"]');
            $browserMenu.empty().append(availableBrowsers);

            // Set browser to same version (frex, m4->fx4), if possible.
            var version = currentBrowser.replace(/^\D+/,'');
            $browserMenu.find('option').each(function() {
                var $this = $(this);
                if ($this.val().replace(/^\D+/,'') == version) {
                    $browserMenu.val($this.val());
                }
            });
            updateShowforSelectors();
        }

        // Select the right item from the browser or OS menu, taking cues from
        // the following places, in order: the URL hash fragment, the cookie,
        // and client detection. Return whether the item appears to have
        // selected manually: that is, via a cookie or a hash fragment.
        function setSelectorValue(cookieName, hashName, hash, detector, $menu) {
            var initial = hash[hashName],
                isManual = true;
            if (!initial) {
                initial = $.cookie(cookieName);
                if (!initial) {
                    initial = detector();
                    isManual = false;
                }
            }
            if (initial) {
                $menu.val(initial);  // does not fire change event
                updateShowforSelectors();
            }
            return isManual;
        }

        // Set the selector value to the first option that doesn't
        // have the passed in dependency.
        function setSelectorDefault($select, dependency) {
            $select.val(
                $select.find('option:not([data-dependency="' + dependency +
                             '"]):first').attr('value'));
        }

        // If we are on mobile or desktop home page, make sure
        // appropriate OS is selected
        function checkSelectorValues() {
            var currentDependency,
                isManual = false;

            if ($body.is('.desktop, .mobile')) {
                currentDependency = getCurrentDependency();
                // currentDependency will be 'desktop' or 'mobile'
                // Make sure we are on the corresponding home page. Otherwise,
                // change the selection appropriately.
                if (!$body.is('.' + currentDependency)) {
                    var $detectedOS = $osMenu.find('[value=' + BrowserDetect.OS + ']');
                    if ($detectedOS.data('dependency') != currentDependency) {
                        // The detected OS is valid. Make it the new selection.
                        $osMenu.val($detectedOS.attr('value'));
                        $browserMenu.val(ShowFor.detectBrowser());
                        clearSelectionCookies();
                    } else {
                        // Force a new selection.
                        setSelectorDefault($osMenu, currentDependency);
                        setSelectorDefault($browserMenu, currentDependency);

                        // Set the cookie so that the selection sticks when
                        // browsing to articles.
                        persistSelection();
                        isManual = true;
                    }
                }
            }
            return isManual;
        }

        // Select the sniffed, cookied, or hashed browser or OS if there is one:
        isSetManually = setSelectorValue("for_os", "os", hash, function() { return BrowserDetect.OS; }, $osMenu);
        isSetManually |= setSelectorValue("for_browser", "browser", hash, ShowFor.detectBrowser, $browserMenu);
        isSetManually |= checkSelectorValues();

        // Possibly change the settings based on dependency rules:
        handleDependencies(null, true);

        if (isSetManually) {
            updateHashFragment();
        }

        $osMenu.change(handleDependencies);
        $osMenu.change(persistSelection);
        $osMenu.change(updateForsAndToc);
        $browserMenu.change(persistSelection);
        $browserMenu.change(updateForsAndToc);

        // Fire off the change handler for the first time:
        updateForsAndToc();

        updateShowforSelectors();
    },

    // Return a table of contents (an <ul>) listing the visible headers within
    // elements in the $pageBody set.
    //
    // The highest header level found within $pageBody is considered to be the
    // top of the TOC: if $pageBody has h2s but no h1s, h2s will be used as the
    // first level of the TOC. Missing headers (such as if you follow an h2
    // directly with an h4) are noted prominently so you can fix them.
    //
    // excludesSelector is an optional jQuery selector for excluding certain
    // headings from the table of contents.
    filteredToc: function($pageBody, excludesSelector) {
        function headerLevel(index, hTag) {
            return parseInt(hTag.tagName.charAt(1), 10);
        }

        var $headers = $pageBody.find(':header:not(:hidden)'),  // :hidden is a little overkill, but it's short.
            $root = $('<ul />'),
            $cur_ul = $root,
            ul_level = Math.min.apply(Math, $headers.map(headerLevel).get());

        // For each header in the document, look upward until you hit something that's hidden. If nothing is found, add the header to the TOC.
        $headers.each(function addIfShown(index) {
            var h_level = headerLevel(0, this),
                $h = $(this);

            if (excludesSelector && $h.is(excludesSelector)) {
                // Skip excluded headers.
                return;
            }

            // If we're too far down the tree, walk up it.
            for (; ul_level > h_level; ul_level--) {
                $cur_ul = $cur_ul.parent().closest('ul');
            }

            // If we're too far up the tree, walk down it, create <ul>s until we aren't:
            for (; ul_level < h_level; ul_level++) {
                var $last_li = $cur_ul.children().last();
                if ($last_li.length === 0) {
                    $last_li = $('<li />').append($('<em />')
                                                  .text(MISSING_MSG))
                                          .appendTo($cur_ul);
                }
                // Now the current <ul> ends in an <li>, one way or another.
                $cur_ul = $('<ul />').appendTo($last_li);
            }

            // Now $cur_ul is at exactly the right level to add a header by appending an <li>.
            // Clone the header, remove any hidden elements and get the text,
            // and replace back with the clone.
            var $tmpClone = $h.clone(),
                text = $h.find(':hidden').remove().end().text();
            $h.replaceWith($tmpClone);
            $cur_ul.append($('<li />').text(text).wrapInner($('<a>').attr('href', '#' + $h.attr('id'))));
        });
        return $root;
    }
};

window.ShowFor = ShowFor;

})(jQuery);
<commit_msg>[bug 638884] Scroll to the right position after showfor hides/shows content, but only on page load.<commit_after>/*
 * showfor.js
 * Scripts for the showfor browser/os detection.
 *
 * Depends on: browserdetect.js
 * Optional for dhtml selects: jquery.selectbox-1.2.js
 */

(function($) {

"use strict";  // following the Ricky kbox awesomeness

var OSES, BROWSERS, VERSIONS, MISSING_MSG;
var ShowFor = {
    // Return the browser and version that appears to be running. Possible
    // values resemble {fx4, fx35, m1, m11}. Return undefined if the currently
    // running browser can't be identified.
    detectBrowser: function() {
        function getVersionGroup(browser, version) {
            if ((browser === undefined) || (version === undefined) || !VERSIONS[browser]) {
                return;
            }

            for (var i = 0; i < VERSIONS[browser].length; i++) {
                if (version < VERSIONS[browser][i][0]) {
                    return browser + VERSIONS[browser][i][1];
                }
            }
        }
        return getVersionGroup(BrowserDetect.browser, BrowserDetect.version);
    },

    // Treat the hash fragment of the URL as a querystring (e.g.
    // #os=this&browser=that), and return an object with a property for each
    // param. May not handle URL escaping yet.
    hashFragment: function() {
        var o = {},
            args = document.location.hash.substr(1).split('&'),
            chunks;
        for (var i = 0; i < args.length; i++) {
            chunks = args[i].split('=');
            o[chunks[0]] = chunks[1];
        }
        return o;
    },

    // Hide/show the proper page sections that are marked with {for} tags as
    // applying to only certain browsers or OSes. Update the table of contents
    // to reflect what was hidden/shown.
    initForTags: function(options, $container) {
        if (!$container) {
            $container = $('body');
        }
        options = $.extend({
            osSelector: '#os',
            browserSelector: '#browser'
        }, options);
        var self = this,
            $osMenu = $container.find(options.osSelector),
            $browserMenu = $container.find(options.browserSelector),
            $origBrowserOptions = $browserMenu.find('option').clone(),
            $body = $('body'),
            hash = self.hashFragment(),
            isSetManually;

        OSES = $osMenu.data('oses');  // {'mac': true, 'win': true, ...}
        BROWSERS = $browserMenu.data('browsers');  // {'fx4': true, ...}
        VERSIONS = $browserMenu.data('version-groups');  // {'fx': [[3.4999, '3'], [3.9999, '35']], 'm': [[1.0999, '1'], [1.9999, '11']]}
        MISSING_MSG = gettext('[missing header]');

        // Make the 'Table of Contents' header localizable.
        $('#toc > h2').text(gettext('Table of Contents'));

        function updateForsAndToc(calledOnLoad) {
            // Hide and show document sections accordingly:
            showAndHideFors($osMenu.val(), $browserMenu.val());

            // Update the table of contents in case headers were hidden or shown:
            $('#toc > :not(h2)').remove(); // __TOC__ generates <ul/>'s.
            $('#toc').append(self.filteredToc($('#doc-content'), '#toc h2'));

            if (true === calledOnLoad) {
                // Called on document load. Scroll to hash if there is one.
                var hash = document.location.hash.substring(1),
                    element;
                if (hash) {
                    element = document.getElementById(hash);
                    if (element) {
                        element.scrollIntoView();
                    }
                }
            }
            return false;
        }

        // Set the {for} nodes to the proper visibility for the given OS and
        // browser combination.
        //
        // Hidden are {for}s that {list at least one OS but not the passed-in one}
        // or that {list at least one browser but not the passed-in one}. Also, the
        // entire condition can be inverted by prefixing it with "not ", as in {for
        // not mac,linux}.
        function showAndHideFors(os, browser) {
            $container.find('.for').each(function(index) {
                var osAttrs = {}, browserAttrs = {},
                    foundAnyOses = false, foundAnyBrowsers = false,
                    forData,
                    isInverted,
                    shouldHide;

                // Catch the "not" operator if it's there:
                forData = $(this).data('for');
                if (!forData) {
                    // If the data-for attribute is missing, move on.
                    return;
                }

                isInverted = forData.substring(0, 4) == 'not ';
                if (isInverted) {
                    forData = forData.substring(4);  // strip off "not "
                }

                // Divide {for} attrs into OSes and browsers:
                $(forData.split(',')).each(function(index) {
                    if (OSES[this] != undefined) {
                        osAttrs[this] = true;
                        foundAnyOses = true;
                    } else if (BROWSERS[this] != undefined) {
                        browserAttrs[this] = true;
                        foundAnyBrowsers = true;
                    }
                });

                shouldHide = ((foundAnyOses && osAttrs[os] == undefined) ||
                             (foundAnyBrowsers && browserAttrs[browser] == undefined)) &&
                             // Special cases ):
                             // TODO: make this easier to maintain somehow?
                             // Show android/m4 on desktop selection
                             !(osAttrs['android'] && os !== 'maemo' /* only one mobile browser ATM */) &&
                             !(browserAttrs['m4'] && browser !== 'm4' && (osAttrs['android'] || !foundAnyOses)) &&
                             // Show win/fx4 on mobile selection
                             !(osAttrs['win'] && (os === 'android' || os == 'maemo') && (browserAttrs['fx4'] || !foundAnyBrowsers)) &&
                             !(browserAttrs['fx4'] && browser === 'm4' && (osAttrs['win'] || !foundAnyOses));

                if ((shouldHide && !isInverted) || (!shouldHide && isInverted)) {
                    $(this).hide();  // saves original visibility, which is nice but not necessary
                }
                else {
                    $(this).show();  // restores original visibility
                }
            });
        }

        function updateShowforSelectors() {
            if ($.fn.selectbox) {
                $browserMenu.siblings('input.selectbox, div.selectbox-wrapper').remove();
                $osMenu.siblings('input.selectbox, div.selectbox-wrapper').remove();
                $browserMenu.selectbox();
                $osMenu.selectbox();
            } else {
                $browserMenu.removeAttr('disabled');
                $osMenu.removeAttr('disabled');
            }
        }

        // If there isn't already a hash for purposes of actual navigation,
        // stick our {for} settings in there.
        function updateHashFragment() {
            var hash = self.hashFragment();

            // Kind of a shortcut. What we really want to know is "Is there anything in the hash fragment that isn't a {for} selector?"
            if (!document.location.hash || hash.hasOwnProperty("os") || hash.hasOwnProperty("browser")) {
                var newHash = "os=" + $osMenu.val() + "&browser=" + $browserMenu.val();
                document.location.replace(document.location.href.split('#')[0] + '#' + newHash);
            }
        }

        // Persist the menu selection in a cookie and hash fragment.
        function persistSelection() {
            $.cookie("for_os", $osMenu.val(), {path: '/'});
            $.cookie("for_browser", $browserMenu.val(), {path: '/'});
            updateHashFragment();
        }

        // Clear the menu selection cookies.
        function clearSelectionCookies() {
            $.cookie("for_os", null, {path: '/'});
            $.cookie("for_browser", null, {path: '/'});
        }

        // Get the dependency based on the currently selected OS
        function getCurrentDependency() {
            return $osMenu.find('[value="' + $osMenu.val() + '"]')
                          .data('dependency');
        }

        //Handle OS->Browser dependencies
        function handleDependencies(evt, noRedirect) {
            var currentDependency = getCurrentDependency(),
                currentBrowser, newBrowser, availableBrowsers;

            if (!noRedirect && $body.is('.mobile, .desktop') &&
                !$body.is('.' + currentDependency)) {
                // If we are on the mobile page and select a desktop OS,
                // redirect to the desktop home page. And vice-versa.
                // TODO: maybe use data-* attrs for the URLs?
                persistSelection();
                var url = document.location.href;
                if ($body.is('.mobile')) {
                    document.location = url.replace('/mobile', '/home');
                } else {
                    document.location = url.replace('/home', '/mobile');
                }
            }

            currentBrowser = $browserMenu.val();
            availableBrowsers = $origBrowserOptions.filter(
                '[data-dependency="' + currentDependency + '"]');
            $browserMenu.empty().append(availableBrowsers);

            // Set browser to same version (frex, m4->fx4), if possible.
            var version = currentBrowser.replace(/^\D+/,'');
            $browserMenu.find('option').each(function() {
                var $this = $(this);
                if ($this.val().replace(/^\D+/,'') == version) {
                    $browserMenu.val($this.val());
                }
            });
            updateShowforSelectors();
        }

        // Select the right item from the browser or OS menu, taking cues from
        // the following places, in order: the URL hash fragment, the cookie,
        // and client detection. Return whether the item appears to have
        // selected manually: that is, via a cookie or a hash fragment.
        function setSelectorValue(cookieName, hashName, hash, detector, $menu) {
            var initial = hash[hashName],
                isManual = true;
            if (!initial) {
                initial = $.cookie(cookieName);
                if (!initial) {
                    initial = detector();
                    isManual = false;
                }
            }
            if (initial) {
                $menu.val(initial);  // does not fire change event
                updateShowforSelectors();
            }
            return isManual;
        }

        // Set the selector value to the first option that doesn't
        // have the passed in dependency.
        function setSelectorDefault($select, dependency) {
            $select.val(
                $select.find('option:not([data-dependency="' + dependency +
                             '"]):first').attr('value'));
        }

        // If we are on mobile or desktop home page, make sure
        // appropriate OS is selected
        function checkSelectorValues() {
            var currentDependency,
                isManual = false;

            if ($body.is('.desktop, .mobile')) {
                currentDependency = getCurrentDependency();
                // currentDependency will be 'desktop' or 'mobile'
                // Make sure we are on the corresponding home page. Otherwise,
                // change the selection appropriately.
                if (!$body.is('.' + currentDependency)) {
                    var $detectedOS = $osMenu.find('[value=' + BrowserDetect.OS + ']');
                    if ($detectedOS.data('dependency') != currentDependency) {
                        // The detected OS is valid. Make it the new selection.
                        $osMenu.val($detectedOS.attr('value'));
                        $browserMenu.val(ShowFor.detectBrowser());
                        clearSelectionCookies();
                    } else {
                        // Force a new selection.
                        setSelectorDefault($osMenu, currentDependency);
                        setSelectorDefault($browserMenu, currentDependency);

                        // Set the cookie so that the selection sticks when
                        // browsing to articles.
                        persistSelection();
                        isManual = true;
                    }
                }
            }
            return isManual;
        }

        // Select the sniffed, cookied, or hashed browser or OS if there is one:
        isSetManually = setSelectorValue("for_os", "os", hash, function() { return BrowserDetect.OS; }, $osMenu);
        isSetManually |= setSelectorValue("for_browser", "browser", hash, ShowFor.detectBrowser, $browserMenu);
        isSetManually |= checkSelectorValues();

        // Possibly change the settings based on dependency rules:
        handleDependencies(null, true);

        if (isSetManually) {
            updateHashFragment();
        }

        $osMenu.change(handleDependencies);
        $osMenu.change(persistSelection);
        $osMenu.change(updateForsAndToc);
        $browserMenu.change(persistSelection);
        $browserMenu.change(updateForsAndToc);

        // Fire off the change handler for the first time:
        updateForsAndToc(true);

        updateShowforSelectors();
    },

    // Return a table of contents (an <ul>) listing the visible headers within
    // elements in the $pageBody set.
    //
    // The highest header level found within $pageBody is considered to be the
    // top of the TOC: if $pageBody has h2s but no h1s, h2s will be used as the
    // first level of the TOC. Missing headers (such as if you follow an h2
    // directly with an h4) are noted prominently so you can fix them.
    //
    // excludesSelector is an optional jQuery selector for excluding certain
    // headings from the table of contents.
    filteredToc: function($pageBody, excludesSelector) {
        function headerLevel(index, hTag) {
            return parseInt(hTag.tagName.charAt(1), 10);
        }

        var $headers = $pageBody.find(':header:not(:hidden)'),  // :hidden is a little overkill, but it's short.
            $root = $('<ul />'),
            $cur_ul = $root,
            ul_level = Math.min.apply(Math, $headers.map(headerLevel).get());

        // For each header in the document, look upward until you hit something that's hidden. If nothing is found, add the header to the TOC.
        $headers.each(function addIfShown(index) {
            var h_level = headerLevel(0, this),
                $h = $(this);

            if (excludesSelector && $h.is(excludesSelector)) {
                // Skip excluded headers.
                return;
            }

            // If we're too far down the tree, walk up it.
            for (; ul_level > h_level; ul_level--) {
                $cur_ul = $cur_ul.parent().closest('ul');
            }

            // If we're too far up the tree, walk down it, create <ul>s until we aren't:
            for (; ul_level < h_level; ul_level++) {
                var $last_li = $cur_ul.children().last();
                if ($last_li.length === 0) {
                    $last_li = $('<li />').append($('<em />')
                                                  .text(MISSING_MSG))
                                          .appendTo($cur_ul);
                }
                // Now the current <ul> ends in an <li>, one way or another.
                $cur_ul = $('<ul />').appendTo($last_li);
            }

            // Now $cur_ul is at exactly the right level to add a header by appending an <li>.
            // Clone the header, remove any hidden elements and get the text,
            // and replace back with the clone.
            var $tmpClone = $h.clone(),
                text = $h.find(':hidden').remove().end().text();
            $h.replaceWith($tmpClone);
            $cur_ul.append($('<li />').text(text).wrapInner($('<a>').attr('href', '#' + $h.attr('id'))));
        });
        return $root;
    }
};

window.ShowFor = ShowFor;

})(jQuery);
<|endoftext|>


<commit_before>const renderFrameElem = require('./renderFrameElem');
const renderCellRowElem = require('./renderCellRowElem');
const renderCellElem = require('./renderCellElem');
const renderFrameButton = require('./renderFrameButton');

const renderDuplicateChannelControls = require('./renderDuplicateChannelControls');
const renderDeleteChannelControls = require('./renderDeleteChannelControls');

module.exports = (way, opts) => {
  // Params:
  //   way
  //   opts
  //     class: string
  //     deleteChannel: fn(channel)
  //     deleteFrame: fn(time)
  //     duplicateChannel: fn(channel)
  //     duplicateFrame: fn(time)
  //     label: string
  //     numbers: bool
  //     numbersBegin: int
  //     setCell: fn(channel, time, value)
  //
  const container = document.createElement('div');
  container.classList.add('way');

  if (typeof opts !== 'object') {
    if (typeof opts === 'string') {
      opts = {
        label: opts
      };
    } else {
      opts = {};
    }
  }

  if (typeof opts.numbersBegin !== 'number') {
    opts.numbersBegin = 0;
  }

  if (typeof opts.class === 'string') {
    container.classList.add(opts.class);
  }

  // Channel controls

  if (typeof opts.duplicateChannel === 'function') {
    container.appendChild(renderDuplicateChannelControls(way, opts.duplicateChannel));
  }

  if (typeof opts.deleteChannel === 'function') {
    container.appendChild(renderDeleteChannelControls(way, opts.deleteChannel));
  }

  // Frames

  for (let t = 0; t < way[0].length; t += 1) {

    // Label for the frame row
    let lab = '';
    if (t === 0 && typeof opts.label === 'string') {
      lab = opts.label;
    } else if (opts.numbers) {
      const rownum = opts.numbersBegin + t;
      if (rownum % 2 === 0) {
        lab = '' + rownum;
      }
    }

    const fr = renderFrameElem(lab);
    const cr = renderCellRowElem(t);

    for (let c = 0; c < way.length; c += 1) {
      const val = way[c][t];
      const cel = renderCellElem(t, c, val, way.length);

      if (typeof opts.setCell === 'function') {
        cel.addEventListener('click', () => {
          opts.setCell(c, t, val ? 0 : 1);
        });
      }
      cr.appendChild(cel);
    }

    fr.appendChild(cr);

    if (typeof opts.deleteFrame === 'function') {
      fr.appendChild(renderFrameButton('–', () => {
        opts.deleteFrame(t);
      }));
    }

    if (typeof opts.duplicateFrame === 'function') {
      fr.appendChild(renderFrameButton('+', () => {
        opts.duplicateFrame(t);
      }));
    }

    container.appendChild(fr);
  }

  return container;
};
<commit_msg>Allow way element to have multiple classes<commit_after>const renderFrameElem = require('./renderFrameElem');
const renderCellRowElem = require('./renderCellRowElem');
const renderCellElem = require('./renderCellElem');
const renderFrameButton = require('./renderFrameButton');

const renderDuplicateChannelControls = require('./renderDuplicateChannelControls');
const renderDeleteChannelControls = require('./renderDeleteChannelControls');

module.exports = (way, opts) => {
  // Params:
  //   way
  //   opts
  //     class: string
  //     deleteChannel: fn(channel)
  //     deleteFrame: fn(time)
  //     duplicateChannel: fn(channel)
  //     duplicateFrame: fn(time)
  //     label: string
  //     numbers: bool
  //     numbersBegin: int
  //     setCell: fn(channel, time, value)
  //
  const container = document.createElement('div');
  container.classList.add('way');

  if (typeof opts !== 'object') {
    if (typeof opts === 'string') {
      opts = {
        label: opts
      };
    } else {
      opts = {};
    }
  }

  if (typeof opts.numbersBegin !== 'number') {
    opts.numbersBegin = 0;
  }

  if (typeof opts.class === 'string') {
    const classes = opts.class.split(' ');
    classes.forEach(className => {
      container.classList.add(className);
    });
  }

  // Channel controls

  if (typeof opts.duplicateChannel === 'function') {
    container.appendChild(renderDuplicateChannelControls(way, opts.duplicateChannel));
  }

  if (typeof opts.deleteChannel === 'function') {
    container.appendChild(renderDeleteChannelControls(way, opts.deleteChannel));
  }

  // Frames

  for (let t = 0; t < way[0].length; t += 1) {

    // Label for the frame row
    let lab = '';
    if (t === 0 && typeof opts.label === 'string') {
      lab = opts.label;
    } else if (opts.numbers) {
      const rownum = opts.numbersBegin + t;
      if (rownum % 2 === 0) {
        lab = '' + rownum;
      }
    }

    const fr = renderFrameElem(lab);
    const cr = renderCellRowElem(t);

    for (let c = 0; c < way.length; c += 1) {
      const val = way[c][t];
      const cel = renderCellElem(t, c, val, way.length);

      if (typeof opts.setCell === 'function') {
        cel.addEventListener('click', () => {
          opts.setCell(c, t, val ? 0 : 1);
        });
      }
      cr.appendChild(cel);
    }

    fr.appendChild(cr);

    if (typeof opts.deleteFrame === 'function') {
      fr.appendChild(renderFrameButton('–', () => {
        opts.deleteFrame(t);
      }));
    }

    if (typeof opts.duplicateFrame === 'function') {
      fr.appendChild(renderFrameButton('+', () => {
        opts.duplicateFrame(t);
      }));
    }

    container.appendChild(fr);
  }

  return container;
};
<|endoftext|>


<commit_before>// @flow
import * as CommonConstants from '../constants/common'
import * as Constants from '../constants/chat'
import * as WindowConstants from '../constants/window'
import {Set, List, Map} from 'immutable'

import type {Actions, State, Message, ConversationState, AppendMessages, ServerMessage, InboxState} from '../constants/chat'

const {StateRecord, ConversationStateRecord, makeSnippet, serverMessageToMessageBody, MetaDataRecord} = Constants
const initialState: State = new StateRecord()
const initialConversation: ConversationState = new ConversationStateRecord()

// _filterMessages dedupes and removed deleted messages
function _filterMessages (seenMessages: Set<any>, messages: List<ServerMessage> = List(), prepend: List<ServerMessage> = List(), append: List<ServerMessage> = List(), deletedIDs: Set<any>): {nextSeenMessages: Set<any>, nextMessages: List<ServerMessage>} {
  const filteredPrepend = prepend.filter(m => !seenMessages.has(m.key))
  const filteredAppend = append.filter(m => !seenMessages.has(m.key))

  const messagesToUpdate = Map(prepend.concat(append).filter(m => seenMessages.has(m.key)).map(m => [m.key, m]))
  const updatedMessages = messages.map(m => messagesToUpdate.has(m.key) ? messagesToUpdate.get(m.key) : m)
  // We have to check for m.messageID being falsey and set.has(undefined) is true!. We shouldn't ever have a zero messageID
  const nextMessages = filteredPrepend.concat(updatedMessages, filteredAppend).filter(m => !m.messageID || !deletedIDs.has(m.messageID))
  const nextSeenMessages = Set(nextMessages.map(m => m.key))

  return {
    nextMessages,
    nextSeenMessages,
  }
}

// _filterTypes separates out deleted message types and returns their ID's
function _filterTypes (inMessages: Array<ServerMessage>): {messages: Array<ServerMessage>, deletedIDs: Array<any>} {
  const messages = []
  const deletedIDs = []
  inMessages.forEach((message, idx) => {
    if (message.type === 'Deleted') {
      deletedIDs.push(...message.deletedIDs)
    } else {
      messages.push(message)
    }
  })
  return {messages, deletedIDs}
}

type ConversationsStates = Map<Constants.ConversationIDKey, ConversationState>
type ConversationUpdateFn = (c: Constants.ConversationState) => Constants.ConversationState
function updateConversation (conversationStates: ConversationsStates, conversationIDKey: Constants.ConversationIDKey, conversationUpdateFn: ConversationUpdateFn): ConversationsStates {
  return conversationStates.update(
    conversationIDKey,
    initialConversation,
    conversationUpdateFn,
  )
}

type MessageUpdateFn = (message: $Subtype<Message>) => Message
type MessageFindPredFn = (message: Message) => boolean
function updateConversationMessage (conversationStates: ConversationsStates, conversationIDKey: Constants.ConversationIDKey, pred: MessageFindPredFn, messageUpdateFn: MessageUpdateFn): ConversationsStates {
  return updateConversation(
    conversationStates,
    conversationIDKey,
    conversation => {
      const index = conversation.get('messages').findIndex(pred)
      if (index < 0) {
        console.warn("Couldn't find an outbox entry to modify")
        return conversation
      }
      // $FlowIssue
      return conversation.updateIn(['messages', index], messageUpdateFn)
    }
  )
}

function reducer (state: State = initialState, action: Actions) {
  switch (action.type) {
    case CommonConstants.resetStore:
      return initialState
    case 'chat:clearMessages': {
      const {conversationIDKey} = action.payload
      const origConversationState = state.get('conversationStates').get(conversationIDKey)
      if (!origConversationState) {
        console.warn('Attempted to clear conversation state that doesn\'t exist')
        return
      }
      const clearedConversationState = initialConversation.set('firstNewMessageID', origConversationState.get('firstNewMessageID'))
      // $FlowIssue
      return state.update('conversationStates', conversationStates =>
        conversationStates.set(conversationIDKey, clearedConversationState)
      )
    }
    case 'chat:prependMessages': {
      const {messages: prependMessages, moreToLoad, paginationNext, conversationIDKey} = action.payload
      const {messages, deletedIDs} = _filterTypes(prependMessages)

      const newConversationStates = state.get('conversationStates').update(
        conversationIDKey,
        initialConversation,
        conversation => {
          const nextDeletedIDs = conversation.get('deletedIDs').add(...deletedIDs)
          const {nextMessages, nextSeenMessages} = _filterMessages(conversation.seenMessages, conversation.messages, List(messages), List(), nextDeletedIDs)

          return conversation
            .set('messages', nextMessages)
            .set('seenMessages', nextSeenMessages)
            .set('moreToLoad', moreToLoad)
            .set('paginationNext', paginationNext)
            .set('deletedIDs', nextDeletedIDs)
            .set('isRequesting', false)
        })

      // Reset the unread count
      const newInboxStates = state.get('inbox').map(inbox => inbox.get('conversationIDKey') !== conversationIDKey ? inbox : inbox.set('unreadCount', 0))

      return state
        .set('conversationStates', newConversationStates)
        .set('inbox', newInboxStates)
    }
    case 'chat:appendMessages': {
      const appendAction: AppendMessages = action
      const appendMessages = appendAction.payload.messages
      const isSelected = action.payload.isSelected
      const message: ServerMessage = appendMessages[appendMessages.length - 1]
      const conversationIDKey = appendAction.payload.conversationIDKey

      const {messages, deletedIDs} = _filterTypes(appendMessages)

      const newConversationStates = state.get('conversationStates').update(
        conversationIDKey,
        initialConversation,
        conversation => {
          const nextDeletedIDs = conversation.get('deletedIDs').add(...deletedIDs)
          const {nextMessages, nextSeenMessages} = _filterMessages(conversation.seenMessages, conversation.messages, List(), List(messages), nextDeletedIDs)

          const firstMessage = appendMessages[0]
          const inConversationFocused = (isSelected && state.get('focused'))
          if (!conversation.get('firstNewMessageID') && !inConversationFocused) {
            // Set first new message if we don't have one set, and are not in
            // the conversation with window focused
            conversation = conversation.set('firstNewMessageID', firstMessage.messageID)
          } else if (inConversationFocused) {
            // Clear new message if we received a new message while in
            // conversation and window is focused
            conversation = conversation.set('firstNewMessageID', null)
          }

          return conversation
            .set('messages', nextMessages)
            .set('seenMessages', nextSeenMessages)
            .set('deletedIDs', nextDeletedIDs)
        })

      const snippet = makeSnippet(serverMessageToMessageBody(message))
      const author = message.type === 'Text' && message.author
      // Update snippets / unread / participant order

      let updatedIdx = -1
      let newInboxStates = state.get('inbox').map((inbox, inboxIdx) => {
        if (inbox.get('conversationIDKey') !== conversationIDKey) {
          return inbox
        }

        updatedIdx = inboxIdx

        let newInbox = inbox
          .set('unreadCount', isSelected ? 0 : inbox.get('unreadCount') + appendMessages.length)
          .set('time', message.timestamp)

        if (snippet) {
          newInbox = newInbox.set('snippet', snippet).set('snippetKey', message.key)
        } else {
          const oldSnippetID = inbox.get('snippetKey')
          // Deleted the current showing snippet
          if (oldSnippetID && deletedIDs.includes(oldSnippetID)) {
            newInbox = newInbox.set('snippet', '').set('snippetKey', null)
          }
        }

        const oldParticipants = newInbox.get('participants')

        if (author && oldParticipants.count() > 1) {
          const idx = oldParticipants.findKey(p => p.username === author)
          if (idx > 0) {
            const newFirst = oldParticipants.get(idx)
            newInbox = newInbox.set('participants', oldParticipants.delete(idx).unshift(newFirst))
          }
        }

        return newInbox
      })

      if (updatedIdx !== -1) {
        const newFirst = newInboxStates.get(updatedIdx)
        newInboxStates = newInboxStates.delete(updatedIdx).unshift(newFirst)
      }

      return state
        .set('conversationStates', newConversationStates)
        .set('inbox', newInboxStates)
    }
    case 'chat:updateTempMessage': {
      if (action.error) {
        console.warn('Error in updateTempMessage')
        const {conversationIDKey, outboxID} = action.payload
        // $FlowIssue
        return state.update('conversationStates', conversationStates => updateConversationMessage(
          conversationStates,
          conversationIDKey,
          item => !!item.outboxID && item.outboxID === outboxID,
          m => ({
            ...m,
            messageState: 'failed',
          })
        ))
      } else {
        const {outboxID, message, conversationIDKey} = action.payload
        // $FlowIssue
        return state.update('conversationStates', conversationStates => updateConversationMessage(
          conversationStates,
          conversationIDKey,
          item => !!item.outboxID && item.outboxID === outboxID,
          m => ({
            ...m,
            ...message,
          })
        )).update('inbox', inbox => inbox.map((i, inboxIdx) => {
          // Update snippetKey to message.messageID so we can clear deleted message snippets
          if (i.get('conversationIDKey') === conversationIDKey) {
            if (i.get('snippetKey') === outboxID && message.messageID) {
              return i.set('snippetKey', message.messageID)
            }
          }
          return i
        })
        )
      }
    }
    case 'chat:markSeenMessage': {
      const {messageID, conversationIDKey} = action.payload
      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversation(
        conversationStates,
        conversationIDKey,
        // $FlowIssue
        conversation => conversation.update('seenMessages', seenMessages => seenMessages.add(messageID))
      ))
    }
    case 'chat:createPendingFailure': {
      const {outboxID} = action.payload
      return state.set('pendingFailures', state.get('pendingFailures').add(outboxID))
    }
    case 'chat:removePendingFailure': {
      const {outboxID} = action.payload
      return state.set('pendingFailures', state.get('pendingFailures').remove(outboxID))
    }
    case 'chat:attachmentLoaded': {
      const {conversationIDKey, messageID, path, isPreview} = action.payload

      const toMerge = isPreview ? {previewPath: path, messageState: 'sent'} : {downloadedPath: path, messageState: 'downloaded'}

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.messageID && item.messageID === messageID,
        m => ({
          ...m,
          ...toMerge,
        })
      ))
    }
    case 'chat:downloadProgress': {
      const {conversationIDKey, messageID, bytesComplete, bytesTotal} = action.payload
      const progress = bytesComplete / bytesTotal

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.messageID && item.messageID === messageID,
        m => ({
          ...m,
          messageState: 'downloading',
          progress,
        })
      ))
    }
    case 'chat:uploadProgress': {
      const {conversationIDKey, outboxID, bytesComplete, bytesTotal} = action.payload
      const progress = bytesComplete / bytesTotal

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.outboxID && item.outboxID === outboxID,
        m => ({
          ...m,
          messageState: 'uploading',
          progress,
        })
      ))
    }
    case 'chat:markThreadsStale': {
      const {convIDKeys} = action.payload
      // $FlowIssue
      return state.update('conversationStates', conversationStates =>
        conversationStates.map((conversationState, conversationIDKey) => {
          if (convIDKeys.length === 0 || convIDKeys.includes(conversationIDKey)) {
            return conversationState.set('isStale', true)
          }
          return conversationState
        })
      )
    }
    case 'chat:updateLatestMessage':
      // Clear new messages id of conversation
      const newConversationStates = state.get('conversationStates').update(
        action.payload.conversationIDKey,
        initialConversation,
        conversation => conversation.set('firstNewMessageID', null))
      state = state.set('conversationStates', newConversationStates)
      return state
    case 'chat:selectConversation':
      const conversationIDKey = action.payload.conversationIDKey

      // Set unread to zero
      const newInboxStates = state.get('inbox').map(inbox => inbox.get('conversationIDKey') !== conversationIDKey ? inbox : inbox.set('unreadCount', 0))
      return state
        .set('inbox', newInboxStates)
    case 'chat:loadingMessages': {
      const newConversationStates = state.get('conversationStates').update(
        action.payload.conversationIDKey,
        initialConversation,
        conversation => conversation.set('isRequesting', true))

      return state.set('conversationStates', newConversationStates)
    }
    case 'chat:updatedMetadata':
      return state.set('metaData', state.get('metaData').merge(action.payload))
    case 'chat:loadedInbox':
      // Don't overwrite existing verified inbox data
      const existingRows = state.get('inbox')
      return state.set('inbox', action.payload.inbox.map(newRow => {
        const id = newRow.get('conversationIDKey')
        const existingRow = existingRows.find(existingRow => existingRow.get('conversationIDKey') === id)
        return existingRow || newRow
      }))
    case 'chat:updateInboxComplete':
      return state.set('inbox', state.get('inbox').filter(i => i.get('validated')))
    case 'chat:updateInbox':
      const convo: InboxState = action.payload.conversation
      const toFind = convo.get('conversationIDKey')
      return state.set('inbox', state.get('inbox').map(i => {
        if (i.get('conversationIDKey') === toFind) {
          return convo
        } else {
          return i
        }
      }))
    case 'chat:updateBrokenTracker':
      const userToBroken = action.payload.userToBroken
      let metaData = state.get('metaData')

      Object.keys(userToBroken).forEach(user => {
        metaData = metaData.update(user, new MetaDataRecord(), old => old.set('brokenTracker', userToBroken[user]))
      })

      return state.set('metaData', metaData)
    case WindowConstants.changedFocus:
      return state.set('focused', action.payload)
  }

  return state
}

export default reducer
<commit_msg>Retain pending messages when clearing threads<commit_after>// @flow
import * as CommonConstants from '../constants/common'
import * as Constants from '../constants/chat'
import * as WindowConstants from '../constants/window'
import {Set, List, Map} from 'immutable'

import type {Actions, State, Message, ConversationState, AppendMessages, ServerMessage, InboxState} from '../constants/chat'

const {StateRecord, ConversationStateRecord, makeSnippet, serverMessageToMessageBody, MetaDataRecord} = Constants
const initialState: State = new StateRecord()
const initialConversation: ConversationState = new ConversationStateRecord()

// _filterMessages dedupes and removed deleted messages
function _filterMessages (seenMessages: Set<any>, messages: List<ServerMessage> = List(), prepend: List<ServerMessage> = List(), append: List<ServerMessage> = List(), deletedIDs: Set<any>): {nextSeenMessages: Set<any>, nextMessages: List<ServerMessage>} {
  const filteredPrepend = prepend.filter(m => !seenMessages.has(m.key))
  const filteredAppend = append.filter(m => !seenMessages.has(m.key))

  const messagesToUpdate = Map(prepend.concat(append).filter(m => seenMessages.has(m.key)).map(m => [m.key, m]))
  const updatedMessages = messages.map(m => messagesToUpdate.has(m.key) ? messagesToUpdate.get(m.key) : m)
  // We have to check for m.messageID being falsey and set.has(undefined) is true!. We shouldn't ever have a zero messageID
  const nextMessages = filteredPrepend.concat(updatedMessages, filteredAppend).filter(m => !m.messageID || !deletedIDs.has(m.messageID))
  const nextSeenMessages = Set(nextMessages.map(m => m.key))

  return {
    nextMessages,
    nextSeenMessages,
  }
}

// _filterTypes separates out deleted message types and returns their ID's
function _filterTypes (inMessages: Array<ServerMessage>): {messages: Array<ServerMessage>, deletedIDs: Array<any>} {
  const messages = []
  const deletedIDs = []
  inMessages.forEach((message, idx) => {
    if (message.type === 'Deleted') {
      deletedIDs.push(...message.deletedIDs)
    } else {
      messages.push(message)
    }
  })
  return {messages, deletedIDs}
}

type ConversationsStates = Map<Constants.ConversationIDKey, ConversationState>
type ConversationUpdateFn = (c: Constants.ConversationState) => Constants.ConversationState
function updateConversation (conversationStates: ConversationsStates, conversationIDKey: Constants.ConversationIDKey, conversationUpdateFn: ConversationUpdateFn): ConversationsStates {
  return conversationStates.update(
    conversationIDKey,
    initialConversation,
    conversationUpdateFn,
  )
}

type MessageUpdateFn = (message: $Subtype<Message>) => Message
type MessageFindPredFn = (message: Message) => boolean
function updateConversationMessage (conversationStates: ConversationsStates, conversationIDKey: Constants.ConversationIDKey, pred: MessageFindPredFn, messageUpdateFn: MessageUpdateFn): ConversationsStates {
  return updateConversation(
    conversationStates,
    conversationIDKey,
    conversation => {
      const index = conversation.get('messages').findIndex(pred)
      if (index < 0) {
        console.warn("Couldn't find an outbox entry to modify")
        return conversation
      }
      // $FlowIssue
      return conversation.updateIn(['messages', index], messageUpdateFn)
    }
  )
}

function reducer (state: State = initialState, action: Actions) {
  switch (action.type) {
    case CommonConstants.resetStore:
      return initialState
    case 'chat:clearMessages': {
      const {conversationIDKey} = action.payload
      const origConversationState = state.get('conversationStates').get(conversationIDKey)
      if (!origConversationState) {
        console.warn('Attempted to clear conversation state that doesn\'t exist')
        return
      }
      const clearedConversationState = initialConversation.merge({
        firstNewMessageID: origConversationState.get('firstNewMessageID'),
        messages: origConversationState.get('messages').filter(m => m.messageState === 'pending'),
      })
      // $FlowIssue
      return state.update('conversationStates', conversationStates =>
        conversationStates.set(conversationIDKey, clearedConversationState)
      )
    }
    case 'chat:prependMessages': {
      const {messages: prependMessages, moreToLoad, paginationNext, conversationIDKey} = action.payload
      const {messages, deletedIDs} = _filterTypes(prependMessages)

      const newConversationStates = state.get('conversationStates').update(
        conversationIDKey,
        initialConversation,
        conversation => {
          const nextDeletedIDs = conversation.get('deletedIDs').add(...deletedIDs)
          const {nextMessages, nextSeenMessages} = _filterMessages(conversation.seenMessages, conversation.messages, List(messages), List(), nextDeletedIDs)

          return conversation
            .set('messages', nextMessages)
            .set('seenMessages', nextSeenMessages)
            .set('moreToLoad', moreToLoad)
            .set('paginationNext', paginationNext)
            .set('deletedIDs', nextDeletedIDs)
            .set('isRequesting', false)
        })

      // Reset the unread count
      const newInboxStates = state.get('inbox').map(inbox => inbox.get('conversationIDKey') !== conversationIDKey ? inbox : inbox.set('unreadCount', 0))

      return state
        .set('conversationStates', newConversationStates)
        .set('inbox', newInboxStates)
    }
    case 'chat:appendMessages': {
      const appendAction: AppendMessages = action
      const appendMessages = appendAction.payload.messages
      const isSelected = action.payload.isSelected
      const message: ServerMessage = appendMessages[appendMessages.length - 1]
      const conversationIDKey = appendAction.payload.conversationIDKey

      const {messages, deletedIDs} = _filterTypes(appendMessages)

      const newConversationStates = state.get('conversationStates').update(
        conversationIDKey,
        initialConversation,
        conversation => {
          const nextDeletedIDs = conversation.get('deletedIDs').add(...deletedIDs)
          const {nextMessages, nextSeenMessages} = _filterMessages(conversation.seenMessages, conversation.messages, List(), List(messages), nextDeletedIDs)

          const firstMessage = appendMessages[0]
          const inConversationFocused = (isSelected && state.get('focused'))
          if (!conversation.get('firstNewMessageID') && !inConversationFocused) {
            // Set first new message if we don't have one set, and are not in
            // the conversation with window focused
            conversation = conversation.set('firstNewMessageID', firstMessage.messageID)
          } else if (inConversationFocused) {
            // Clear new message if we received a new message while in
            // conversation and window is focused
            conversation = conversation.set('firstNewMessageID', null)
          }

          return conversation
            .set('messages', nextMessages)
            .set('seenMessages', nextSeenMessages)
            .set('deletedIDs', nextDeletedIDs)
        })

      const snippet = makeSnippet(serverMessageToMessageBody(message))
      const author = message.type === 'Text' && message.author
      // Update snippets / unread / participant order

      let updatedIdx = -1
      let newInboxStates = state.get('inbox').map((inbox, inboxIdx) => {
        if (inbox.get('conversationIDKey') !== conversationIDKey) {
          return inbox
        }

        updatedIdx = inboxIdx

        let newInbox = inbox
          .set('unreadCount', isSelected ? 0 : inbox.get('unreadCount') + appendMessages.length)
          .set('time', message.timestamp)

        if (snippet) {
          newInbox = newInbox.set('snippet', snippet).set('snippetKey', message.key)
        } else {
          const oldSnippetID = inbox.get('snippetKey')
          // Deleted the current showing snippet
          if (oldSnippetID && deletedIDs.includes(oldSnippetID)) {
            newInbox = newInbox.set('snippet', '').set('snippetKey', null)
          }
        }

        const oldParticipants = newInbox.get('participants')

        if (author && oldParticipants.count() > 1) {
          const idx = oldParticipants.findKey(p => p.username === author)
          if (idx > 0) {
            const newFirst = oldParticipants.get(idx)
            newInbox = newInbox.set('participants', oldParticipants.delete(idx).unshift(newFirst))
          }
        }

        return newInbox
      })

      if (updatedIdx !== -1) {
        const newFirst = newInboxStates.get(updatedIdx)
        newInboxStates = newInboxStates.delete(updatedIdx).unshift(newFirst)
      }

      return state
        .set('conversationStates', newConversationStates)
        .set('inbox', newInboxStates)
    }
    case 'chat:updateTempMessage': {
      if (action.error) {
        console.warn('Error in updateTempMessage')
        const {conversationIDKey, outboxID} = action.payload
        // $FlowIssue
        return state.update('conversationStates', conversationStates => updateConversationMessage(
          conversationStates,
          conversationIDKey,
          item => !!item.outboxID && item.outboxID === outboxID,
          m => ({
            ...m,
            messageState: 'failed',
          })
        ))
      } else {
        const {outboxID, message, conversationIDKey} = action.payload
        // $FlowIssue
        return state.update('conversationStates', conversationStates => updateConversationMessage(
          conversationStates,
          conversationIDKey,
          item => !!item.outboxID && item.outboxID === outboxID,
          m => ({
            ...m,
            ...message,
          })
        )).update('inbox', inbox => inbox.map((i, inboxIdx) => {
          // Update snippetKey to message.messageID so we can clear deleted message snippets
          if (i.get('conversationIDKey') === conversationIDKey) {
            if (i.get('snippetKey') === outboxID && message.messageID) {
              return i.set('snippetKey', message.messageID)
            }
          }
          return i
        })
        )
      }
    }
    case 'chat:markSeenMessage': {
      const {messageID, conversationIDKey} = action.payload
      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversation(
        conversationStates,
        conversationIDKey,
        // $FlowIssue
        conversation => conversation.update('seenMessages', seenMessages => seenMessages.add(messageID))
      ))
    }
    case 'chat:createPendingFailure': {
      const {outboxID} = action.payload
      return state.set('pendingFailures', state.get('pendingFailures').add(outboxID))
    }
    case 'chat:removePendingFailure': {
      const {outboxID} = action.payload
      return state.set('pendingFailures', state.get('pendingFailures').remove(outboxID))
    }
    case 'chat:attachmentLoaded': {
      const {conversationIDKey, messageID, path, isPreview} = action.payload

      const toMerge = isPreview ? {previewPath: path, messageState: 'sent'} : {downloadedPath: path, messageState: 'downloaded'}

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.messageID && item.messageID === messageID,
        m => ({
          ...m,
          ...toMerge,
        })
      ))
    }
    case 'chat:downloadProgress': {
      const {conversationIDKey, messageID, bytesComplete, bytesTotal} = action.payload
      const progress = bytesComplete / bytesTotal

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.messageID && item.messageID === messageID,
        m => ({
          ...m,
          messageState: 'downloading',
          progress,
        })
      ))
    }
    case 'chat:uploadProgress': {
      const {conversationIDKey, outboxID, bytesComplete, bytesTotal} = action.payload
      const progress = bytesComplete / bytesTotal

      // $FlowIssue
      return state.update('conversationStates', conversationStates => updateConversationMessage(
        conversationStates,
        conversationIDKey,
        item => !!item.outboxID && item.outboxID === outboxID,
        m => ({
          ...m,
          messageState: 'uploading',
          progress,
        })
      ))
    }
    case 'chat:markThreadsStale': {
      const {convIDKeys} = action.payload
      // $FlowIssue
      return state.update('conversationStates', conversationStates =>
        conversationStates.map((conversationState, conversationIDKey) => {
          if (convIDKeys.length === 0 || convIDKeys.includes(conversationIDKey)) {
            return conversationState.set('isStale', true)
          }
          return conversationState
        })
      )
    }
    case 'chat:updateLatestMessage':
      // Clear new messages id of conversation
      const newConversationStates = state.get('conversationStates').update(
        action.payload.conversationIDKey,
        initialConversation,
        conversation => conversation.set('firstNewMessageID', null))
      state = state.set('conversationStates', newConversationStates)
      return state
    case 'chat:selectConversation':
      const conversationIDKey = action.payload.conversationIDKey

      // Set unread to zero
      const newInboxStates = state.get('inbox').map(inbox => inbox.get('conversationIDKey') !== conversationIDKey ? inbox : inbox.set('unreadCount', 0))
      return state
        .set('inbox', newInboxStates)
    case 'chat:loadingMessages': {
      const newConversationStates = state.get('conversationStates').update(
        action.payload.conversationIDKey,
        initialConversation,
        conversation => conversation.set('isRequesting', true))

      return state.set('conversationStates', newConversationStates)
    }
    case 'chat:updatedMetadata':
      return state.set('metaData', state.get('metaData').merge(action.payload))
    case 'chat:loadedInbox':
      // Don't overwrite existing verified inbox data
      const existingRows = state.get('inbox')
      return state.set('inbox', action.payload.inbox.map(newRow => {
        const id = newRow.get('conversationIDKey')
        const existingRow = existingRows.find(existingRow => existingRow.get('conversationIDKey') === id)
        return existingRow || newRow
      }))
    case 'chat:updateInboxComplete':
      return state.set('inbox', state.get('inbox').filter(i => i.get('validated')))
    case 'chat:updateInbox':
      const convo: InboxState = action.payload.conversation
      const toFind = convo.get('conversationIDKey')
      return state.set('inbox', state.get('inbox').map(i => {
        if (i.get('conversationIDKey') === toFind) {
          return convo
        } else {
          return i
        }
      }))
    case 'chat:updateBrokenTracker':
      const userToBroken = action.payload.userToBroken
      let metaData = state.get('metaData')

      Object.keys(userToBroken).forEach(user => {
        metaData = metaData.update(user, new MetaDataRecord(), old => old.set('brokenTracker', userToBroken[user]))
      })

      return state.set('metaData', metaData)
    case WindowConstants.changedFocus:
      return state.set('focused', action.payload)
  }

  return state
}

export default reducer
<|endoftext|>