<commit_before>/**
 * @author mrdoob / http://mrdoob.com/
 */

Viewport.Info = function ( editor ) {

	var signals = editor.signals;

	var container = new UI.Panel();
	container.setId( 'info' );
	container.setPosition( 'absolute' );
	container.setLeft( '10px' );
	container.setBottom( '10px' );
	container.setFontSize( '12px' );
	container.setColor( '#fff' );

	var objectsText = new UI.Text( '0' ).setMarginLeft( '6px' );
	var verticesText = new UI.Text( '0' ).setMarginLeft( '6px' );
	var trianglesText = new UI.Text( '0' ).setMarginLeft( '6px' );

	container.add( new UI.Text( 'objects' ), objectsText, new UI.Break() );
	container.add( new UI.Text( 'vertices' ), verticesText, new UI.Break() );
	container.add( new UI.Text( 'triangles' ), trianglesText, new UI.Break() );

	signals.objectAdded.add( update );
	signals.objectRemoved.add( update );
	signals.geometryChanged.add( update );

	//

	function update() {

		var scene = editor.scene;

		var objects = 0, vertices = 0, triangles = 0;

		for ( var i = 0, l = scene.children.length; i < l; i ++ ) {

			var object = scene.children[ i ];

			object.traverseVisible( function ( object ) {

				objects ++;

				if ( object instanceof THREE.Mesh ) {

					var geometry = object.geometry;

					if ( geometry instanceof THREE.Geometry ) {

						vertices += geometry.vertices.length;
						triangles += geometry.faces.length;

					} else if ( geometry instanceof THREE.BufferGeometry ) {

						if ( geometry.index !== null ) {

							vertices += geometry.index.count * 3;
							triangles += geometry.index.count;

						} else {

							vertices += geometry.attributes.position.count;
							triangles += geometry.attributes.position.count / 3;

						}

					}

				}

			} );

		}

		objectsText.setValue( objects.format() );
		verticesText.setValue( vertices.format() );
		trianglesText.setValue( triangles.format() );

	}

	return container;

};
<commit_msg>Editor: Fix triangle counting for indexed buffer geometry.<commit_after>/**
 * @author mrdoob / http://mrdoob.com/
 */

Viewport.Info = function ( editor ) {

	var signals = editor.signals;

	var container = new UI.Panel();
	container.setId( 'info' );
	container.setPosition( 'absolute' );
	container.setLeft( '10px' );
	container.setBottom( '10px' );
	container.setFontSize( '12px' );
	container.setColor( '#fff' );

	var objectsText = new UI.Text( '0' ).setMarginLeft( '6px' );
	var verticesText = new UI.Text( '0' ).setMarginLeft( '6px' );
	var trianglesText = new UI.Text( '0' ).setMarginLeft( '6px' );

	container.add( new UI.Text( 'objects' ), objectsText, new UI.Break() );
	container.add( new UI.Text( 'vertices' ), verticesText, new UI.Break() );
	container.add( new UI.Text( 'triangles' ), trianglesText, new UI.Break() );

	signals.objectAdded.add( update );
	signals.objectRemoved.add( update );
	signals.geometryChanged.add( update );

	//

	function update() {

		var scene = editor.scene;

		var objects = 0, vertices = 0, triangles = 0;

		for ( var i = 0, l = scene.children.length; i < l; i ++ ) {

			var object = scene.children[ i ];

			object.traverseVisible( function ( object ) {

				objects ++;

				if ( object instanceof THREE.Mesh ) {

					var geometry = object.geometry;

					if ( geometry instanceof THREE.Geometry ) {

						vertices += geometry.vertices.length;
						triangles += geometry.faces.length;

					} else if ( geometry instanceof THREE.BufferGeometry ) {

						vertices += geometry.attributes.position.count;

						if ( geometry.index !== null ) {

							triangles += geometry.index.count / 3;

						} else {

							triangles += geometry.attributes.position.count / 3;

						}

					}

				}

			} );

		}

		objectsText.setValue( objects.format() );
		verticesText.setValue( vertices.format() );
		trianglesText.setValue( triangles.format() );

	}

	return container;

};
<|endoftext|>


<commit_before>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		};
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function MenuLabel(label) {
		// Object for manipulating menulabel
		label.click(function(e) {
			e.preventDefault();
		});
		var labeltext;
		function init() {
			labeltext = label.contents().get(0).nodeValue;
		}
		this.obj = label;
		this.addColon = function() {
			// Show colon in label when expanded
			init();
			if(labeltext.indexOf(":") == -1) {
				label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
			}
		};
		this.removeColon = function() {
			// Remove colon in label when collapsed
			init();
			label.contents().get(0).nodeValue = labeltext.replace(":", "");
		};
		this.removeColon();
	}
	function RichSelect(el, opts) {
		// Object for rich menu functionality
		opts = opts || {};
		var settings = $.extend({
			imagePath : ""
		}, opts);
		var imgpath = settings.imagePath;
		var rs = this;
		function MenuBox(menubox) {
			// Add empty element for holding option text
			menubox.prepend("<span />");
			var menutext = menubox.children().first();
			var richopt = menubox.find(".richopt");
			hide(menubox, richopt);
			// Add remove button
			menubox.append($("<span />", {
				"class" : "removeval",
				click : function() {
					rs.removeVal();
				}
			}));
			menubox.children().last().html($("<img />", {
				src : imgpath + "remove.png",
				alt : "remove"
			}));
			this.update = function(option) {
				// Shows selected option. If option is a richopt show that element.
				rs.menulabel.addColon();
				if(option.hasClass("richopt")) {
					menutext.text("");
					show(richopt, menubox);
				}
				else {
					if(!richopt.hasClass("jshidden")) hide(richopt);
					menutext.text(option.text());
					show(menubox);
				}
				rs.menu.position();
			};
			this.show = function() {
				rs.menulabel.addColon();
				show(menubox);
			};
			this.hide = function() {
				rs.menulabel.removeColon();
				hide(menubox);
			};
			this.reset = function() {
				this.hide();
				menutext.text("");
			};
			this.focus = function() {
				if(!visible(richopt)) return false;
				var inputs = richopt.find("input");
				// Select form element for added usability
				if(inputs.size()) {
					inputs.first().select();
				}
				return true;
			};
		}
		function Menu(menu) {
			var self = this;
			menu = (menu && menu.size()) ? menu : null; // Check for existence of menu
			this.name = menu ? menu.attr("name") : "";
			this.position = function() {
				// Set menu to show below button
				if(menu) menu.css("top", el.innerHeight() - 2);
			};
			this.visible = function() {
				return menu ? visible(menu) : false;
			};
			this.show = function() {
				if(menu) {
					show(menu);
					menu.focus();
				}
				else rs.menubox.show();
			};
			this.hide = function() {
				if(menu) {
					hide(menu);
					rs.menubox.focus();
				}
				else rs.menubox.hide();
			};
			this.update = function() {
				// Update selected menu option from valstore value
				if(!menu) return false;
				var option;
				var found = false;
				if(rs.valstore.exists()) {
					menu.children().each(function(i, el) {
						option = $(el);
						if(option.val() === rs.valstore.val()) {
							self.selected(option);
							found = true;
							return false;
						}
					});
				}
				else {
					option = this.selected();
					if(option) found = true;
				}
				if(found) {
					rs.menubox.update(option);
				}
				return found;
			};
			this.selected = function(o) {
				if(!menu) return null;
				if(o) {
					o.attr("selected", "selected");
					return o;
				}
				o = menu.children("option:selected");
				if(!o.size()) o = null;
				return o;
			};
			this.reset = function() {
				if(!menu) return false;
				menu.get(0).selectedIndex = -1;
				hide(menu);
				this.position();
				return true;
			};
			this.val = function() {
				var val = "";
				var o = menu.selected();
				if(o) val = o.val();
				return val;
			};
			if(menu) {
				// Event handlers keep menu and valstore in sync
				menu.click(function(e) {
					rs.valstore.update();
					// The richselect click handler hides the menu
				});
				menu.change(function(e) {
					rs.valstore.update();
				});
				menu.jkey('enter', function() {
					rs.valstore.update();
					self.hide();
				});
				// Prevent pressing enter in the menu from submitting the form
				menu.keypress(function(e) {
					if(e.which == 13) e.preventDefault();
				});
				hide(menu);
				// Make into multiline select
				menu.attr("size", menu.children().size());
				this.position();
			}
		}
		function Valstore(valstore) {
			valstore = valstore.size() ? valstore : null; // Check for existence of valstore
			this.name = valstore ? valstore.attr("name") : "";
			this.exists = function() {
				return valstore ? true : false;
			};
			this.val = function() {
				return valstore ? valstore.val() : "";
			};
			this.update = function() {
				// Update valstore from selected menu option
				if(!valstore) return false;
				var option = rs.menu.selected();
				if(option) {
					rs.menubox.update(option);
					valstore.val(option.val());
				}
				return true;
			};
			this.reset = function() {
				if(!valstore) return false;
				valstore.val("");
				return true;
			};
			if(valstore) {
				valstore.change(function() {
					rs.menu.update();
				});
			}
		}
		this.getVal = function() {
			var val;
			if(rs.valstore.exists()) {
				val = rs.valstore.val();
			}
			else {
				val = rs.menu.val();
			}
			return val;
		};
		this.removeVal = function() {
			// Call event and if it returns false cancel
			if(typeof opts.removeVal === "function") {
				if(opts.removeVal(rs) === false) return false;
			}
			// Remove value and collapse menu button to initial form
			rs.menubox.reset();
			rs.valstore.reset();
			rs.menu.reset();
			return true;
		};
		this.menulabel = new MenuLabel(el.find(".showmenu"));
		this.menubox = new MenuBox(el.find(".menubox"));
		this.menu = new Menu(el.find(".menu"));
		this.valstore = new Valstore(el.find(".valstore"));
		this.menu.update();
		this.name = (this.valstore.exists() ? this.valstore.name : this.menu.name);
		// Make text unselectable in IE and Opera
		if(typeof el.get(0).unselectable !== "undefined") {
			el.get(0).unselectable = "on";
			el.find("*").each(function(i, el) {
				if(!$(el).is("input, select, textarea")) el.unselectable = "on";
			});
		}
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(rs.menu.visible())
				rs.menu.hide();
			else
				rs.menu.show();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			rs.menu.show();
		});
		// Add dropdown image
		el.append($("<img />", {
			src : imgpath + "dropdown.png",
			alt : "dropdown"
		}));
	}
	// Array holding all rich menus
	var selects = [];
	$(function() {
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(selects[i].menu.visible()) selects[i].menu.hide();
			}
		});
	});
	// jQuery plugin
	$.richselect = $.richselect || function(selector, opts, callback) {
		opts = opts || {};
		var a = [];
		$(selector).each(function(i, el) {
			var select = new RichSelect($(el), opts);
			a[a.length] = select;
			selects[selects.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	};
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<commit_msg>Update menubox even though there is no valstore.<commit_after>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		};
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function MenuLabel(label) {
		// Object for manipulating menulabel
		label.click(function(e) {
			e.preventDefault();
		});
		var labeltext;
		function init() {
			labeltext = label.contents().get(0).nodeValue;
		}
		this.obj = label;
		this.addColon = function() {
			// Show colon in label when expanded
			init();
			if(labeltext.indexOf(":") == -1) {
				label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
			}
		};
		this.removeColon = function() {
			// Remove colon in label when collapsed
			init();
			label.contents().get(0).nodeValue = labeltext.replace(":", "");
		};
		this.removeColon();
	}
	function RichSelect(el, opts) {
		// Object for rich menu functionality
		opts = opts || {};
		var settings = $.extend({
			imagePath : ""
		}, opts);
		var imgpath = settings.imagePath;
		var rs = this;
		function MenuBox(menubox) {
			// Add empty element for holding option text
			menubox.prepend("<span />");
			var menutext = menubox.children().first();
			var richopt = menubox.find(".richopt");
			hide(menubox, richopt);
			// Add remove button
			menubox.append($("<span />", {
				"class" : "removeval",
				click : function() {
					rs.removeVal();
				}
			}));
			menubox.children().last().html($("<img />", {
				src : imgpath + "remove.png",
				alt : "remove"
			}));
			this.update = function(option) {
				// Shows selected option. If option is a richopt show that element.
				rs.menulabel.addColon();
				if(option.hasClass("richopt")) {
					menutext.text("");
					show(richopt, menubox);
				}
				else {
					if(!richopt.hasClass("jshidden")) hide(richopt);
					menutext.text(option.text());
					show(menubox);
				}
				rs.menu.position();
			};
			this.show = function() {
				rs.menulabel.addColon();
				show(menubox);
			};
			this.hide = function() {
				rs.menulabel.removeColon();
				hide(menubox);
			};
			this.reset = function() {
				this.hide();
				menutext.text("");
			};
			this.focus = function() {
				if(!visible(richopt)) return false;
				var inputs = richopt.find("input");
				// Select form element for added usability
				if(inputs.size()) {
					inputs.first().select();
				}
				return true;
			};
		}
		function Menu(menu) {
			var self = this;
			menu = (menu && menu.size()) ? menu : null; // Check for existence of menu
			this.name = menu ? menu.attr("name") : "";
			this.position = function() {
				// Set menu to show below button
				if(menu) menu.css("top", el.innerHeight() - 2);
			};
			this.visible = function() {
				return menu ? visible(menu) : false;
			};
			this.show = function() {
				if(menu) {
					show(menu);
					menu.focus();
				}
				else rs.menubox.show();
			};
			this.hide = function() {
				if(menu) {
					hide(menu);
					rs.menubox.focus();
				}
				else rs.menubox.hide();
			};
			this.update = function() {
				// Update selected menu option from valstore value
				if(!menu) return false;
				var option;
				var found = false;
				if(rs.valstore.exists()) {
					menu.children().each(function(i, el) {
						option = $(el);
						if(option.val() === rs.valstore.val()) {
							self.selected(option);
							found = true;
							return false;
						}
					});
				}
				else {
					option = this.selected();
					if(option) found = true;
				}
				if(found) {
					rs.menubox.update(option);
				}
				return found;
			};
			this.selected = function(o) {
				if(!menu) return null;
				if(o) {
					o.attr("selected", "selected");
					return o;
				}
				o = menu.children("option:selected");
				if(!o.size()) o = null;
				return o;
			};
			this.reset = function() {
				if(!menu) return false;
				menu.get(0).selectedIndex = -1;
				hide(menu);
				this.position();
				return true;
			};
			this.val = function() {
				var val = "";
				var o = menu.selected();
				if(o) val = o.val();
				return val;
			};
			if(menu) {
				// Event handlers keep menu and valstore in sync
				menu.click(function(e) {
					rs.valstore.update();
					// The richselect click handler hides the menu
				});
				menu.change(function(e) {
					rs.valstore.update();
				});
				menu.jkey('enter', function() {
					rs.valstore.update();
					self.hide();
				});
				// Prevent pressing enter in the menu from submitting the form
				menu.keypress(function(e) {
					if(e.which == 13) e.preventDefault();
				});
				hide(menu);
				// Make into multiline select
				menu.attr("size", menu.children().size());
				this.position();
			}
		}
		function Valstore(valstore) {
			valstore = valstore.size() ? valstore : null; // Check for existence of valstore
			this.name = valstore ? valstore.attr("name") : "";
			this.exists = function() {
				return valstore ? true : false;
			};
			this.val = function() {
				return valstore ? valstore.val() : "";
			};
			this.update = function() {
				// Update valstore from selected menu option
				var option = rs.menu.selected();
				if(option) {
					rs.menubox.update(option);
					if(valstore) valstore.val(option.val());
				}
				return valstore;
			};
			this.reset = function() {
				if(!valstore) return false;
				valstore.val("");
				return true;
			};
			if(valstore) {
				valstore.change(function() {
					rs.menu.update();
				});
			}
		}
		this.getVal = function() {
			var val;
			if(rs.valstore.exists()) {
				val = rs.valstore.val();
			}
			else {
				val = rs.menu.val();
			}
			return val;
		};
		this.removeVal = function() {
			// Call event and if it returns false cancel
			if(typeof opts.removeVal === "function") {
				if(opts.removeVal(rs) === false) return false;
			}
			// Remove value and collapse menu button to initial form
			rs.menubox.reset();
			rs.valstore.reset();
			rs.menu.reset();
			return true;
		};
		this.menulabel = new MenuLabel(el.find(".showmenu"));
		this.menubox = new MenuBox(el.find(".menubox"));
		this.menu = new Menu(el.find(".menu"));
		this.valstore = new Valstore(el.find(".valstore"));
		this.menu.update();
		this.name = (this.valstore.exists() ? this.valstore.name : this.menu.name);
		// Make text unselectable in IE and Opera
		if(typeof el.get(0).unselectable !== "undefined") {
			el.get(0).unselectable = "on";
			el.find("*").each(function(i, el) {
				if(!$(el).is("input, select, textarea")) el.unselectable = "on";
			});
		}
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(rs.menu.visible())
				rs.menu.hide();
			else
				rs.menu.show();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			rs.menu.show();
		});
		// Add dropdown image
		el.append($("<img />", {
			src : imgpath + "dropdown.png",
			alt : "dropdown"
		}));
	}
	// Array holding all rich menus
	var selects = [];
	$(function() {
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(selects[i].menu.visible()) selects[i].menu.hide();
			}
		});
	});
	// jQuery plugin
	$.richselect = $.richselect || function(selector, opts, callback) {
		opts = opts || {};
		var a = [];
		$(selector).each(function(i, el) {
			var select = new RichSelect($(el), opts);
			a[a.length] = select;
			selects[selects.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	};
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<|endoftext|>


<commit_before>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		}
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	var toggle = changeMultiple("toggleClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function RichSelect(el) {
		// Object for rich menu functionality
		var name, showmenu, menubox, menutext, richopt, menu, valstore;
		var showMenu = function() {
			var el = menu ? menu : menubox;
			show(el);
			if(menu) {
				menu.focus();
			}
		}
		var hideMenu = function() {
			var el = menu ? menu : menubox;
			hide(el);
			var inputs = richopt.find("input");
			// Select form element for added usability
			if(inputs.size()) {
				inputs.first().select();
			}
		}
		var getVal = function() {
			var val;
			if(valstore) {
				val = valstore.val();
			}
			else {
				var option = menu.children("option:selected");
				if(option.size()) val = option.val();
			}
			return val;
		}
		var updateMenubox = function(option) {
			// Shows selected option. If option is a richopt show that element.
			showmenu.addColon();
			option = option || menu.children("option:selected");
			if(option.hasClass("richopt")) {
				menutext.text("");
				show(richopt, menubox);
			}
			else {
				if(!richopt.hasClass("jshidden")) hide(richopt);
				menutext.text(option.text());
				show(menubox);
			}
		}
		var updateMenu = function() {
			// Update selected menu option from valstore value
			if(!menu) return;
			var option;
			var found = false;
			if(valstore) {
				menu.children().each(function(i, el) {
					option = $(el);
					if(option.val() === valstore.val()) {
						option.attr("selected", "selected");
						found = true;
						return false;
					}
				});
			}
			else {
				option = menu.children("option:selected");
				if(option.size()) found = true;
			}
			if(found) updateMenubox(option);
		}
		var updateValstore = function() {
			// Update valstore from selected menu option
			var option = menu.children("option:selected");
			if(option.size()) {
				updateMenubox(option);
				if(valstore) valstore.val(option.val());
			}
		}
		var removeVal = function() {
			// Remove value and collapse menu button to initial form
			showmenu.removeColon();
			hide(menubox);
			menutext.text("");
			if(valstore) valstore.val("");
			if(menu) {
				menu.get(0).selectedIndex = -1;
				hide(menu);
			}
		}
		showmenu = (function() {
			// Object for manipulating showmenu
			var label = el.find(".showmenu");
			label.click(function(e) {
				e.preventDefault();
			});
			var labeltext;
			function init() {
				labeltext = label.contents().get(0).nodeValue;
			}
			return {
				addColon : function() {
					// Show colon in label when expanded
					init();
					if(labeltext.indexOf(":") == -1) {
						label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
					}
				},
				removeColon : function() {
					// Remove colon in label when collapsed
					init();
					label.contents().get(0).nodeValue = labeltext.replace(":", "");
				}
			}
		})();
		showmenu.removeColon();
		menubox = el.find(".menubox");
		// Add empty element for holding option text
		menubox.prepend("<span />");
		menutext = menubox.children().first();
		richopt = menubox.find(".richopt");
		menu = el.find(".menu");
		menu = menu.size() ? menu : null; // Check for existence of menu
		hide(menubox, richopt, menu);
		valstore = menubox.find(".valstore");
		valstore = valstore.size() ? valstore : null; // Check for existence of valstore
		name = (valstore ? valstore.attr("name") : menu.attr("name"));
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(visible(menu))
				hideMenu();
			else
				showMenu();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			showMenu();
		});
		// Make into multiline select
		menu.attr("size", menu.children().size());
		// Event handlers keep menu and valstore in sync
		menu.click(function(e) {
			updateValstore();
			// The richselect click handler hides the menu
		});
		menu.change(function(e) {
			updateValstore();
		});
		menu.jkey('enter', function() {
			updateValstore();
			hideMenu();
		});
		// Prevent pressing enter in the menu from submitting the form
		menu.keypress(function(e) {
			if(e.which == 13) e.preventDefault();
		});
		valstore.change(function() {
			updateMenu();
		});
		// Add remove button
		menubox.append($("<img />", {
			src : "remove.png",
			alt : "remove",
			"class" : "removeval",
			click : function() {
				removeVal();
			}
		}));
		// Add dropdown image
		el.append($("<img />", {
			src : "dropdown.png",
			alt : "dropdown"
		}));
		// Public object properties and methods (pretty much everything by now)
		return {
			menu : menu,
			menubox : menubox,
			menutext : menutext,
			valstore : valstore,
			name : name,
			showMenu : showMenu,
			hideMenu : hideMenu,
			getVal : getVal,
			updateMenubox : updateMenubox,
			updateMenu : updateMenu,
			updateValstore : updateValstore,
			removeVal : removeVal
		}
	}
	$(function() {
		// Reset form
		$("form").get(0).reset();
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(visible(selects[i].menu)) selects[i].hideMenu();
			}
		});
		docready = true;
	});
	// Array holding all rich menus
	var selects = [];
	// jQuery plugin
	$.richselect = $.richselect || function(selector, callback) {
		var a = [];
		$(selector).each(function(i, el) {
			var select = RichSelect($(el));
			a[a.length] = select;
			selects[select.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	}
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<commit_msg>Remove setting obsolete docready variable.<commit_after>(function($) {
	function changeMultiple(method, arg) {
		// Apply the same method and arg to multiple objects
		return function() {
			var args = arguments;
			if(!args.length) return false; // no args
			for(var i = 0; i < args.length; i++) {
				if(args[i] && args[i][method]) args[i][method](arg);
			}
			return true;
		}
	}
	var show = changeMultiple("removeClass", "jshidden");
	var hide = changeMultiple("addClass", "jshidden");
	var toggle = changeMultiple("toggleClass", "jshidden");
	function visible(el) {
		return (el ? !el.hasClass("jshidden") : null);
	}
	function RichSelect(el) {
		// Object for rich menu functionality
		var name, showmenu, menubox, menutext, richopt, menu, valstore;
		var showMenu = function() {
			var el = menu ? menu : menubox;
			show(el);
			if(menu) {
				menu.focus();
			}
		}
		var hideMenu = function() {
			var el = menu ? menu : menubox;
			hide(el);
			var inputs = richopt.find("input");
			// Select form element for added usability
			if(inputs.size()) {
				inputs.first().select();
			}
		}
		var getVal = function() {
			var val;
			if(valstore) {
				val = valstore.val();
			}
			else {
				var option = menu.children("option:selected");
				if(option.size()) val = option.val();
			}
			return val;
		}
		var updateMenubox = function(option) {
			// Shows selected option. If option is a richopt show that element.
			showmenu.addColon();
			option = option || menu.children("option:selected");
			if(option.hasClass("richopt")) {
				menutext.text("");
				show(richopt, menubox);
			}
			else {
				if(!richopt.hasClass("jshidden")) hide(richopt);
				menutext.text(option.text());
				show(menubox);
			}
		}
		var updateMenu = function() {
			// Update selected menu option from valstore value
			if(!menu) return;
			var option;
			var found = false;
			if(valstore) {
				menu.children().each(function(i, el) {
					option = $(el);
					if(option.val() === valstore.val()) {
						option.attr("selected", "selected");
						found = true;
						return false;
					}
				});
			}
			else {
				option = menu.children("option:selected");
				if(option.size()) found = true;
			}
			if(found) updateMenubox(option);
		}
		var updateValstore = function() {
			// Update valstore from selected menu option
			var option = menu.children("option:selected");
			if(option.size()) {
				updateMenubox(option);
				if(valstore) valstore.val(option.val());
			}
		}
		var removeVal = function() {
			// Remove value and collapse menu button to initial form
			showmenu.removeColon();
			hide(menubox);
			menutext.text("");
			if(valstore) valstore.val("");
			if(menu) {
				menu.get(0).selectedIndex = -1;
				hide(menu);
			}
		}
		showmenu = (function() {
			// Object for manipulating showmenu
			var label = el.find(".showmenu");
			label.click(function(e) {
				e.preventDefault();
			});
			var labeltext;
			function init() {
				labeltext = label.contents().get(0).nodeValue;
			}
			return {
				addColon : function() {
					// Show colon in label when expanded
					init();
					if(labeltext.indexOf(":") == -1) {
						label.contents().get(0).nodeValue = $.trim(labeltext) + ":";
					}
				},
				removeColon : function() {
					// Remove colon in label when collapsed
					init();
					label.contents().get(0).nodeValue = labeltext.replace(":", "");
				}
			}
		})();
		showmenu.removeColon();
		menubox = el.find(".menubox");
		// Add empty element for holding option text
		menubox.prepend("<span />");
		menutext = menubox.children().first();
		richopt = menubox.find(".richopt");
		menu = el.find(".menu");
		menu = menu.size() ? menu : null; // Check for existence of menu
		hide(menubox, richopt, menu);
		valstore = menubox.find(".valstore");
		valstore = valstore.size() ? valstore : null; // Check for existence of valstore
		name = (valstore ? valstore.attr("name") : menu.attr("name"));
		// Event handlers for menu button
		el.click(function(e) {
			var target = $(e.target);
			if(target.is(".menubox label") || target.is("input") || target.is(".removeval")) {
				return;
			}
			if(visible(menu))
				hideMenu();
			else
				showMenu();
			e.stopPropagation(); // Don't let the document close the menu again
		});
		el.jkey('space', function() {
			showMenu();
		});
		// Make into multiline select
		menu.attr("size", menu.children().size());
		// Event handlers keep menu and valstore in sync
		menu.click(function(e) {
			updateValstore();
			// The richselect click handler hides the menu
		});
		menu.change(function(e) {
			updateValstore();
		});
		menu.jkey('enter', function() {
			updateValstore();
			hideMenu();
		});
		// Prevent pressing enter in the menu from submitting the form
		menu.keypress(function(e) {
			if(e.which == 13) e.preventDefault();
		});
		valstore.change(function() {
			updateMenu();
		});
		// Add remove button
		menubox.append($("<img />", {
			src : "remove.png",
			alt : "remove",
			"class" : "removeval",
			click : function() {
				removeVal();
			}
		}));
		// Add dropdown image
		el.append($("<img />", {
			src : "dropdown.png",
			alt : "dropdown"
		}));
		// Public object properties and methods (pretty much everything by now)
		return {
			menu : menu,
			menubox : menubox,
			menutext : menutext,
			valstore : valstore,
			name : name,
			showMenu : showMenu,
			hideMenu : hideMenu,
			getVal : getVal,
			updateMenubox : updateMenubox,
			updateMenu : updateMenu,
			updateValstore : updateValstore,
			removeVal : removeVal
		}
	}
	$(function() {
		// Reset form
		$("form").get(0).reset();
		// Close any open menu
		$(document).click(function() {
			for(var i = 0; i < selects.length; i++) {
				if(visible(selects[i].menu)) selects[i].hideMenu();
			}
		});
	});
	// Array holding all rich menus
	var selects = [];
	// jQuery plugin
	$.richselect = $.richselect || function(selector, callback) {
		var a = [];
		$(selector).each(function(i, el) {
			var select = RichSelect($(el));
			a[a.length] = select;
			selects[select.length] = select;
			if(typeof callback === "function") callback(select);
		});
		return a;
	}
	$.richselect.selects = $.richselect.selects || selects;
})(jQuery);<|endoftext|>


<commit_before>/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Externs for the $q service in Angular 1.4
 * NOTE: Due to a JS compiler bug, any use of a templated class must occur after
 * the class is defined. Please be careful with the ordering of the classes and
 * functions.
 * @see https://docs.angularjs.org/api/ng/service/$q
 * @externs
 */

/******************************************************************************
 * $q Service
 *****************************************************************************/

/**
 * @constructor
 */
angular.$q = function() {};

/**
 * @constructor
 * @template T
 */
angular.$q.Promise = function() {};

/**
 * Apply Type Transformation Language to allow more accurate templated type
 * definition.
 * This is copied from goog.Thenable.prototype.then, with the only
 * difference being the raw type as angular.$q.Promise instead of goog.Promise.
 *
 * @param {?(function(this:THIS, T): VALUE)=} opt_onFulfilled
 * @param {?(function(?): ?)=} opt_onRejected
 * @param {?(function(?): ?)=} opt_notifyCallback
 * @return {RESULT}
 * @template THIS
 * @template VALUE
 * @template RESULT := type('angular.$q.Promise',
 *     cond(isUnknown(VALUE), unknown(),
 *       mapunion(VALUE, (V) =>
 *         cond(isTemplatized(V) && sub(rawTypeOf(V), 'IThenable'),
 *           templateTypeOf(V, 0),
 *           cond(sub(V, 'angular.$q.Promise'),
 *              unknown(),
 *              V)))))
 *  =:
 */
angular.$q.Promise.prototype.then =
    function(opt_onFulfilled, opt_onRejected, opt_notifyCallback) {};

/**
 * @param {?function(?)} callback
 * @return {!angular.$q.Promise.<T>}
 */
angular.$q.Promise.prototype.catch = function(callback) {};

/**
 * @param {?function(?)} callback
 * @param {?(function(?): ?)=} opt_notifyCallback
 * @return {!angular.$q.Promise.<T>}
 */
angular.$q.Promise.prototype.finally =
    function(callback, opt_notifyCallback) {};

/**
 * @constructor
 * @template T
 */
angular.$q.Deferred = function() {};

/** @param {T=} opt_value */
angular.$q.Deferred.prototype.resolve = function(opt_value) {};

/** @param {?=} opt_reason */
angular.$q.Deferred.prototype.reject = function(opt_reason) {};

/** @param {?=} opt_value */
angular.$q.Deferred.prototype.notify = function(opt_value) {};

/** @type {!angular.$q.Promise.<T>} */
angular.$q.Deferred.prototype.promise;

/**
 * $q.all has different output type based on the input type.
 * When {@code promise} is an array, the output is an array too: for each item n
 * in the input array, the corresponding item in the returned array would be the
 * the same type of n, or if n is a templated $q.Promise, the type of the
 * resolve value.
 * When {@code promise} is in form of a record, the output should be also be a
 * record with the same properties.
 * When {@code promise} is other forms, the returned type is an Object.
 *
 * @param {VALUE} promises
 * @template VALUE
 * @return {ALLTYPE}
 * @template ALLTYPE := type('angular.$q.Promise',
 *   cond(isUnknown(VALUE), unknown(),
 *     mapunion(VALUE, (x) =>
 *       cond(sub(x, 'Array'),
 *         cond(isTemplatized(x) && sub(rawTypeOf(x), 'IThenable'),
 *           type('Array', templateTypeOf(x, 0)),
 *           'Array'
 *         ),
 *         cond(isRecord(x),
 *           maprecord(record(x), (kx, vx) => record({[kx]:
 *             cond(isTemplatized(vx) && sub(rawTypeOf(vx), 'IThenable'),
 *               templateTypeOf(vx, 0),
 *               cond(sub(vx, 'angular.$q.Promise'),
 *                 unknown(),
 *                 vx
 *               )
 *             )
 *           })),
 *           'Object')))))
 * =:
 */
angular.$q.prototype.all = function(promises) {};

/**
 * @return {!angular.$q.Deferred}
 */
angular.$q.prototype.defer = function() {};

/**
 * @param {?=} opt_reason
 * @return {!angular.$q.Promise}
 */
angular.$q.prototype.reject = function(opt_reason) {};

/**
 * This definition was added to fix "non-existent property" warnings in
 * Closure Compiler. By defining as unknown, we declare that the property
 * exists, but do not require correct usage from existing code.
 *
 * @type {?}
 */
angular.$q.prototype.resolve;

/**
 * @param {RESULT} value
 * @return {!angular.$q.Promise.<RESULT>}
 * @template RESULT
 */
angular.$q.prototype.when = function(value) {};
<commit_msg>Rollback of change 143991869 because of test breakages.<commit_after>/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Externs for the $q service in Angular 1.4
 * NOTE: Due to a JS compiler bug, any use of a templated class must occur after
 * the class is defined. Please be careful with the ordering of the classes and
 * functions.
 * @see https://docs.angularjs.org/api/ng/service/$q
 * @externs
 */

/******************************************************************************
 * $q Service
 *****************************************************************************/

/**
 * @constructor
 */
angular.$q = function() {};

/**
 * @constructor
 * @template T
 */
angular.$q.Promise = function() {};

/**
 * Apply Type Transformation Language to allow more accurate templated type
 * definition.
 * This is copied from goog.Thenable.prototype.then, with the only
 * difference being the raw type as angular.$q.Promise instead of goog.Promise.
 *
 * @param {?(function(this:THIS, T): VALUE)=} opt_onFulfilled
 * @param {?(function(?): ?)=} opt_onRejected
 * @param {?(function(?): ?)=} opt_notifyCallback
 * @return {RESULT}
 * @template THIS
 * @template VALUE
 * @template RESULT := type('angular.$q.Promise',
 *     cond(isUnknown(VALUE), unknown(),
 *       mapunion(VALUE, (V) =>
 *         cond(isTemplatized(V) && sub(rawTypeOf(V), 'IThenable'),
 *           templateTypeOf(V, 0),
 *           cond(sub(V, 'angular.$q.Promise'),
 *              unknown(),
 *              V)))))
 *  =:
 */
angular.$q.Promise.prototype.then =
    function(opt_onFulfilled, opt_onRejected, opt_notifyCallback) {};

/**
 * @param {?function(?)} callback
 * @return {!angular.$q.Promise.<T>}
 */
angular.$q.Promise.prototype.catch = function(callback) {};

/**
 * @param {?function(?)} callback
 * @param {?(function(?): ?)=} opt_notifyCallback
 * @return {!angular.$q.Promise.<T>}
 */
angular.$q.Promise.prototype.finally =
    function(callback, opt_notifyCallback) {};

/**
 * @constructor
 * @template T
 */
angular.$q.Deferred = function() {};

/** @param {T=} opt_value */
angular.$q.Deferred.prototype.resolve = function(opt_value) {};

/** @param {?=} opt_reason */
angular.$q.Deferred.prototype.reject = function(opt_reason) {};

/** @param {?=} opt_value */
angular.$q.Deferred.prototype.notify = function(opt_value) {};

/** @type {!angular.$q.Promise.<T>} */
angular.$q.Deferred.prototype.promise;

/**
 * $q.all has different output type based on the input type.
 * When {@code promise} is an array, the output is an array too: for each item n
 * in the input array, the corresponding item in the returned array would be the
 * the same type of n, or if n is a templated $q.Promise, the type of the
 * resolve value.
 * When {@code promise} is in form of a record, the output should be also be a
 * record with the same properties.
 * When {@code promise} is other forms, the returned type is an Object.
 *
 * @param {VALUE} promises
 * @template VALUE
 * @return {ALLTYPE}
 * @template ALLTYPE := type('angular.$q.Promise',
 *   cond(isUnknown(VALUE), unknown(),
 *     mapunion(VALUE, (x) =>
 *       cond(sub(x, 'Array'),
 *         cond(isTemplatized(x) && sub(rawTypeOf(x), 'IThenable'),
 *           type('Array', templateTypeOf(x, 0)),
 *           'Array'
 *         ),
 *         cond(isRecord(x),
 *           maprecord(record(x), (kx, vx) => record({[kx]:
 *             cond(isTemplatized(vx) && sub(rawTypeOf(vx), 'IThenable'),
 *               templateTypeOf(vx, 0),
 *               cond(sub(vx, 'angular.$q.Promise'),
 *                 unknown(),
 *                 vx
 *               )
 *             )
 *           })),
 *           'Object')))))
 * =:
 */
angular.$q.prototype.all = function(promises) {};

/**
 * @return {!angular.$q.Deferred}
 */
angular.$q.prototype.defer = function() {};

/**
 * @param {?=} opt_reason
 * @return {!angular.$q.Promise}
 */
angular.$q.prototype.reject = function(opt_reason) {};

/**
 * @param {RESULT} value
 * @return {!angular.$q.Promise.<RESULT>}
 * @template RESULT
 */
angular.$q.prototype.when = function(value) {};
<|endoftext|>


<commit_before>'use strict';

const EventEmitter = require('events');
const isResumableError = require('./error').isResumableError;
const MongoError = require('mongodb-core').MongoError;

var cursorOptionNames = ['maxAwaitTimeMS', 'collation', 'readPreference'];

const CHANGE_DOMAIN_TYPES = {
  COLLECTION: Symbol('Collection'),
  DATABASE: Symbol('Database'),
  CLUSTER: Symbol('Cluster')
};

/**
 * Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}.
 * @class ChangeStream
 * @since 3.0.0
 * @param {(MongoClient|Db|Collection)} changeDomain The domain against which to create the change stream
 * @param {Array} pipeline An array of {@link https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents
 * @param {object} [options] Optional settings
 * @param {string} [options.fullDocument='default'] Allowed values: ‘default’, ‘updateLookup’. When set to ‘updateLookup’, the change stream will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred.
 * @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a change stream query
 * @param {object} [options.resumeAfter] Specifies the logical starting point for the new change stream. This should be the _id field from a previously returned change stream document.
 * @param {number} [options.batchSize] The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
 * @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
 * @param {ReadPreference} [options.readPreference] The read preference. Defaults to the read preference of the database or collection. See {@link https://docs.mongodb.com/manual/reference/read-preference|read preference documentation}.
 * @fires ChangeStream#close
 * @fires ChangeStream#change
 * @fires ChangeStream#end
 * @fires ChangeStream#error
 * @return {ChangeStream} a ChangeStream instance.
 */

class ChangeStream extends EventEmitter {
  constructor(changeDomain, pipeline, options) {
    super();
    const Collection = require('./collection');
    const Db = require('./db');
    const MongoClient = require('./mongo_client');

    this.pipeline = pipeline || [];
    this.options = options || {};
    this.cursorNamespace = undefined;
    this.namespace = {};

    if (changeDomain instanceof Collection) {
      this.type = CHANGE_DOMAIN_TYPES.COLLECTION;
      this.topology = changeDomain.s.db.serverConfig;

      this.namespace = {
        collection: changeDomain.collectionName,
        database: changeDomain.s.db.databaseName
      };

      this.cursorNamespace = `${this.namespace.database}.${this.namespace.collection}`;
    } else if (changeDomain instanceof Db) {
      this.type = CHANGE_DOMAIN_TYPES.DATABASE;
      this.namespace = { collection: '', database: changeDomain.databaseName };
      this.cursorNamespace = this.namespace.database;
      this.topology = changeDomain.serverConfig;
    } else if (changeDomain instanceof MongoClient) {
      this.type = CHANGE_DOMAIN_TYPES.CLUSTER;
      this.namespace = { collection: '', database: 'admin' };
      this.cursorNamespace = this.namespace.database;
      this.topology = changeDomain.topology;
    } else {
      throw new TypeError(
        'changeDomain provided to ChangeStream constructor is not an instance of Collection, Db, or MongoClient'
      );
    }

    this.promiseLibrary = changeDomain.s.promiseLibrary;
    if (!this.options.readPreference && changeDomain.s.readPreference) {
      this.options.readPreference = changeDomain.s.readPreference;
    }

    // We need to get the operationTime as early as possible
    const isMaster = this.topology.lastIsMaster();
    if (!isMaster) {
      throw new MongoError('Topology does not have an ismaster yet.');
    }

    this.operationTime = isMaster.operationTime;

    // Create contained Change Stream cursor
    this.cursor = createChangeStreamCursor(this);

    // Listen for any `change` listeners being added to ChangeStream
    this.on('newListener', eventName => {
      if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
        this.cursor.on('data', change =>
          processNewChange({ changeStream: this, change, eventEmitter: true })
        );
      }
    });

    // Listen for all `change` listeners being removed from ChangeStream
    this.on('removeListener', eventName => {
      if (eventName === 'change' && this.listenerCount('change') === 0 && this.cursor) {
        this.cursor.removeAllListeners('data');
      }
    });
  }

  /**
   * Check if there is any document still available in the Change Stream
   * @function ChangeStream.prototype.hasNext
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @throws {MongoError}
   * @return {Promise} returns Promise if no callback passed
   */
  hasNext(callback) {
    return this.cursor.hasNext(callback);
  }

  /**
   * Get the next available document from the Change Stream, returns null if no more documents are available.
   * @function ChangeStream.prototype.next
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @throws {MongoError}
   * @return {Promise} returns Promise if no callback passed
   */
  next(callback) {
    var self = this;
    if (this.isClosed()) {
      if (callback) return callback(new Error('Change Stream is not open.'), null);
      return self.promiseLibrary.reject(new Error('Change Stream is not open.'));
    }

    return this.cursor
      .next()
      .then(change => processNewChange({ changeStream: self, change, callback }))
      .catch(error => processNewChange({ changeStream: self, error, callback }));
  }

  /**
   * Is the cursor closed
   * @method ChangeStream.prototype.isClosed
   * @return {boolean}
   */
  isClosed() {
    if (this.cursor) {
      return this.cursor.isClosed();
    }
    return true;
  }

  /**
   * Close the Change Stream
   * @method ChangeStream.prototype.close
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @return {Promise} returns Promise if no callback passed
   */
  close(callback) {
    if (!this.cursor) {
      if (callback) return callback();
      return this.promiseLibrary.resolve();
    }

    // Tidy up the existing cursor
    var cursor = this.cursor;
    delete this.cursor;
    return cursor.close(callback);
  }

  /**
   * This method pulls all the data out of a readable stream, and writes it to the supplied destination, automatically managing the flow so that the destination is not overwhelmed by a fast readable stream.
   * @method
   * @param {Writable} destination The destination for writing data
   * @param {object} [options] {@link https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options|Pipe options}
   * @return {null}
   */
  pipe(destination, options) {
    if (!this.pipeDestinations) {
      this.pipeDestinations = [];
    }
    this.pipeDestinations.push(destination);
    return this.cursor.pipe(destination, options);
  }

  /**
   * This method will remove the hooks set up for a previous pipe() call.
   * @param {Writable} [destination] The destination for writing data
   * @return {null}
   */
  unpipe(destination) {
    if (this.pipeDestinations && this.pipeDestinations.indexOf(destination) > -1) {
      this.pipeDestinations.splice(this.pipeDestinations.indexOf(destination), 1);
    }
    return this.cursor.unpipe(destination);
  }

  /**
   * Return a modified Readable stream including a possible transform method.
   * @method
   * @param {object} [options] Optional settings.
   * @param {function} [options.transform] A transformation method applied to each document emitted by the stream.
   * @return {Cursor}
   */
  stream(options) {
    this.streamOptions = options;
    return this.cursor.stream(options);
  }

  /**
   * This method will cause a stream in flowing mode to stop emitting data events. Any data that becomes available will remain in the internal buffer.
   * @return {null}
   */
  pause() {
    return this.cursor.pause();
  }

  /**
   * This method will cause the readable stream to resume emitting data events.
   * @return {null}
   */
  resume() {
    return this.cursor.resume();
  }
}

// Create a new change stream cursor based on self's configuration
var createChangeStreamCursor = function(self) {
  if (self.resumeToken) {
    self.options.resumeAfter = self.resumeToken;
  }

  var changeStreamCursor = buildChangeStreamAggregationCommand(self);

  /**
   * Fired for each new matching change in the specified namespace. Attaching a `change`
   * event listener to a Change Stream will switch the stream into flowing mode. Data will
   * then be passed as soon as it is available.
   *
   * @event ChangeStream#change
   * @type {object}
   */
  if (self.listenerCount('change') > 0) {
    changeStreamCursor.on('data', function(change) {
      processNewChange({ changeStream: self, change, eventEmitter: true });
    });
  }

  /**
   * Change stream close event
   *
   * @event ChangeStream#close
   * @type {null}
   */
  changeStreamCursor.on('close', function() {
    self.emit('close');
  });

  /**
   * Change stream end event
   *
   * @event ChangeStream#end
   * @type {null}
   */
  changeStreamCursor.on('end', function() {
    self.emit('end');
  });

  /**
   * Fired when the stream encounters an error.
   *
   * @event ChangeStream#error
   * @type {Error}
   */
  changeStreamCursor.on('error', function(error) {
    processNewChange({ changeStream: self, error, eventEmitter: true });
  });

  if (self.pipeDestinations) {
    const cursorStream = changeStreamCursor.stream(self.streamOptions);
    for (let pipeDestination in self.pipeDestinations) {
      cursorStream.pipe(pipeDestination);
    }
  }

  return changeStreamCursor;
};

function getResumeToken(self) {
  return self.resumeToken || self.options.resumeAfter;
}

function getStartAtOperationTime(self) {
  const isMaster = self.topology.lastIsMaster() || {};
  return (
    isMaster.maxWireVersion && isMaster.maxWireVersion >= 7 && self.options.startAtOperationTime
  );
}

var buildChangeStreamAggregationCommand = function(self) {
  const topology = self.topology;
  const namespace = self.namespace;
  const pipeline = self.pipeline;
  const options = self.options;
  const cursorNamespace = self.cursorNamespace;

  var changeStreamStageOptions = {
    fullDocument: options.fullDocument || 'default'
  };

  const resumeToken = getResumeToken(self);
  const startAtOperationTime = getStartAtOperationTime(self);
  if (resumeToken) {
    changeStreamStageOptions.resumeAfter = resumeToken;
  }

  if (startAtOperationTime) {
    changeStreamStageOptions.startAtOperationTime = startAtOperationTime;
  }

  // Map cursor options
  var cursorOptions = {};
  cursorOptionNames.forEach(function(optionName) {
    if (options[optionName]) {
      cursorOptions[optionName] = options[optionName];
    }
  });

  if (self.type === CHANGE_DOMAIN_TYPES.CLUSTER) {
    changeStreamStageOptions.allChangesForCluster = true;
  }

  var changeStreamPipeline = [{ $changeStream: changeStreamStageOptions }];

  changeStreamPipeline = changeStreamPipeline.concat(pipeline);

  var command = {
    aggregate: self.type === CHANGE_DOMAIN_TYPES.COLLECTION ? namespace.collection : 1,
    pipeline: changeStreamPipeline,
    readConcern: { level: 'majority' },
    cursor: {
      batchSize: options.batchSize || 1
    }
  };

  // Create and return the cursor
  return topology.cursor(cursorNamespace, command, cursorOptions);
};

// This method performs a basic server selection loop, satisfying the requirements of
// ChangeStream resumability until the new SDAM layer can be used.
const SELECTION_TIMEOUT = 30000;
function waitForTopologyConnected(topology, options, callback) {
  setTimeout(() => {
    if (options && options.start == null) options.start = process.hrtime();
    const start = options.start || process.hrtime();
    const timeout = options.timeout || SELECTION_TIMEOUT;
    const readPreference = options.readPreference;

    if (topology.isConnected({ readPreference })) return callback(null, null);
    const hrElapsed = process.hrtime(start);
    const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
    if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
    waitForTopologyConnected(topology, options, callback);
  }, 3000); // this is an arbitrary wait time to allow SDAM to transition
}

// Handle new change events. This method brings together the routes from the callback, event emitter, and promise ways of using ChangeStream.
function processNewChange(args) {
  const changeStream = args.changeStream;
  const error = args.error;
  const change = args.change;
  const callback = args.callback;
  const eventEmitter = args.eventEmitter || false;

  // If the changeStream is closed, then it should not process a change.
  if (changeStream.isClosed()) {
    // We do not error in the eventEmitter case.
    if (eventEmitter) {
      return;
    }

    const error = new MongoError('ChangeStream is closed');
    return typeof callback === 'function'
      ? callback(error, null)
      : changeStream.promiseLibrary.reject(error);
  }

  const topology = changeStream.topology;
  const options = changeStream.cursor.options;

  if (error) {
    if (isResumableError(error) && !changeStream.attemptingResume) {
      changeStream.attemptingResume = true;

      if (!(getResumeToken(changeStream) || getStartAtOperationTime(changeStream))) {
        const startAtOperationTime = changeStream.cursor.cursorState.operationTime;
        changeStream.options = Object.assign({ startAtOperationTime }, changeStream.options);
      }

      // stop listening to all events from old cursor
      ['data', 'close', 'end', 'error'].forEach(event =>
        changeStream.cursor.removeAllListeners(event)
      );

      // close internal cursor, ignore errors
      changeStream.cursor.close();

      // attempt recreating the cursor
      if (eventEmitter) {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return changeStream.emit('error', err);
          changeStream.cursor = createChangeStreamCursor(changeStream);
        });

        return;
      }

      if (callback) {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return callback(err, null);

          changeStream.cursor = createChangeStreamCursor(changeStream);
          changeStream.next(callback);
        });

        return;
      }

      return new Promise((resolve, reject) => {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return reject(err);
          resolve();
        });
      })
        .then(() => (changeStream.cursor = createChangeStreamCursor(changeStream)))
        .then(() => changeStream.next());
    }

    if (eventEmitter) return changeStream.emit('error', error);
    if (typeof callback === 'function') return callback(error, null);
    return changeStream.promiseLibrary.reject(error);
  }

  changeStream.attemptingResume = false;

  // Cache the resume token if it is present. If it is not present return an error.
  if (!change || !change._id) {
    var noResumeTokenError = new Error(
      'A change stream document has been received that lacks a resume token (_id).'
    );

    if (eventEmitter) return changeStream.emit('error', noResumeTokenError);
    if (typeof callback === 'function') return callback(noResumeTokenError, null);
    return changeStream.promiseLibrary.reject(noResumeTokenError);
  }

  changeStream.resumeToken = change._id;

  // Return the change
  if (eventEmitter) return changeStream.emit('change', change);
  if (typeof callback === 'function') return callback(error, change);
  return changeStream.promiseLibrary.resolve(change);
}

/**
 * The callback format for results
 * @callback ChangeStream~resultCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {(object|null)} result The result object if the command was executed successfully.
 */

module.exports = ChangeStream;
<commit_msg>fix(ChangeStream): remove startAtOperationTime once we have resumeToken<commit_after>'use strict';

const EventEmitter = require('events');
const isResumableError = require('./error').isResumableError;
const MongoError = require('mongodb-core').MongoError;

var cursorOptionNames = ['maxAwaitTimeMS', 'collation', 'readPreference'];

const CHANGE_DOMAIN_TYPES = {
  COLLECTION: Symbol('Collection'),
  DATABASE: Symbol('Database'),
  CLUSTER: Symbol('Cluster')
};

/**
 * Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}.
 * @class ChangeStream
 * @since 3.0.0
 * @param {(MongoClient|Db|Collection)} changeDomain The domain against which to create the change stream
 * @param {Array} pipeline An array of {@link https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents
 * @param {object} [options] Optional settings
 * @param {string} [options.fullDocument='default'] Allowed values: ‘default’, ‘updateLookup’. When set to ‘updateLookup’, the change stream will include both a delta describing the changes to the document, as well as a copy of the entire document that was changed from some time after the change occurred.
 * @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a change stream query
 * @param {object} [options.resumeAfter] Specifies the logical starting point for the new change stream. This should be the _id field from a previously returned change stream document.
 * @param {number} [options.batchSize] The number of documents to return per batch. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
 * @param {object} [options.collation] Specify collation settings for operation. See {@link https://docs.mongodb.com/manual/reference/command/aggregate|aggregation documentation}.
 * @param {ReadPreference} [options.readPreference] The read preference. Defaults to the read preference of the database or collection. See {@link https://docs.mongodb.com/manual/reference/read-preference|read preference documentation}.
 * @fires ChangeStream#close
 * @fires ChangeStream#change
 * @fires ChangeStream#end
 * @fires ChangeStream#error
 * @return {ChangeStream} a ChangeStream instance.
 */

class ChangeStream extends EventEmitter {
  constructor(changeDomain, pipeline, options) {
    super();
    const Collection = require('./collection');
    const Db = require('./db');
    const MongoClient = require('./mongo_client');

    this.pipeline = pipeline || [];
    this.options = options || {};
    this.cursorNamespace = undefined;
    this.namespace = {};

    if (changeDomain instanceof Collection) {
      this.type = CHANGE_DOMAIN_TYPES.COLLECTION;
      this.topology = changeDomain.s.db.serverConfig;

      this.namespace = {
        collection: changeDomain.collectionName,
        database: changeDomain.s.db.databaseName
      };

      this.cursorNamespace = `${this.namespace.database}.${this.namespace.collection}`;
    } else if (changeDomain instanceof Db) {
      this.type = CHANGE_DOMAIN_TYPES.DATABASE;
      this.namespace = { collection: '', database: changeDomain.databaseName };
      this.cursorNamespace = this.namespace.database;
      this.topology = changeDomain.serverConfig;
    } else if (changeDomain instanceof MongoClient) {
      this.type = CHANGE_DOMAIN_TYPES.CLUSTER;
      this.namespace = { collection: '', database: 'admin' };
      this.cursorNamespace = this.namespace.database;
      this.topology = changeDomain.topology;
    } else {
      throw new TypeError(
        'changeDomain provided to ChangeStream constructor is not an instance of Collection, Db, or MongoClient'
      );
    }

    this.promiseLibrary = changeDomain.s.promiseLibrary;
    if (!this.options.readPreference && changeDomain.s.readPreference) {
      this.options.readPreference = changeDomain.s.readPreference;
    }

    // We need to get the operationTime as early as possible
    const isMaster = this.topology.lastIsMaster();
    if (!isMaster) {
      throw new MongoError('Topology does not have an ismaster yet.');
    }

    this.operationTime = isMaster.operationTime;

    // Create contained Change Stream cursor
    this.cursor = createChangeStreamCursor(this);

    // Listen for any `change` listeners being added to ChangeStream
    this.on('newListener', eventName => {
      if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
        this.cursor.on('data', change =>
          processNewChange({ changeStream: this, change, eventEmitter: true })
        );
      }
    });

    // Listen for all `change` listeners being removed from ChangeStream
    this.on('removeListener', eventName => {
      if (eventName === 'change' && this.listenerCount('change') === 0 && this.cursor) {
        this.cursor.removeAllListeners('data');
      }
    });
  }

  /**
   * Check if there is any document still available in the Change Stream
   * @function ChangeStream.prototype.hasNext
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @throws {MongoError}
   * @return {Promise} returns Promise if no callback passed
   */
  hasNext(callback) {
    return this.cursor.hasNext(callback);
  }

  /**
   * Get the next available document from the Change Stream, returns null if no more documents are available.
   * @function ChangeStream.prototype.next
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @throws {MongoError}
   * @return {Promise} returns Promise if no callback passed
   */
  next(callback) {
    var self = this;
    if (this.isClosed()) {
      if (callback) return callback(new Error('Change Stream is not open.'), null);
      return self.promiseLibrary.reject(new Error('Change Stream is not open.'));
    }

    return this.cursor
      .next()
      .then(change => processNewChange({ changeStream: self, change, callback }))
      .catch(error => processNewChange({ changeStream: self, error, callback }));
  }

  /**
   * Is the cursor closed
   * @method ChangeStream.prototype.isClosed
   * @return {boolean}
   */
  isClosed() {
    if (this.cursor) {
      return this.cursor.isClosed();
    }
    return true;
  }

  /**
   * Close the Change Stream
   * @method ChangeStream.prototype.close
   * @param {ChangeStream~resultCallback} [callback] The result callback.
   * @return {Promise} returns Promise if no callback passed
   */
  close(callback) {
    if (!this.cursor) {
      if (callback) return callback();
      return this.promiseLibrary.resolve();
    }

    // Tidy up the existing cursor
    var cursor = this.cursor;
    delete this.cursor;
    return cursor.close(callback);
  }

  /**
   * This method pulls all the data out of a readable stream, and writes it to the supplied destination, automatically managing the flow so that the destination is not overwhelmed by a fast readable stream.
   * @method
   * @param {Writable} destination The destination for writing data
   * @param {object} [options] {@link https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options|Pipe options}
   * @return {null}
   */
  pipe(destination, options) {
    if (!this.pipeDestinations) {
      this.pipeDestinations = [];
    }
    this.pipeDestinations.push(destination);
    return this.cursor.pipe(destination, options);
  }

  /**
   * This method will remove the hooks set up for a previous pipe() call.
   * @param {Writable} [destination] The destination for writing data
   * @return {null}
   */
  unpipe(destination) {
    if (this.pipeDestinations && this.pipeDestinations.indexOf(destination) > -1) {
      this.pipeDestinations.splice(this.pipeDestinations.indexOf(destination), 1);
    }
    return this.cursor.unpipe(destination);
  }

  /**
   * Return a modified Readable stream including a possible transform method.
   * @method
   * @param {object} [options] Optional settings.
   * @param {function} [options.transform] A transformation method applied to each document emitted by the stream.
   * @return {Cursor}
   */
  stream(options) {
    this.streamOptions = options;
    return this.cursor.stream(options);
  }

  /**
   * This method will cause a stream in flowing mode to stop emitting data events. Any data that becomes available will remain in the internal buffer.
   * @return {null}
   */
  pause() {
    return this.cursor.pause();
  }

  /**
   * This method will cause the readable stream to resume emitting data events.
   * @return {null}
   */
  resume() {
    return this.cursor.resume();
  }
}

// Create a new change stream cursor based on self's configuration
var createChangeStreamCursor = function(self) {
  if (self.resumeToken) {
    self.options.resumeAfter = self.resumeToken;
  }

  var changeStreamCursor = buildChangeStreamAggregationCommand(self);

  /**
   * Fired for each new matching change in the specified namespace. Attaching a `change`
   * event listener to a Change Stream will switch the stream into flowing mode. Data will
   * then be passed as soon as it is available.
   *
   * @event ChangeStream#change
   * @type {object}
   */
  if (self.listenerCount('change') > 0) {
    changeStreamCursor.on('data', function(change) {
      processNewChange({ changeStream: self, change, eventEmitter: true });
    });
  }

  /**
   * Change stream close event
   *
   * @event ChangeStream#close
   * @type {null}
   */
  changeStreamCursor.on('close', function() {
    self.emit('close');
  });

  /**
   * Change stream end event
   *
   * @event ChangeStream#end
   * @type {null}
   */
  changeStreamCursor.on('end', function() {
    self.emit('end');
  });

  /**
   * Fired when the stream encounters an error.
   *
   * @event ChangeStream#error
   * @type {Error}
   */
  changeStreamCursor.on('error', function(error) {
    processNewChange({ changeStream: self, error, eventEmitter: true });
  });

  if (self.pipeDestinations) {
    const cursorStream = changeStreamCursor.stream(self.streamOptions);
    for (let pipeDestination in self.pipeDestinations) {
      cursorStream.pipe(pipeDestination);
    }
  }

  return changeStreamCursor;
};

function getResumeToken(self) {
  return self.resumeToken || self.options.resumeAfter;
}

function getStartAtOperationTime(self) {
  const isMaster = self.topology.lastIsMaster() || {};
  return (
    isMaster.maxWireVersion && isMaster.maxWireVersion >= 7 && self.options.startAtOperationTime
  );
}

var buildChangeStreamAggregationCommand = function(self) {
  const topology = self.topology;
  const namespace = self.namespace;
  const pipeline = self.pipeline;
  const options = self.options;
  const cursorNamespace = self.cursorNamespace;

  var changeStreamStageOptions = {
    fullDocument: options.fullDocument || 'default'
  };

  const resumeToken = getResumeToken(self);
  const startAtOperationTime = getStartAtOperationTime(self);
  if (resumeToken) {
    changeStreamStageOptions.resumeAfter = resumeToken;
  }

  if (startAtOperationTime) {
    changeStreamStageOptions.startAtOperationTime = startAtOperationTime;
  }

  // Map cursor options
  var cursorOptions = {};
  cursorOptionNames.forEach(function(optionName) {
    if (options[optionName]) {
      cursorOptions[optionName] = options[optionName];
    }
  });

  if (self.type === CHANGE_DOMAIN_TYPES.CLUSTER) {
    changeStreamStageOptions.allChangesForCluster = true;
  }

  var changeStreamPipeline = [{ $changeStream: changeStreamStageOptions }];

  changeStreamPipeline = changeStreamPipeline.concat(pipeline);

  var command = {
    aggregate: self.type === CHANGE_DOMAIN_TYPES.COLLECTION ? namespace.collection : 1,
    pipeline: changeStreamPipeline,
    readConcern: { level: 'majority' },
    cursor: {
      batchSize: options.batchSize || 1
    }
  };

  // Create and return the cursor
  return topology.cursor(cursorNamespace, command, cursorOptions);
};

// This method performs a basic server selection loop, satisfying the requirements of
// ChangeStream resumability until the new SDAM layer can be used.
const SELECTION_TIMEOUT = 30000;
function waitForTopologyConnected(topology, options, callback) {
  setTimeout(() => {
    if (options && options.start == null) options.start = process.hrtime();
    const start = options.start || process.hrtime();
    const timeout = options.timeout || SELECTION_TIMEOUT;
    const readPreference = options.readPreference;

    if (topology.isConnected({ readPreference })) return callback(null, null);
    const hrElapsed = process.hrtime(start);
    const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
    if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
    waitForTopologyConnected(topology, options, callback);
  }, 3000); // this is an arbitrary wait time to allow SDAM to transition
}

// Handle new change events. This method brings together the routes from the callback, event emitter, and promise ways of using ChangeStream.
function processNewChange(args) {
  const changeStream = args.changeStream;
  const error = args.error;
  const change = args.change;
  const callback = args.callback;
  const eventEmitter = args.eventEmitter || false;

  // If the changeStream is closed, then it should not process a change.
  if (changeStream.isClosed()) {
    // We do not error in the eventEmitter case.
    if (eventEmitter) {
      return;
    }

    const error = new MongoError('ChangeStream is closed');
    return typeof callback === 'function'
      ? callback(error, null)
      : changeStream.promiseLibrary.reject(error);
  }

  const topology = changeStream.topology;
  const options = changeStream.cursor.options;

  if (error) {
    if (isResumableError(error) && !changeStream.attemptingResume) {
      changeStream.attemptingResume = true;

      if (!(getResumeToken(changeStream) || getStartAtOperationTime(changeStream))) {
        const startAtOperationTime = changeStream.cursor.cursorState.operationTime;
        changeStream.options = Object.assign({ startAtOperationTime }, changeStream.options);
      }

      // stop listening to all events from old cursor
      ['data', 'close', 'end', 'error'].forEach(event =>
        changeStream.cursor.removeAllListeners(event)
      );

      // close internal cursor, ignore errors
      changeStream.cursor.close();

      // attempt recreating the cursor
      if (eventEmitter) {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return changeStream.emit('error', err);
          changeStream.cursor = createChangeStreamCursor(changeStream);
        });

        return;
      }

      if (callback) {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return callback(err, null);

          changeStream.cursor = createChangeStreamCursor(changeStream);
          changeStream.next(callback);
        });

        return;
      }

      return new Promise((resolve, reject) => {
        waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
          if (err) return reject(err);
          resolve();
        });
      })
        .then(() => (changeStream.cursor = createChangeStreamCursor(changeStream)))
        .then(() => changeStream.next());
    }

    if (eventEmitter) return changeStream.emit('error', error);
    if (typeof callback === 'function') return callback(error, null);
    return changeStream.promiseLibrary.reject(error);
  }

  changeStream.attemptingResume = false;

  // Cache the resume token if it is present. If it is not present return an error.
  if (!change || !change._id) {
    var noResumeTokenError = new Error(
      'A change stream document has been received that lacks a resume token (_id).'
    );

    if (eventEmitter) return changeStream.emit('error', noResumeTokenError);
    if (typeof callback === 'function') return callback(noResumeTokenError, null);
    return changeStream.promiseLibrary.reject(noResumeTokenError);
  }

  changeStream.resumeToken = change._id;

  // wipe the startAtOperationTime if there was one so that there won't be a conflict
  // between resumeToken and startAtOperationTime if we need to reconnect the cursor
  changeStream.options.startAtOperationTime = undefined;

  // Return the change
  if (eventEmitter) return changeStream.emit('change', change);
  if (typeof callback === 'function') return callback(error, change);
  return changeStream.promiseLibrary.resolve(change);
}

/**
 * The callback format for results
 * @callback ChangeStream~resultCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {(object|null)} result The result object if the command was executed successfully.
 */

module.exports = ChangeStream;
<|endoftext|>