<commit_before>Redwood.controller("SubjectCtrl", ["$rootScope", "$scope", "RedwoodSubject", 'SynchronizedStopWatch', function($rootScope, $scope, rs, SynchronizedStopWatch) {

	var CLOCK_FREQUENCY = 5;

	rs.on_load(function() { //called once the page has loaded for a new sub period

		rs.config.pairs.forEach(function(pair, index) { //decide who is the first player and who is the second player
			var userIndex = pair.indexOf(parseInt(rs.user_id));
			if(userIndex > -1) {
				$scope.pair_index = index;
				$scope.user_index = userIndex;
				$scope.partner_id = pair[($scope.user_index + 1) % 2].toString();
			}
		});

		$scope.matrix = $scope.user_index === 0 ? rs.config.matrix : transpose(rs.config.matrix);

		$scope.config = rs.config;

		$scope.yMax = $scope.matrix[0][0][0];
		$scope.matrix.forEach(function(row) {
			row.forEach(function(cell) {
				cell.forEach(function(value) {
					$scope.yMax = Math.max($scope.yMax, value);
				})
			})
		});

		$scope.clock = SynchronizedStopWatch.instance()
			.frequency(CLOCK_FREQUENCY).onTick(processTick)
			.duration(rs.config.period_length_s).onComplete(function() {
				rs.trigger("simulation_complete");
			});

		var numSubPeriods = rs.config.num_sub_periods || (rs.config.period_length_s * CLOCK_FREQUENCY); //If this config value is zero there will be a subperiod every tick
		$scope.ticksPerSubPeriod = Math.max(Math.floor(rs.config.period_length_s * CLOCK_FREQUENCY / numSubPeriods), 1);

		$scope.subPeriods = [];
		if($scope.ticksPerSubPeriod > 1){ //set up the sub period markers so they can be displayed on the plot
			var subPeriod = 0;
			do {
				subPeriod += $scope.ticksPerSubPeriod;
				$scope.subPeriods.push(subPeriod / $scope.clock.getDurationInTicks());
			} while(subPeriod < $scope.clock.getDurationInTicks());
		}

		$scope.rewards = [];
		$scope.opponentRewards = [];
		$scope.plot = [];
		$scope.opponentPlot = [];

		$scope.readyEnabled = true;
	});

	$scope.ready = function() {
		$scope.readyEnabled = false;
		rs.trigger("ready");
	};

	rs.on("ready", function(value){ //event handler for ready button click
		$scope.readyEnabled = false;
		rs.gate('ready', function() {

			if(rs.config.initial_delay_s > 0) {
				$scope.tEnableInput = rs.config.initial_delay_s * CLOCK_FREQUENCY; //only enable input after the initial delay period specified in the config
			} else {
				$scope.actionsEnabled = true; //otherwise enable input immediately
			}

			$scope.action = rs.config.initial_actions[rs.user_id - 1]; //set actions to the initial actions specified in the config
			$scope.partnerAction = rs.config.initial_actions[$scope.partner_id - 1];
			$scope.prevPartnerAction = $scope.partnerAction;

			$scope.clock.start();
		});
	});

	var processTick = function(tick){ //sent by admin at the specified frequency as long as all subjects have acknowledged the previous tick

		$scope.tick = tick;

		if(tick % $scope.ticksPerSubPeriod === 0) { //if this is the end of a sub period (in the "continuous" version, every tick is the end of a sub period)
			var reward = $scope.matrix[$scope.action - 1][$scope.partnerAction - 1][0]; //allocate reward based on the current user actions and the matrix
			$scope.rewards.push(reward);
			rs.add_points(reward * $scope.ticksPerSubPeriod / $scope.clock.getDurationInTicks()); //add the integral of the current reward value over the length of the subperiod

			$scope.plot.push([(tick - $scope.ticksPerSubPeriod) / $scope.clock.getDurationInTicks(), $scope.rewards[$scope.rewards.length - 1]]);
			$scope.plot.push([tick / $scope.clock.getDurationInTicks(), $scope.rewards[$scope.rewards.length - 1]]);
			var opponentReward = $scope.matrix[$scope.action - 1][$scope.partnerAction - 1][1];
			$scope.opponentRewards.push(opponentReward);
			$scope.opponentPlot.push([(tick - $scope.ticksPerSubPeriod) / $scope.clock.getDurationInTicks(), $scope.opponentRewards[$scope.opponentRewards.length - 1]]);
			$scope.opponentPlot.push([tick / $scope.clock.getDurationInTicks(), $scope.opponentRewards[$scope.opponentRewards.length - 1]]);
			$scope.prevPartnerAction = $scope.partnerAction;
		}

		if($scope.tEnableInput) {
			$scope.inputCountdown = Math.ceil(($scope.tEnableInput - $scope.tick) / CLOCK_FREQUENCY);
			if(tick >= $scope.tEnableInput) {
				$scope.actionsEnabled = true;
				$scope.tEnableInput = 0;
			}
		}

	};

	$scope.onAction = function(action) {
		if(action !== $scope.action){ //only trigger action events when selection has actually changed
			$scope.action = action;
			rs.trigger("action", action);
		}
	};

	rs.on("action", function(value){ //triggered when the user changes their selection
		$scope.action = value;
		if(rs.config.action_cost > 0) { //subtract the action cost specified in the config
			rs.add_points(-rs.config.action_cost);
		}
		if(rs.config.action_delay_s > 0) { //disable inputs for the action delay specified in the config
			$scope.tEnableInput = $scope.tick + (rs.config.action_delay_s * CLOCK_FREQUENCY);
			$scope.actionsEnabled = false;
		}
	});

	rs.recv("action", function(sender, value){ //receive other subjects actions
		if(sender === $scope.partner_id){ //if the other subject is the opponent, update their current action
			$scope.partnerAction = value;
		}
	});

	rs.on("simulation_complete", function(value) {
		$scope.actionsEnabled = false;
		rs.next_period(5); //request the framework to advance to the next period
	});

	var transpose = function(matrix) { //transpose a 2x2 matrix
		var transposed = [[[], []], [[], []]];
		for(var i = 0; i < 4; i++){
			var row = Math.floor(i/2);
			var column = i % 2;
			transposed[column][row] = [matrix[row][column][1], matrix[row][column][0]];
		}
		return transposed;
	};

}]);

Redwood.directive('plot', [function() {
	return {
		link: function($scope, elem, attr) {

			$scope.$watch('config', function(plot) {
				replot();
			}, true);

			$scope.$watch('tick', function(plot) {
				replot();
			}, true);

			function replot() {

				if(!$scope.config) return;

				var xRange = 1;
				var opts = {
					xaxis: { tickLength: 0, min: 0, max: xRange },
					yaxis: { tickLength: 0, min: 0, max: $scope.yMax + ($scope.yMax * 0.2) },
					series: { shadowSize: 0 }
				};
				var dataset = [];
				if($scope.subPeriods) {
					for(var p = 0; p < $scope.subPeriods.length; p++){ //mark each sub-period with a vertical red line
						dataset.push({
							data: [
								[$scope.subPeriods[p], opts.yaxis.min],
								[$scope.subPeriods[p], opts.yaxis.max]
							],
							lines: { lineWidth: 1 },
							color: "red"
						});
					}
				}
				if($scope.plot) {
					dataset.push({ //plot your rewards as a grey integral
						data: $scope.plot,
						lines: { fill: true, lineWidth: 0, fillColor: "grey" },
						color: "grey"
					});
					dataset.push({ //plot your opponent's rewards as a black line
						data: $scope.opponentPlot,
						lines: { lineWidth: 2 },
						color: "black"
					});
				}

				if($scope.clock) {
					dataset.push({ //display the current time indicator as a vertical grey line
						data: [
							[$scope.tick / $scope.clock.getDurationInTicks(), opts.yaxis.min],
							[$scope.tick / $scope.clock.getDurationInTicks(), opts.yaxis.max]
						],
						color: "grey"
					});
				}
				$.plot(elem, dataset, opts);
			}
		}
	}
}]);<commit_msg>Moved some plot-specific code into the directive.<commit_after>Redwood.controller("SubjectCtrl", ["$rootScope", "$scope", "RedwoodSubject", 'SynchronizedStopWatch', function($rootScope, $scope, rs, SynchronizedStopWatch) {

	var CLOCK_FREQUENCY = 5;

	rs.on_load(function() { //called once the page has loaded for a new sub period

		rs.config.pairs.forEach(function(pair, index) { //decide who is the first player and who is the second player
			var userIndex = pair.indexOf(parseInt(rs.user_id));
			if(userIndex > -1) {
				$scope.pair_index = index;
				$scope.user_index = userIndex;
				$scope.partner_id = pair[($scope.user_index + 1) % 2].toString();
			}
		});

		$scope.matrix = $scope.user_index === 0 ? rs.config.matrix : transpose(rs.config.matrix);

		$scope.config = rs.config;

		$scope.yMax = $scope.matrix[0][0][0]; //Find the maximum reward value to set the y-axis on the plot
		$scope.matrix.forEach(function(row) {
			row.forEach(function(cell) {
				cell.forEach(function(value) {
					$scope.yMax = Math.max($scope.yMax, value);
				})
			})
		});

		$scope.clock = SynchronizedStopWatch.instance()
			.frequency(CLOCK_FREQUENCY).onTick(processTick)
			.duration(rs.config.period_length_s).onComplete(function() {
				rs.trigger("simulation_complete");
			});

		var numSubPeriods = rs.config.num_sub_periods || (rs.config.period_length_s * CLOCK_FREQUENCY); //If this config value is zero there will be a subperiod every tick
		$scope.ticksPerSubPeriod = Math.max(Math.floor(rs.config.period_length_s * CLOCK_FREQUENCY / numSubPeriods), 1);

		$scope.rewards = [];
		$scope.opponentRewards = [];

		$scope.readyEnabled = true;
	});

	$scope.ready = function() {
		$scope.readyEnabled = false;
		rs.trigger("ready");
	};

	rs.on("ready", function(value){ //event handler for ready button click
		$scope.readyEnabled = false;
		rs.gate('ready', function() {

			if(rs.config.initial_delay_s > 0) {
				$scope.tEnableInput = rs.config.initial_delay_s * CLOCK_FREQUENCY; //only enable input after the initial delay period specified in the config
			} else {
				$scope.actionsEnabled = true; //otherwise enable input immediately
			}

			$scope.action = rs.config.initial_actions[rs.user_id - 1]; //set actions to the initial actions specified in the config
			$scope.partnerAction = rs.config.initial_actions[$scope.partner_id - 1];
			$scope.prevPartnerAction = $scope.partnerAction;

			$scope.clock.start();
		});
	});

	var processTick = function(tick){ //sent by admin at the specified frequency as long as all subjects have acknowledged the previous tick

		$scope.tick = tick;

		if(tick % $scope.ticksPerSubPeriod === 0) { //if this is the end of a sub period (in the "continuous" version, every tick is the end of a sub period)

			var reward = $scope.matrix[$scope.action - 1][$scope.partnerAction - 1][0]; //allocate reward based on the current user actions and the matrix
			$scope.rewards.push(reward);
			rs.add_points(reward * $scope.ticksPerSubPeriod / $scope.clock.getDurationInTicks()); //add the integral of the current reward value over the length of the subperiod
			var opponentReward = $scope.matrix[$scope.action - 1][$scope.partnerAction - 1][1];
			$scope.opponentRewards.push(opponentReward);

			$scope.prevPartnerAction = $scope.partnerAction;
		}

		if($scope.tEnableInput) {
			$scope.inputCountdown = Math.ceil(($scope.tEnableInput - $scope.tick) / CLOCK_FREQUENCY);
			if(tick >= $scope.tEnableInput) {
				$scope.actionsEnabled = true;
				$scope.tEnableInput = 0;
			}
		}

	};

	$scope.onAction = function(action) {
		if(action !== $scope.action){ //only trigger action events when selection has actually changed
			$scope.action = action;
			rs.trigger("action", action);
		}
	};

	rs.on("action", function(value){ //triggered when the user changes their selection
		$scope.action = value;
		if(rs.config.action_cost > 0) { //subtract the action cost specified in the config
			rs.add_points(-rs.config.action_cost);
		}
		if(rs.config.action_delay_s > 0) { //disable inputs for the action delay specified in the config
			$scope.tEnableInput = $scope.tick + (rs.config.action_delay_s * CLOCK_FREQUENCY);
			$scope.actionsEnabled = false;
		}
	});

	rs.recv("action", function(sender, value){ //receive other subjects actions
		if(sender === $scope.partner_id){ //if the other subject is the opponent, update their current action
			$scope.partnerAction = value;
		}
	});

	rs.on("simulation_complete", function(value) {
		$scope.actionsEnabled = false;
		rs.next_period(5); //request the framework to advance to the next period
	});

	var transpose = function(matrix) { //transpose a 2x2 matrix
		var transposed = [[[], []], [[], []]];
		for(var i = 0; i < 4; i++){
			var row = Math.floor(i/2);
			var column = i % 2;
			transposed[column][row] = [matrix[row][column][1], matrix[row][column][0]];
		}
		return transposed;
	};

}]);

Redwood.directive('plot', [function() {
	return {
		link: function($scope, elem, attr) {

			var plot = [], opponentPlot = [], subPeriods = [];

			$scope.$watch('config', function() {
				if($scope.ticksPerSubPeriod > 1){ //set up the sub period markers so they can be displayed on the plot
					var subPeriod = 0;
					do {
						subPeriod += $scope.ticksPerSubPeriod;
						subPeriods.push(subPeriod / $scope.clock.getDurationInTicks());
					} while(subPeriod < $scope.clock.getDurationInTicks());
				}
				replot();
			}, true);

			$scope.$watch('tick', function(tick) {
				if(tick % $scope.ticksPerSubPeriod === 0) {
					plot.push([($scope.tick - $scope.ticksPerSubPeriod) / $scope.clock.getDurationInTicks(), $scope.rewards[$scope.rewards.length - 1]]);
					plot.push([$scope.tick / $scope.clock.getDurationInTicks(), $scope.rewards[$scope.rewards.length - 1]]);
					opponentPlot.push([($scope.tick - $scope.ticksPerSubPeriod) / $scope.clock.getDurationInTicks(), $scope.opponentRewards[$scope.opponentRewards.length - 1]]);
					opponentPlot.push([$scope.tick / $scope.clock.getDurationInTicks(), $scope.opponentRewards[$scope.opponentRewards.length - 1]]);
					replot();
				}
				replot();
			}, true);

			function replot() {

				if(!$scope.config) return;

				var xRange = 1;
				var opts = {
					xaxis: { tickLength: 0, min: 0, max: xRange },
					yaxis: { tickLength: 0, min: 0, max: $scope.yMax + ($scope.yMax * 0.2) },
					series: { shadowSize: 0 }
				};
				var dataset = [];
				for(var p = 0; p < subPeriods.length; p++){ //mark each sub-period with a vertical red line
					dataset.push({
						data: [
							[subPeriods[p], opts.yaxis.min],
							[subPeriods[p], opts.yaxis.max]
						],
						lines: { lineWidth: 1 },
						color: "red"
					});
				}
				dataset.push({ //plot your rewards as a grey integral
					data: plot,
					lines: { fill: true, lineWidth: 0, fillColor: "grey" },
					color: "grey"
				});
				dataset.push({ //plot your opponent's rewards as a black line
					data: opponentPlot,
					lines: { lineWidth: 2 },
					color: "black"
				});

				dataset.push({ //display the current time indicator as a vertical grey line
					data: [
						[$scope.tick / $scope.clock.getDurationInTicks(), opts.yaxis.min],
						[$scope.tick / $scope.clock.getDurationInTicks(), opts.yaxis.max]
					],
					color: "grey"
				});

				$.plot(elem, dataset, opts);
			}
		}
	}
}]);<|endoftext|>


<commit_before>var itemModel = require('../models/itemModel');

module.exports = {
	findOrCreate: function(req, res){
		itemModel.findOne({'api_id':req.id},function(err, found){
			if(err){
				return null;
			} else{
				if(found){
					return found;
				}else{
					var item = new itemModel({
						api_id: req.id,
						name: req.name,
						nutrition: {
						}
					});

					item.save(function(err, newitem){
						return newitem;
					});
				}
			}
		});
	},
	delete: function(req,res){
		itemModel.remove({
			'item_id' : req.item_id
		}, function(err, item){
			if(err){
				return null;
			}
			return item;
		});
	}
};
<commit_msg>Added nutrition information to itemCtrl<commit_after>var itemModel = require('../models/itemModel');

module.exports = {
	findOrCreate: function(req, res){
		itemModel.findOne({'api_id':req.id},function(err, found){
			if(err){
				return null;
			} else{
				if(found){
					return found;
				}else{
					var item = new itemModel({
						api_id: req.id,
						name: req.name,
						nutrition: {
							energy: req.nutrients.eng,
							sugars: req.nutrients.sug,
							fat: req.nutrients.fat,
							carbs: req.nutrients.car,
							fiber: req.nutrients.fib
						}
					});

					item.save(function(err, newitem){
						return newitem;
					});
				}
			}
		});
	},
	delete: function(req,res){
		itemModel.remove({
			'item_id' : req.item_id
		}, function(err, item){
			if(err){
				return null;
			}
			return item;
		});
	}
};<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var express = require('../../lib/express');

var app = express.createServer();

// Ad-hoc example resource method

app.resource = function(path, obj) {
    this.get(path, obj.index);
    this.get(path + '/:a..:b', function(req, res){
        var a = parseInt(req.params.a, 10),
            b = parseInt(req.params.b, 10);
        obj.range(req, res, a, b);
    });
    this.get(path + '/:id', obj.show);
    this.del(path + '/:id', obj.destroy);
};

// Fake records

var users = [
    { name: 'tj' },
    { name: 'ciaran' },
    { name: 'aaron' },
    { name: 'guillermo' },
    { name: 'simon' },
    { name: 'tobi' }
];

// Fake model

var User = {
    index: function(req, res){
        res.send(users);
    },
    show: function(req, res){
        res.send(users[req.params.id] || { error: 'Cannot find user' });
    },
    destroy: function(req, res){
        var id = req.params.id;
        var destroyed = id in users;
        delete users[id];
        res.send(destroyed ? 'destroyed' : 'Cannot find user');
    },
    range: function(req, res, a, b){
        res.send(users.slice(a, b+1));
    }
};

// curl http://localhost:3000/users     -- responds with all users
// curl http://localhost:3000/users/1   -- responds with user 1
// curl http://localhost:3000/users/4   -- responds with error
// curl http://localhost:3000/users/1..3 -- responds with several users
// curl -X DELETE http://localhost:3000/users/1  -- deletes the user

app.resource('/users', User);

app.get('/', function(req, res){
    res.send([
        '
Examples:

',
        '
GET /users
',
        '
GET /users/1
',
        '
GET /users/3
',
        '
GET /users/1..3
',
        '
DELETE /users/4
',
        '
',
    ].join('\n'));
});

app.listen(3000);
console.log('Express app started on port 3000');<commit_msg>Example typo<commit_after>
/**
 * Module dependencies.
 */

var express = require('../../lib/express');

var app = express.createServer();

// Ad-hoc example resource method

app.resource = function(path, obj) {
    this.get(path, obj.index);
    this.get(path + '/:a..:b', function(req, res){
        var a = parseInt(req.params.a, 10),
            b = parseInt(req.params.b, 10);
        obj.range(req, res, a, b);
    });
    this.get(path + '/:id', obj.show);
    this.del(path + '/:id', obj.destroy);
};

// Fake records

var users = [
    { name: 'tj' },
    { name: 'ciaran' },
    { name: 'aaron' },
    { name: 'guillermo' },
    { name: 'simon' },
    { name: 'tobi' }
];

// Fake controller

var User = {
    index: function(req, res){
        res.send(users);
    },
    show: function(req, res){
        res.send(users[req.params.id] || { error: 'Cannot find user' });
    },
    destroy: function(req, res){
        var id = req.params.id;
        var destroyed = id in users;
        delete users[id];
        res.send(destroyed ? 'destroyed' : 'Cannot find user');
    },
    range: function(req, res, a, b){
        res.send(users.slice(a, b+1));
    }
};

// curl http://localhost:3000/users     -- responds with all users
// curl http://localhost:3000/users/1   -- responds with user 1
// curl http://localhost:3000/users/4   -- responds with error
// curl http://localhost:3000/users/1..3 -- responds with several users
// curl -X DELETE http://localhost:3000/users/1  -- deletes the user

app.resource('/users', User);

app.get('/', function(req, res){
    res.send([
        '
Examples:

',
        '
GET /users
',
        '
GET /users/1
',
        '
GET /users/3
',
        '
GET /users/1..3
',
        '
DELETE /users/4
',
        '
',
    ].join('\n'));
});

app.listen(3000);
console.log('Express app started on port 3000');<|endoftext|>


<commit_before>dojo.provide("dijit.layout.StackContainer");

dojo.require("dijit._Templated");
dojo.require("dijit.layout._LayoutWidget");
dojo.require("dijit.form.Button");

dojo.declare(
	"dijit.layout.StackContainer",
	dijit.layout._LayoutWidget,

	// summary
	//	A container that has multiple children, but shows only
	//	one child at a time (like looking at the pages in a book one by one).
	//
	//	Publishes topics <widgetId>-addChild, <widgetId>-removeChild, and <widgetId>-selectChild
	//
	//	Can be base class for container, Wizard, Show, etc.
{
	// doLayout: Boolean
	//  if true, change the size of my currently displayed child to match my size
	doLayout: true,

	_started: false,

	// selectedChildWidget: Widget
	//	References the currently selected child widget, if any

	postCreate: function(){
		this.connect(this.domNode, "onkeypress", this._onKeyPress);
	},

	startup: function(){
		if(this._started){ return; }

		var children = this.getChildren();

		// Setup each page panel
		dojo.forEach(children, this._setupChild, this);

		// Figure out which child to initially display
		dojo.some(children, function(child){
			if(child.selected){
				this.selectedChildWidget = child;
			}
			return child.selected;
		}, this);

		var selected = this.selectedChildWidget;

		// Default to the first child
		if(!selected && children[0]){
			selected = this.selectedChildWidget = children[0];
			selected.selected = true;
		}
		if(selected){
			this._showChild(selected);
		}

		// Now publish information about myself so any StackControllers can initialize..
		dojo.publish(this.id+"-startup", [{children: children, selected: selected}]);

		dijit.layout._LayoutWidget.prototype.startup.apply(this, arguments);
		this._started = true;
	},

	_setupChild: function(/*Widget*/ page){
		// Summary: prepare the given child

		page.domNode.style.display = "none";

		// since we are setting the width/height of the child elements, they need
		// to be position:relative, or IE has problems (See bug #2033)
		page.domNode.style.position = "relative";

		return page;
	},

	addChild: function(/*Widget*/ child, /*Integer?*/ insertIndex){
		dijit._Container.prototype.addChild.apply(this, arguments);
		child = this._setupChild(child);

		if(this._started){
			// in case the tab titles have overflowed from one line to two lines
			this.layout();

			dojo.publish(this.id+"-addChild", [child, insertIndex]);

			// if this is the first child, then select it
			if(!this.selectedChildWidget){
				this.selectChild(child);
			}
		}
	},

	removeChild: function(/*Widget*/ page){

		dijit._Container.prototype.removeChild.apply(this, arguments);

		// If we are being destroyed than don't run the code below (to select another page), because we are deleting
		// every page one by one
		if(this._beingDestroyed){ return; }

		if(this._started){
			// this will notify any tablists to remove a button; do this first because it may affect sizing
			dojo.publish(this.id+"-removeChild", [page]);

			// in case the tab titles now take up one line instead of two lines
			this.layout();
		}

		if(this.selectedChildWidget === page){
			this.selectedChildWidget = undefined;
			if(this._started){
				var children = this.getChildren();
				if(children.length){
					this.selectChild(children[0]);
				}
			}
		}
	},

	selectChild: function(/*Widget*/ page){
		// summary
		//	Show the given widget (which must be one of my children)

		page = dijit.byId(page);

		if(this.selectedChildWidget != page){
			// Deselect old page and select new one
			this._transition(page, this.selectedChildWidget);
			this.selectedChildWidget = page;
			dojo.publish(this.id+"-selectChild", [page]);
		}
	},

	_transition: function(/*Widget*/newWidget, /*Widget*/oldWidget){
		if(oldWidget){
			this._hideChild(oldWidget);
		}
		this._showChild(newWidget);

		// Size the new widget, in case this is the first time it's being shown,
		// or I have been resized since the last time it was shown.
		// page must be visible for resizing to work
		if(this.doLayout && newWidget.resize){
			newWidget.resize(this._containerContentBox || this._contentBox);
		}
	},

	_adjacent: function(/*Boolean*/ forward){
		// summary: Gets the next/previous child widget in this container from the current selection
		var children = this.getChildren();
		var index = dojo.indexOf(children, this.selectedChildWidget);
		index += forward ? 1 : children.length - 1;
		return children[ index % children.length ];
	},

	forward: function(){
		// Summary: advance to next page
		this.selectChild(this._adjacent(true));
	},

	back: function(){
		// Summary: go back to previous page
		this.selectChild(this._adjacent(false));
	},

	_onKeyPress: function(e){
		dojo.publish(this.id+"-containerKeyPress", [{ e: e, page: this}]);
	},

	layout: function(){
		if(this.doLayout && this.selectedChildWidget && this.selectedChildWidget.resize){
			this.selectedChildWidget.resize(this._contentBox);
		}
	},

	_showChild: function(/*Widget*/ page){
		var children = this.getChildren();
		page.isFirstChild = (page == children[0]);
		page.isLastChild = (page == children[children.length-1]);
		page.selected = true;

		page.domNode.style.display="";
		if(page._loadCheck){
			page._loadCheck(); // trigger load in ContentPane
		}
		if(page.onShow){
			page.onShow();
		}
	},

	_hideChild: function(/*Widget*/ page){
		page.selected=false;
		page.domNode.style.display="none";
		if(page.onHide){
			page.onHide();
		}
	},

	closeChild: function(/*Widget*/ page){
		// summary
		//	callback when user clicks the [X] to remove a page
		//	if onClose() returns true then remove and destroy the childd
		var remove = page.onClose(this, page);
		if(remove){
			this.removeChild(page);
			// makes sure we can clean up executeScripts in ContentPane onUnLoad
			page.destroy();
		}
	},

	destroy: function(){
		this._beingDestroyed = true;
		dijit.layout.StackContainer.superclass.destroy.apply(this, arguments);
	}
});

dojo.declare(
	"dijit.layout.StackController",
	[dijit._Widget, dijit._Templated, dijit._Container],
	{
		// summary
		//	Set of buttons to select a page in a page list.
		//	Monitors the specified StackContainer, and whenever a page is
		//	added, deleted, or selected, updates itself accordingly.

		templateString: "",

		// containerId: String
		//	the id of the page container that I point to
		containerId: "",

		// buttonWidget: String
		//	the name of the button widget to create to correspond to each page
		buttonWidget: "dijit.layout._StackButton",

		postCreate: function(){
			dijit.setWaiRole(this.domNode, "tablist");

			this.pane2button = {};		// mapping from panes to buttons
			this._subscriptions=[
				dojo.subscribe(this.containerId+"-startup", this, "onStartup"),
				dojo.subscribe(this.containerId+"-addChild", this, "onAddChild"),
				dojo.subscribe(this.containerId+"-removeChild", this, "onRemoveChild"),
				dojo.subscribe(this.containerId+"-selectChild", this, "onSelectChild"),
				dojo.subscribe(this.containerId+"-containerKeyPress", this, "onContainerKeyPress")
			];
		},

		onStartup: function(/*Object*/ info){
			// summary: called after StackContainer has finished initializing
			dojo.forEach(info.children, this.onAddChild, this);
			this.onSelectChild(info.selected);
		},

		destroy: function(){
			dojo.forEach(this._subscriptions, dojo.unsubscribe);
			dijit.layout.StackController.superclass.destroy.apply(this, arguments);
		},

		onAddChild: function(/*Widget*/ page, /*Integer?*/ insertIndex){
			// summary
			//   Called whenever a page is added to the container.
			//   Create button corresponding to the page.

			// add a node that will be promoted to the button widget
			var refNode = document.createElement("span");
			this.domNode.appendChild(refNode);
			// create an instance of the button widget
			var cls = dojo.getObject(this.buttonWidget);
			var button = new cls({label: page.title, closeButton: page.closable}, refNode);
			this.addChild(button, insertIndex);
			this.pane2button[page] = button;
			page.controlButton = button;	// this value might be overwritten if two tabs point to same container

			var _this = this;
			dojo.connect(button, "onClick", function(){ _this.onButtonClick(page); });
			dojo.connect(button, "onClickCloseButton", function(){ _this.onCloseButtonClick(page); });
			if(!this._currentChild){ // put the first child into the tab order
				button.focusNode.setAttribute("tabIndex","0");
				this._currentChild = page;
			}
		},

		onRemoveChild: function(/*Widget*/ page){
			// summary
			//   Called whenever a page is removed from the container.
			//   Remove the button corresponding to the page.
			if(this._currentChild === page){ this._currentChild = null; }
			var button = this.pane2button[page];
			if(button){
				// TODO? if current child { reassign }
				button.destroy();
			}
			this.pane2button[page] = null;
		},

		onSelectChild: function(/*Widget*/ page){
			// Summary
			//	Called when a page has been selected in the StackContainer, either by me or by another StackController

			if(!page){ return; }

			if(this._currentChild){
				var oldButton=this.pane2button[this._currentChild];
				oldButton.setChecked(false);
				oldButton.focusNode.setAttribute("tabIndex", "-1");
			}

			var newButton=this.pane2button[page];
			newButton.setChecked(true);
			this._currentChild = page;
			newButton.focusNode.setAttribute("tabIndex", "0");
		},

		onButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons is pressed in an attempt to select a page
			var container = dijit.byId(this.containerId);	// TODO: do this via topics?
			container.selectChild(page);
		},

		onCloseButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons [X] is pressed in an attempt to close a page
			var container = dijit.byId(this.containerId);
			container.closeChild(page);
			var b = this.pane2button[this._currentChild];
			if(b){
				dijit.focus(b.focusNode || b.domNode);
			}
		},

		// TODO: this is a bit redundant with forward, back api in StackContainer
		adjacent: function(/*Boolean*/ forward){
			// find currently focused button in children array
			var children = this.getChildren();
			var current = dojo.indexOf(children, this.pane2button[this._currentChild]);
			// pick next button to focus on
			var offset = forward ? 1 : children.length - 1;
			return children[ (current + offset) % children.length ];
		},

		onkeypress: function(/*Event*/ e){
			// summary:
			//   Handle keystrokes on the page list, for advancing to next/previous button
			//   and closing the current page.

			if(this.disabled || e.altKey ){ return; }
			var forward = true;
			if(e.ctrlKey || !e._djpage){
				var k = dojo.keys;
				switch(e.keyCode){
					case k.LEFT_ARROW:
					case k.UP_ARROW:
					case k.PAGE_UP:
						forward = false;
						// fall through
					case k.RIGHT_ARROW:
					case k.DOWN_ARROW:
					case k.PAGE_DOWN:
						this.adjacent(forward).onClick();
						dojo.stopEvent(e);
						break;
					case k.DELETE:
						if(this._currentChild.closable){
							this.onCloseButtonClick(this._currentChild);
						}
						dojo.stopEvent(e);
						break;
					default:
						if(e.ctrlKey){
							if(e.keyCode == k.TAB){
								this.adjacent(!e.shiftKey).onClick();
								dojo.stopEvent(e);
							}else if(e.keyChar == "w"){
								if(this._currentChild.closable){
									this.onCloseButtonClick(this._currentChild);
								}
								dojo.stopEvent(e); // avoid browser tab closing.
							}
						}
				}
			}
		},

		onContainerKeyPress: function(/*Object*/ info){
			info.e._djpage = info.page;
			this.onkeypress(info.e);
		}
});

dojo.declare(
	"dijit.layout._StackButton",
	dijit.form.ToggleButton,
{
	// summary
	//	Internal widget used by StackContainer.
	//	The button-like or tab-like object you click to select or delete a page

	tabIndex: "-1", // StackContainer buttons are not in the tab order by default

	onClick: function(/*Event*/ evt){
		// this is for TabContainer where the tabs are  rather than button,
		// so need to set focus explicitly (on some browsers)
		dijit.focus(this.focusNode);

		// ... now let StackController catch the event and tell me what to do
	},

	onClickCloseButton: function(/*Event*/ evt){
		// summary
		//	StackContainer connects to this function; if your widget contains a close button
		//	then clicking it should call this function.
		evt.stopPropagation();
	}
});

// These arguments can be specified for the children of a StackContainer.
// Since any widget can be specified as a StackContainer child, mix them
// into the base widget class.  (This is a hack, but it's effective.)
dojo.extend(dijit._Widget, {
	// title: String
	//		Title of this widget.  Used by TabContainer to the name the tab, etc.
	title: "",

	// selected: Boolean
	//		Is this child currently selected?
	selected: false,

	// closable: Boolean
	//		True if user can close (destroy) this child, such as (for example) clicking the X on the tab.
	closable: false,	// true if user can close this tab pane

	onClose: function(){
		// summary: Callback if someone tries to close the child, child will be closed if func returns true
		return true;
	}
});
<commit_msg>Modified comment to reflect some pages are not closable. (Refs #4787)<commit_after>dojo.provide("dijit.layout.StackContainer");

dojo.require("dijit._Templated");
dojo.require("dijit.layout._LayoutWidget");
dojo.require("dijit.form.Button");

dojo.declare(
	"dijit.layout.StackContainer",
	dijit.layout._LayoutWidget,

	// summary
	//	A container that has multiple children, but shows only
	//	one child at a time (like looking at the pages in a book one by one).
	//
	//	Publishes topics <widgetId>-addChild, <widgetId>-removeChild, and <widgetId>-selectChild
	//
	//	Can be base class for container, Wizard, Show, etc.
{
	// doLayout: Boolean
	//  if true, change the size of my currently displayed child to match my size
	doLayout: true,

	_started: false,

	// selectedChildWidget: Widget
	//	References the currently selected child widget, if any

	postCreate: function(){
		this.connect(this.domNode, "onkeypress", this._onKeyPress);
	},

	startup: function(){
		if(this._started){ return; }

		var children = this.getChildren();

		// Setup each page panel
		dojo.forEach(children, this._setupChild, this);

		// Figure out which child to initially display
		dojo.some(children, function(child){
			if(child.selected){
				this.selectedChildWidget = child;
			}
			return child.selected;
		}, this);

		var selected = this.selectedChildWidget;

		// Default to the first child
		if(!selected && children[0]){
			selected = this.selectedChildWidget = children[0];
			selected.selected = true;
		}
		if(selected){
			this._showChild(selected);
		}

		// Now publish information about myself so any StackControllers can initialize..
		dojo.publish(this.id+"-startup", [{children: children, selected: selected}]);

		dijit.layout._LayoutWidget.prototype.startup.apply(this, arguments);
		this._started = true;
	},

	_setupChild: function(/*Widget*/ page){
		// Summary: prepare the given child

		page.domNode.style.display = "none";

		// since we are setting the width/height of the child elements, they need
		// to be position:relative, or IE has problems (See bug #2033)
		page.domNode.style.position = "relative";

		return page;
	},

	addChild: function(/*Widget*/ child, /*Integer?*/ insertIndex){
		dijit._Container.prototype.addChild.apply(this, arguments);
		child = this._setupChild(child);

		if(this._started){
			// in case the tab titles have overflowed from one line to two lines
			this.layout();

			dojo.publish(this.id+"-addChild", [child, insertIndex]);

			// if this is the first child, then select it
			if(!this.selectedChildWidget){
				this.selectChild(child);
			}
		}
	},

	removeChild: function(/*Widget*/ page){

		dijit._Container.prototype.removeChild.apply(this, arguments);

		// If we are being destroyed than don't run the code below (to select another page), because we are deleting
		// every page one by one
		if(this._beingDestroyed){ return; }

		if(this._started){
			// this will notify any tablists to remove a button; do this first because it may affect sizing
			dojo.publish(this.id+"-removeChild", [page]);

			// in case the tab titles now take up one line instead of two lines
			this.layout();
		}

		if(this.selectedChildWidget === page){
			this.selectedChildWidget = undefined;
			if(this._started){
				var children = this.getChildren();
				if(children.length){
					this.selectChild(children[0]);
				}
			}
		}
	},

	selectChild: function(/*Widget*/ page){
		// summary
		//	Show the given widget (which must be one of my children)

		page = dijit.byId(page);

		if(this.selectedChildWidget != page){
			// Deselect old page and select new one
			this._transition(page, this.selectedChildWidget);
			this.selectedChildWidget = page;
			dojo.publish(this.id+"-selectChild", [page]);
		}
	},

	_transition: function(/*Widget*/newWidget, /*Widget*/oldWidget){
		if(oldWidget){
			this._hideChild(oldWidget);
		}
		this._showChild(newWidget);

		// Size the new widget, in case this is the first time it's being shown,
		// or I have been resized since the last time it was shown.
		// page must be visible for resizing to work
		if(this.doLayout && newWidget.resize){
			newWidget.resize(this._containerContentBox || this._contentBox);
		}
	},

	_adjacent: function(/*Boolean*/ forward){
		// summary: Gets the next/previous child widget in this container from the current selection
		var children = this.getChildren();
		var index = dojo.indexOf(children, this.selectedChildWidget);
		index += forward ? 1 : children.length - 1;
		return children[ index % children.length ];
	},

	forward: function(){
		// Summary: advance to next page
		this.selectChild(this._adjacent(true));
	},

	back: function(){
		// Summary: go back to previous page
		this.selectChild(this._adjacent(false));
	},

	_onKeyPress: function(e){
		dojo.publish(this.id+"-containerKeyPress", [{ e: e, page: this}]);
	},

	layout: function(){
		if(this.doLayout && this.selectedChildWidget && this.selectedChildWidget.resize){
			this.selectedChildWidget.resize(this._contentBox);
		}
	},

	_showChild: function(/*Widget*/ page){
		var children = this.getChildren();
		page.isFirstChild = (page == children[0]);
		page.isLastChild = (page == children[children.length-1]);
		page.selected = true;

		page.domNode.style.display="";
		if(page._loadCheck){
			page._loadCheck(); // trigger load in ContentPane
		}
		if(page.onShow){
			page.onShow();
		}
	},

	_hideChild: function(/*Widget*/ page){
		page.selected=false;
		page.domNode.style.display="none";
		if(page.onHide){
			page.onHide();
		}
	},

	closeChild: function(/*Widget*/ page){
		// summary
		//	callback when user clicks the [X] to remove a page
		//	if onClose() returns true then remove and destroy the childd
		var remove = page.onClose(this, page);
		if(remove){
			this.removeChild(page);
			// makes sure we can clean up executeScripts in ContentPane onUnLoad
			page.destroy();
		}
	},

	destroy: function(){
		this._beingDestroyed = true;
		dijit.layout.StackContainer.superclass.destroy.apply(this, arguments);
	}
});

dojo.declare(
	"dijit.layout.StackController",
	[dijit._Widget, dijit._Templated, dijit._Container],
	{
		// summary
		//	Set of buttons to select a page in a page list.
		//	Monitors the specified StackContainer, and whenever a page is
		//	added, deleted, or selected, updates itself accordingly.

		templateString: "",

		// containerId: String
		//	the id of the page container that I point to
		containerId: "",

		// buttonWidget: String
		//	the name of the button widget to create to correspond to each page
		buttonWidget: "dijit.layout._StackButton",

		postCreate: function(){
			dijit.setWaiRole(this.domNode, "tablist");

			this.pane2button = {};		// mapping from panes to buttons
			this._subscriptions=[
				dojo.subscribe(this.containerId+"-startup", this, "onStartup"),
				dojo.subscribe(this.containerId+"-addChild", this, "onAddChild"),
				dojo.subscribe(this.containerId+"-removeChild", this, "onRemoveChild"),
				dojo.subscribe(this.containerId+"-selectChild", this, "onSelectChild"),
				dojo.subscribe(this.containerId+"-containerKeyPress", this, "onContainerKeyPress")
			];
		},

		onStartup: function(/*Object*/ info){
			// summary: called after StackContainer has finished initializing
			dojo.forEach(info.children, this.onAddChild, this);
			this.onSelectChild(info.selected);
		},

		destroy: function(){
			dojo.forEach(this._subscriptions, dojo.unsubscribe);
			dijit.layout.StackController.superclass.destroy.apply(this, arguments);
		},

		onAddChild: function(/*Widget*/ page, /*Integer?*/ insertIndex){
			// summary
			//   Called whenever a page is added to the container.
			//   Create button corresponding to the page.

			// add a node that will be promoted to the button widget
			var refNode = document.createElement("span");
			this.domNode.appendChild(refNode);
			// create an instance of the button widget
			var cls = dojo.getObject(this.buttonWidget);
			var button = new cls({label: page.title, closeButton: page.closable}, refNode);
			this.addChild(button, insertIndex);
			this.pane2button[page] = button;
			page.controlButton = button;	// this value might be overwritten if two tabs point to same container

			var _this = this;
			dojo.connect(button, "onClick", function(){ _this.onButtonClick(page); });
			dojo.connect(button, "onClickCloseButton", function(){ _this.onCloseButtonClick(page); });
			if(!this._currentChild){ // put the first child into the tab order
				button.focusNode.setAttribute("tabIndex","0");
				this._currentChild = page;
			}
		},

		onRemoveChild: function(/*Widget*/ page){
			// summary
			//   Called whenever a page is removed from the container.
			//   Remove the button corresponding to the page.
			if(this._currentChild === page){ this._currentChild = null; }
			var button = this.pane2button[page];
			if(button){
				// TODO? if current child { reassign }
				button.destroy();
			}
			this.pane2button[page] = null;
		},

		onSelectChild: function(/*Widget*/ page){
			// Summary
			//	Called when a page has been selected in the StackContainer, either by me or by another StackController

			if(!page){ return; }

			if(this._currentChild){
				var oldButton=this.pane2button[this._currentChild];
				oldButton.setChecked(false);
				oldButton.focusNode.setAttribute("tabIndex", "-1");
			}

			var newButton=this.pane2button[page];
			newButton.setChecked(true);
			this._currentChild = page;
			newButton.focusNode.setAttribute("tabIndex", "0");
		},

		onButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons is pressed in an attempt to select a page
			var container = dijit.byId(this.containerId);	// TODO: do this via topics?
			container.selectChild(page);
		},

		onCloseButtonClick: function(/*Widget*/ page){
			// summary
			//   Called whenever one of my child buttons [X] is pressed in an attempt to close a page
			var container = dijit.byId(this.containerId);
			container.closeChild(page);
			var b = this.pane2button[this._currentChild];
			if(b){
				dijit.focus(b.focusNode || b.domNode);
			}
		},

		// TODO: this is a bit redundant with forward, back api in StackContainer
		adjacent: function(/*Boolean*/ forward){
			// find currently focused button in children array
			var children = this.getChildren();
			var current = dojo.indexOf(children, this.pane2button[this._currentChild]);
			// pick next button to focus on
			var offset = forward ? 1 : children.length - 1;
			return children[ (current + offset) % children.length ];
		},

		onkeypress: function(/*Event*/ e){
			// summary:
			//   Handle keystrokes on the page list, for advancing to next/previous button
			//   and closing the current page if the page is closable.

			if(this.disabled || e.altKey ){ return; }
			var forward = true;
			if(e.ctrlKey || !e._djpage){
				var k = dojo.keys;
				switch(e.keyCode){
					case k.LEFT_ARROW:
					case k.UP_ARROW:
					case k.PAGE_UP:
						forward = false;
						// fall through
					case k.RIGHT_ARROW:
					case k.DOWN_ARROW:
					case k.PAGE_DOWN:
						this.adjacent(forward).onClick();
						dojo.stopEvent(e);
						break;
					case k.DELETE:
						if(this._currentChild.closable){
							this.onCloseButtonClick(this._currentChild);
						}
						dojo.stopEvent(e);
						break;
					default:
						if(e.ctrlKey){
							if(e.keyCode == k.TAB){
								this.adjacent(!e.shiftKey).onClick();
								dojo.stopEvent(e);
							}else if(e.keyChar == "w"){
								if(this._currentChild.closable){
									this.onCloseButtonClick(this._currentChild);
								}
								dojo.stopEvent(e); // avoid browser tab closing.
							}
						}
				}
			}
		},

		onContainerKeyPress: function(/*Object*/ info){
			info.e._djpage = info.page;
			this.onkeypress(info.e);
		}
});

dojo.declare(
	"dijit.layout._StackButton",
	dijit.form.ToggleButton,
{
	// summary
	//	Internal widget used by StackContainer.
	//	The button-like or tab-like object you click to select or delete a page

	tabIndex: "-1", // StackContainer buttons are not in the tab order by default

	onClick: function(/*Event*/ evt){
		// this is for TabContainer where the tabs are  rather than button,
		// so need to set focus explicitly (on some browsers)
		dijit.focus(this.focusNode);

		// ... now let StackController catch the event and tell me what to do
	},

	onClickCloseButton: function(/*Event*/ evt){
		// summary
		//	StackContainer connects to this function; if your widget contains a close button
		//	then clicking it should call this function.
		evt.stopPropagation();
	}
});

// These arguments can be specified for the children of a StackContainer.
// Since any widget can be specified as a StackContainer child, mix them
// into the base widget class.  (This is a hack, but it's effective.)
dojo.extend(dijit._Widget, {
	// title: String
	//		Title of this widget.  Used by TabContainer to the name the tab, etc.
	title: "",

	// selected: Boolean
	//		Is this child currently selected?
	selected: false,

	// closable: Boolean
	//		True if user can close (destroy) this child, such as (for example) clicking the X on the tab.
	closable: false,	// true if user can close this tab pane

	onClose: function(){
		// summary: Callback if someone tries to close the child, child will be closed if func returns true
		return true;
	}
});
<|endoftext|>


<commit_before>import { ActionTypes } from '../createStore';
import isPlainObject from '../utils/isPlainObject';
import mapValues from '../utils/mapValues';
import pick from '../utils/pick';

/* eslint-disable no-console */

function getErrorMessage(key, action) {
  var actionType = action && action.type;
  var actionName = actionType && `"${actionType.toString()}"` || 'an action';

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  );
}

function verifyStateShape(inputState, outputState, action) {
  var reducerKeys = Object.keys(outputState);
  var argumentName = action && action.type === ActionTypes.INIT ?
    'initialState argument passed to createStore' :
    'previous state received by the reducer';

  if (reducerKeys.length === 0) {
    console.error(
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
    return;
  }

  if (!isPlainObject(inputState)) {
    console.error(
      `The ${argumentName} has unexpected type of "` +
      ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
    return;
  }

  var unexpectedKeys = Object.keys(inputState).filter(
    key => reducerKeys.indexOf(key) < 0
  );

  if (unexpectedKeys.length > 0) {
    console.error(
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */

export default function combineReducers(reducers) {
  var finalReducers = pick(reducers, (val) => typeof val === 'function');
  var sanityError;

  Object.keys(finalReducers).forEach(key => {
    var reducer = finalReducers[key];
    if (!sanityError && typeof reducer(undefined, { type: ActionTypes.INIT }) === 'undefined') {
      sanityError = new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      );
    }

    var type = Math.random().toString(36).substring(7).split('').join('.');
    if (!sanityError && typeof reducer(undefined, { type }) === 'undefined') {
      sanityError = new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      );
    }
  });

  var defaultState = mapValues(finalReducers, () => undefined);

  return function combination(state = defaultState, action) {
    if (sanityError) {
      throw sanityError;
    }
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var newState = reducer(state[key], action);
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState;
    });

    if (process.env.NODE_ENV !== 'production') {
      verifyStateShape(state, finalState, action);
    }

    return finalState;
  };
}
<commit_msg>Add prefix to randomly generated action type in combineReducers #792<commit_after>import { ActionTypes } from '../createStore';
import isPlainObject from '../utils/isPlainObject';
import mapValues from '../utils/mapValues';
import pick from '../utils/pick';

/* eslint-disable no-console */

function getErrorMessage(key, action) {
  var actionType = action && action.type;
  var actionName = actionType && `"${actionType.toString()}"` || 'an action';

  return (
    `Reducer "${key}" returned undefined handling ${actionName}. ` +
    `To ignore an action, you must explicitly return the previous state.`
  );
}

function verifyStateShape(inputState, outputState, action) {
  var reducerKeys = Object.keys(outputState);
  var argumentName = action && action.type === ActionTypes.INIT ?
    'initialState argument passed to createStore' :
    'previous state received by the reducer';

  if (reducerKeys.length === 0) {
    console.error(
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    );
    return;
  }

  if (!isPlainObject(inputState)) {
    console.error(
      `The ${argumentName} has unexpected type of "` +
      ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    );
    return;
  }

  var unexpectedKeys = Object.keys(inputState).filter(
    key => reducerKeys.indexOf(key) < 0
  );

  if (unexpectedKeys.length > 0) {
    console.error(
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    );
  }
}

/**
 * Turns an object whose values are different reducer functions, into a single
 * reducer function. It will call every child reducer, and gather their results
 * into a single state object, whose keys correspond to the keys of the passed
 * reducer functions.
 *
 * @param {Object} reducers An object whose values correspond to different
 * reducer functions that need to be combined into one. One handy way to obtain
 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
 * undefined for any action. Instead, they should return their initial state
 * if the state passed to them was undefined, and the current state for any
 * unrecognized action.
 *
 * @returns {Function} A reducer function that invokes every reducer inside the
 * passed object, and builds a state object with the same shape.
 */

export default function combineReducers(reducers) {
  var finalReducers = pick(reducers, (val) => typeof val === 'function');
  var sanityError;

  Object.keys(finalReducers).forEach(key => {
    var reducer = finalReducers[key];
    if (!sanityError && typeof reducer(undefined, { type: ActionTypes.INIT }) === 'undefined') {
      sanityError = new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      );
    }

    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.');
    if (!sanityError && typeof reducer(undefined, { type }) === 'undefined') {
      sanityError = new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      );
    }
  });

  var defaultState = mapValues(finalReducers, () => undefined);

  return function combination(state = defaultState, action) {
    if (sanityError) {
      throw sanityError;
    }
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var newState = reducer(state[key], action);
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState;
    });

    if (process.env.NODE_ENV !== 'production') {
      verifyStateShape(state, finalState, action);
    }

    return finalState;
  };
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { connect } from 'react-redux';
import storage from 'electron-json-storage';
import TwitterClient from '../utils/twitterClient';
import { inputUpdate, requestPostUpdate } from '../actions/update';

class PostForm extends Component {
    inputUpdate(e) {
        this.props.dispatch(inputUpdate(e.target.value));
    }

    postUpdate() {
        this.props.dispatch(requestPostUpdate(this.refs.text.value));
    }

    render() {
        return (
            <form className="PostForm">
                <textarea className="PostForm__textarea" placeholder="What are you doing now?" ref="text" value={this.props.data.update} onChange={this.inputUpdate.bind(this)}></textarea>

                    <span className={this.props.data.remainingCharacters < 0 ? 'PostForm__overLength mr10px' : 'mrr10px'}>{this.props.data.remainingCharacters}
                    {
                        this.props.data.postingUpdate
                        ?
                        <input className="Button Button__normal disabled" type="button" value="Posting..." onClick={this.postUpdate.bind(this)} />
                        :
                        <input className="Button Button__normal" type="button" value="Post" onClick={this.postUpdate.bind(this)} />
                    }

            </form>
        );
    }
}

export default connect(function (state) {
    return {
        data : state.updateReducer
    };
})(PostForm);
<commit_msg>[update] adjust margin<commit_after>import React, { Component } from 'react';
import { connect } from 'react-redux';
import storage from 'electron-json-storage';
import TwitterClient from '../utils/twitterClient';
import { inputUpdate, requestPostUpdate } from '../actions/update';

class PostForm extends Component {
    inputUpdate(e) {
        this.props.dispatch(inputUpdate(e.target.value));
    }

    postUpdate() {
        this.props.dispatch(requestPostUpdate(this.refs.text.value));
    }

    render() {
        return (
            <form className="PostForm">
                <textarea className="PostForm__textarea" placeholder="What are you doing now?" ref="text" value={this.props.data.update} onChange={this.inputUpdate.bind(this)}></textarea>

                    <span className={this.props.data.remainingCharacters < 0 ? 'PostForm__overLength mr15px' : 'mr15px'}>{this.props.data.remainingCharacters}
                    {
                        this.props.data.postingUpdate
                        ?
                        <input className="Button Button__normal disabled" type="button" value="Posting..." onClick={this.postUpdate.bind(this)} />
                        :
                        <input className="Button Button__normal" type="button" value="Post" onClick={this.postUpdate.bind(this)} />
                    }

            </form>
        );
    }
}

export default connect(function (state) {
    return {
        data : state.updateReducer
    };
})(PostForm);
<|endoftext|>


<commit_before>// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the License.txt file included with this file.

/** @scope modules */

// TODO: proper backwards search - implement our own component?
//     : implement our own highlighter?
//     : <ESC> should cancel search highlighting in 'incsearch' mode and jump
//       back to the presearch page location - can probably use the same
//       solution as marks
//     : 'linksearch' searches should highlight link matches only
//     : changing any search settings should also update the search state including highlighting
//     : incremental searches shouldn't permanently update search modifiers
//
// TODO: Clean up this rat's nest. --Kris

/**
 * @instance finder
 */
const Finder = Module("finder", {
    requires: ["config"],

    init: function () {
        const self = this;

        this._found = false;               // true if the last search was successful
        this._backwards = false;           // currently searching backwards
        this._searchString = "";           // current search string (without modifiers)
        this._searchPattern = "";          // current search string (includes modifiers)
        this._lastSearchPattern = "";      // the last searched pattern (includes modifiers)
        this._lastSearchString = "";       // the last searched string (without modifiers)
        this._lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
        this._caseSensitive = false;       // search string is case sensitive
        this._linksOnly = false;           // search is limited to link text only
    },

    // set searchString, searchPattern, caseSensitive, linksOnly
    _processUserPattern: function (pattern) {
        //// strip off pattern terminator and offset
        //if (backwards)
        //    pattern = pattern.replace(/\?.*/, "");
        //else
        //    pattern = pattern.replace(/\/.*/, "");

        this._searchPattern = pattern;

        // links only search - \l wins if both modifiers specified
        if (/\\l/.test(pattern))
            this._linksOnly = true;
        else if (/\L/.test(pattern))
            this._linksOnly = false;
        else if (options["linksearch"])
            this._linksOnly = true;
        else
            this._linksOnly = false;

        // strip links-only modifiers
        pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // case sensitivity - \c wins if both modifiers specified
        if (/\c/.test(pattern))
            this._caseSensitive = false;
        else if (/\C/.test(pattern))
            this._caseSensitive = true;
        else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern))
            this._caseSensitive = true;
        else if (options["ignorecase"])
            this._caseSensitive = false;
        else
            this._caseSensitive = true;

        // strip case-sensitive modifiers
        pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // remove any modifier escape \
        pattern = pattern.replace(/\\(\\[cClL])/g, "$1");

        this._searchString = pattern;
    },

    /**
     * Called when the search dialog is requested.
     *
     * @param {number} mode The search mode, either modes.SEARCH_FORWARD or
     *     modes.SEARCH_BACKWARD.
     * @default modes.SEARCH_FORWARD
     */
    openPrompt: function (mode) {
        this._backwards = mode == modes.SEARCH_BACKWARD;
        //commandline.open(this._backwards ? "Find backwards" : "Find", "", mode);
        commandline.input(this._backwards ? "Find backwards" : "Find", this.closure.onSubmit, {
            onChange: function() { if (options["incsearch"]) finder.find(commandline.command) }

        });
        //modes.extended = mode;

        // TODO: focus the top of the currently visible screen
    },

    // TODO: backwards seems impossible i fear
    /**
     * Searches the current buffer for str.
     *
     * @param {string} str The string to find.
     * @see Bug537013 https://bugzilla.mozilla.org/show_bug.cgi?id=537013
     */
    find: Services.vc.compare(Services.appinfo.version, "24.0") > 0 ?
    function (str) {
        let fastFind = config.browser.getFindBar();
        this._processUserPattern(str);
        fastFind._typeAheadCaseSensitive = this._caseSensitive;
        fastFind._typeAheadLinksOnly = this._linksOnly;
        let result = fastFind._find(str);
    } :
    // FIXME: remove when minVersion > 24
    function (str) {
        let fastFind = config.browser.fastFind;

        this._processUserPattern(str);
        fastFind.caseSensitive = this._caseSensitive;
        let result = fastFind.find(this._searchString, this._linksOnly);
        this._displayFindResult(result, this._backwards);
    },

    /**
     * Searches the current buffer again for the most recently used search
     * string.
     *
     * @param {boolean} reverse Whether to search forwards or backwards.
     * @default false
     * @see Bug537013 https://bugzilla.mozilla.org/show_bug.cgi?id=537013
     */
    findAgain: Services.vc.compare(Services.appinfo.version, "24.0") > 0 ?
    function (reverse) {
        let fastFind = config.browser.getFindBar();
        if (fastFind._findField.value != this._lastSearchString)
            this.find(this._lastSearchString);

        let backwards = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards;
        fastFind._typeAheadLinksOnly = this._linksOnly;
        let result = fastFind._findAgain(backwards);
        this._displayFindResult(result, backwards);
    } :
    // FIXME: remove when minVersion > 24
    function (reverse) {
        // This hack is needed to make n/N work with the correct string, if
        // we typed /foo<esc> after the original search.  Since searchString is
        // readonly we have to call find() again to update it.
        if (config.browser.fastFind.searchString != this._lastSearchString)
            this.find(this._lastSearchString);

        let backwards = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards;
        let result = config.browser.fastFind.findAgain(backwards, this._linksOnly);
        this._displayFindResult(result, backwards);

    },

    _displayFindResult: function(result, backwards) {
        if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
            liberator.echoerr("Pattern not found: " + this._searchString, commandline.FORCE_SINGLELINE);
        }
        else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED) {
            let msg = backwards ? "Search hit TOP, continuing at BOTTOM" : "Search hit BOTTOM, continuing at TOP";
            commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
        }
        else {
            liberator.echomsg("Found pattern: " + this._searchString);
        }
    },

    /**
     * Called when the user types a key in the search dialog. Triggers a
     * search attempt if 'incsearch' is set.
     *
     * @param {string} str The search string.
     */
    /*onKeyPress: function (str) {
        if (options["incsearch"])
            this.find(str);
        },*/

    /**
     * Called when the <Enter> key is pressed to trigger a search.
     *
     * @param {string} str The search string.
     * @param {boolean} forcedBackward Whether to search forwards or
     *     backwards. This overrides the direction set in
     *     (@link #openPrompt).
     * @default false
     */
    onSubmit: function (str, forcedBackward) {
        if (typeof forcedBackward === "boolean")
            this._backwards = forcedBackward;

        if (str)
            var pattern = str;
        else {
            liberator.assert(this._lastSearchPattern, "No previous search pattern");
            pattern = this._lastSearchPattern;
        }

        this.clear();

        // liberator.log('inc: ' + options["incsearch"] + ' sea:' + this._searchPattern + ' pat:' + pattern);
        if (!options["incsearch"] /*|| !str || !this._found */|| this._searchPattern != pattern) {
            // prevent any current match from matching again
            if (!window.content.getSelection().isCollapsed)
                window.content.getSelection().getRangeAt(0).collapse(this._backwards);

            this.find(pattern);
        }

        // focus links after searching, so the user can just hit <Enter> another time to follow the link
        // This has to be done async, because the mode reset after onSubmit would
        // clear the focus
        let elem = Services.vc.compare(Services.appinfo.version, "24.0") > 0 ?
                    config.browser.getFindBar()._foundLinkRef.get() :
                    config.browser.fastFind.foundLink; // FIXME: remove when minVersion > 24
        this.setTimeout(function() {
            if (elem)
                elem.focus();
                // fm.moveFocus(elem.ownerDocument.defaultView, null, Ci.nsIFocusManager.MOVEFOCUS_CARET, Ci.nsIFocusManager.FLAG_NOSCROLL);*/
        }, 0);

        this._lastSearchBackwards = this._backwards;
        this._lastSearchPattern = pattern;
        this._lastSearchString = this._searchString;

        // TODO: move to find() when reverse incremental searching is kludged in
        // need to find again for reverse searching
        if (this._backwards)
            this.setTimeout(function () { this.findAgain(false); }, 0);

        if (options["hlsearch"])
            this.highlight(this._searchString);
    },

    /**
     * Highlights all occurances of str in the buffer.
     *
     * @param {string} str The string to highlight.
     */
    highlight: function (str) {
        // FIXME: Thunderbird incompatible
        if (config.name == "Muttator")
            return;

        if (window.gFindBar) {
            window.gFindBar._setCaseSensitivity(this._caseSensitive);
            window.gFindBar._highlightDoc(false);
            window.gFindBar._highlightDoc(true, str);
        }
    },

    /**
     * Clears all search highlighting.
     */
    clear: function () {
        // FIXME: Thunderbird incompatible
        if (config.name == "Muttator")
            return;

        if (window.gFindBar)
            window.gFindBar._highlightDoc(false);
    }
}, {
}, {
    commands: function () {
        // TODO: Remove in favor of :set nohlsearch?
        commands.add(["noh[lsearch]"],
            "Remove the search highlighting",
            function () { finder.clear(); },
            { argCount: "0" });
    },
    mappings: function () {
        var myModes = config.browserModes;
        myModes = myModes.concat([modes.CARET]);

        mappings.add(myModes,
            ["/"], "Search forward for a pattern",
            function () { finder.openPrompt(modes.SEARCH_FORWARD); });

        mappings.add(myModes,
            ["?"], "Search backwards for a pattern",
            function () { finder.openPrompt(modes.SEARCH_BACKWARD); });

        mappings.add(myModes,
            ["n"], "Find next",
            function () { finder.findAgain(false); });

        mappings.add(myModes,
            ["N"], "Find previous",
            function () { finder.findAgain(true); });

        mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
            "Find word under cursor",
            function () {
                this._found = false;
                finder.onSubmit(buffer.getCurrentWord(), false);
            });

        mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
            "Find word under cursor backwards",
            function () {
                this._found = false;
                finder.onSubmit(buffer.getCurrentWord(), true);
            });
    },
    options: function () {
        options.add(["hlsearch", "hls"],
            "Highlight previous search pattern matches",
            "boolean", true, {
                setter: function (value) {
                    try {
                        if (value)
                            finder.highlight();
                        else
                            finder.clear();
                    }
                    catch (e) {}

                    return value;
                }
            });

        options.add(["ignorecase", "ic"],
            "Ignore case in search patterns",
            "boolean", true);

        options.add(["incsearch", "is"],
            "Show where the search pattern matches as it is typed",
            "boolean", true);

        options.add(["linksearch", "lks"],
            "Limit the search to hyperlink text",
            "boolean", false);

        options.add(["smartcase", "scs"],
            "Override the 'ignorecase' option if the pattern contains uppercase characters",
            "boolean", true);
    }
});

// vim: set fdm=marker sw=4 ts=4 et:
<commit_msg>fix: broken finder with Firefox 24.*(>24.0.0)<commit_after>// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the License.txt file included with this file.

/** @scope modules */

// TODO: proper backwards search - implement our own component?
//     : implement our own highlighter?
//     : <ESC> should cancel search highlighting in 'incsearch' mode and jump
//       back to the presearch page location - can probably use the same
//       solution as marks
//     : 'linksearch' searches should highlight link matches only
//     : changing any search settings should also update the search state including highlighting
//     : incremental searches shouldn't permanently update search modifiers
//
// TODO: Clean up this rat's nest. --Kris

/**
 * @instance finder
 */
const Finder = Module("finder", {
    requires: ["config"],

    init: function () {
        const self = this;

        this._found = false;               // true if the last search was successful
        this._backwards = false;           // currently searching backwards
        this._searchString = "";           // current search string (without modifiers)
        this._searchPattern = "";          // current search string (includes modifiers)
        this._lastSearchPattern = "";      // the last searched pattern (includes modifiers)
        this._lastSearchString = "";       // the last searched string (without modifiers)
        this._lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
        this._caseSensitive = false;       // search string is case sensitive
        this._linksOnly = false;           // search is limited to link text only
    },

    // set searchString, searchPattern, caseSensitive, linksOnly
    _processUserPattern: function (pattern) {
        //// strip off pattern terminator and offset
        //if (backwards)
        //    pattern = pattern.replace(/\?.*/, "");
        //else
        //    pattern = pattern.replace(/\/.*/, "");

        this._searchPattern = pattern;

        // links only search - \l wins if both modifiers specified
        if (/\\l/.test(pattern))
            this._linksOnly = true;
        else if (/\L/.test(pattern))
            this._linksOnly = false;
        else if (options["linksearch"])
            this._linksOnly = true;
        else
            this._linksOnly = false;

        // strip links-only modifiers
        pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // case sensitivity - \c wins if both modifiers specified
        if (/\c/.test(pattern))
            this._caseSensitive = false;
        else if (/\C/.test(pattern))
            this._caseSensitive = true;
        else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern))
            this._caseSensitive = true;
        else if (options["ignorecase"])
            this._caseSensitive = false;
        else
            this._caseSensitive = true;

        // strip case-sensitive modifiers
        pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // remove any modifier escape \
        pattern = pattern.replace(/\\(\\[cClL])/g, "$1");

        this._searchString = pattern;
    },

    /**
     * Called when the search dialog is requested.
     *
     * @param {number} mode The search mode, either modes.SEARCH_FORWARD or
     *     modes.SEARCH_BACKWARD.
     * @default modes.SEARCH_FORWARD
     */
    openPrompt: function (mode) {
        this._backwards = mode == modes.SEARCH_BACKWARD;
        //commandline.open(this._backwards ? "Find backwards" : "Find", "", mode);
        commandline.input(this._backwards ? "Find backwards" : "Find", this.closure.onSubmit, {
            onChange: function() { if (options["incsearch"]) finder.find(commandline.command) }

        });
        //modes.extended = mode;

        // TODO: focus the top of the currently visible screen
    },

    // TODO: backwards seems impossible i fear
    /**
     * Searches the current buffer for str.
     *
     * @param {string} str The string to find.
     * @see Bug537013 https://bugzilla.mozilla.org/show_bug.cgi?id=537013
     */
    find: Services.vc.compare(Services.appinfo.version, "25.0") >= 0 ?
    function (str) {
        let fastFind = config.browser.getFindBar();
        this._processUserPattern(str);
        fastFind._typeAheadCaseSensitive = this._caseSensitive;
        fastFind._typeAheadLinksOnly = this._linksOnly;
        let result = fastFind._find(str);
    } :
    // FIXME: remove when minVersion >= 25
    function (str) {
        let fastFind = config.browser.fastFind;

        this._processUserPattern(str);
        fastFind.caseSensitive = this._caseSensitive;
        let result = fastFind.find(this._searchString, this._linksOnly);
        this._displayFindResult(result, this._backwards);
    },

    /**
     * Searches the current buffer again for the most recently used search
     * string.
     *
     * @param {boolean} reverse Whether to search forwards or backwards.
     * @default false
     * @see Bug537013 https://bugzilla.mozilla.org/show_bug.cgi?id=537013
     */
    findAgain: Services.vc.compare(Services.appinfo.version, "24.0") > 0 ?
    function (reverse) {
        let fastFind = config.browser.getFindBar();
        if (fastFind._findField.value != this._lastSearchString)
            this.find(this._lastSearchString);

        let backwards = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards;
        fastFind._typeAheadLinksOnly = this._linksOnly;
        let result = fastFind._findAgain(backwards);
        this._displayFindResult(result, backwards);
    } :
    // FIXME: remove when minVersion > 24
    function (reverse) {
        // This hack is needed to make n/N work with the correct string, if
        // we typed /foo<esc> after the original search.  Since searchString is
        // readonly we have to call find() again to update it.
        if (config.browser.fastFind.searchString != this._lastSearchString)
            this.find(this._lastSearchString);

        let backwards = reverse ? !this._lastSearchBackwards : this._lastSearchBackwards;
        let result = config.browser.fastFind.findAgain(backwards, this._linksOnly);
        this._displayFindResult(result, backwards);

    },

    _displayFindResult: function(result, backwards) {
        if (result == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
            liberator.echoerr("Pattern not found: " + this._searchString, commandline.FORCE_SINGLELINE);
        }
        else if (result == Ci.nsITypeAheadFind.FIND_WRAPPED) {
            let msg = backwards ? "Search hit TOP, continuing at BOTTOM" : "Search hit BOTTOM, continuing at TOP";
            commandline.echo(msg, commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES | commandline.FORCE_SINGLELINE);
        }
        else {
            liberator.echomsg("Found pattern: " + this._searchString);
        }
    },

    /**
     * Called when the user types a key in the search dialog. Triggers a
     * search attempt if 'incsearch' is set.
     *
     * @param {string} str The search string.
     */
    /*onKeyPress: function (str) {
        if (options["incsearch"])
            this.find(str);
        },*/

    /**
     * Called when the <Enter> key is pressed to trigger a search.
     *
     * @param {string} str The search string.
     * @param {boolean} forcedBackward Whether to search forwards or
     *     backwards. This overrides the direction set in
     *     (@link #openPrompt).
     * @default false
     */
    onSubmit: function (str, forcedBackward) {
        if (typeof forcedBackward === "boolean")
            this._backwards = forcedBackward;

        if (str)
            var pattern = str;
        else {
            liberator.assert(this._lastSearchPattern, "No previous search pattern");
            pattern = this._lastSearchPattern;
        }

        this.clear();

        // liberator.log('inc: ' + options["incsearch"] + ' sea:' + this._searchPattern + ' pat:' + pattern);
        if (!options["incsearch"] /*|| !str || !this._found */|| this._searchPattern != pattern) {
            // prevent any current match from matching again
            if (!window.content.getSelection().isCollapsed)
                window.content.getSelection().getRangeAt(0).collapse(this._backwards);

            this.find(pattern);
        }

        // focus links after searching, so the user can just hit <Enter> another time to follow the link
        // This has to be done async, because the mode reset after onSubmit would
        // clear the focus
        let elem = Services.vc.compare(Services.appinfo.version, "24.0") > 0 ?
                    config.browser.getFindBar()._foundLinkRef.get() :
                    config.browser.fastFind.foundLink; // FIXME: remove when minVersion > 24
        this.setTimeout(function() {
            if (elem)
                elem.focus();
                // fm.moveFocus(elem.ownerDocument.defaultView, null, Ci.nsIFocusManager.MOVEFOCUS_CARET, Ci.nsIFocusManager.FLAG_NOSCROLL);*/
        }, 0);

        this._lastSearchBackwards = this._backwards;
        this._lastSearchPattern = pattern;
        this._lastSearchString = this._searchString;

        // TODO: move to find() when reverse incremental searching is kludged in
        // need to find again for reverse searching
        if (this._backwards)
            this.setTimeout(function () { this.findAgain(false); }, 0);

        if (options["hlsearch"])
            this.highlight(this._searchString);
    },

    /**
     * Highlights all occurances of str in the buffer.
     *
     * @param {string} str The string to highlight.
     */
    highlight: function (str) {
        // FIXME: Thunderbird incompatible
        if (config.name == "Muttator")
            return;

        if (window.gFindBar) {
            window.gFindBar._setCaseSensitivity(this._caseSensitive);
            window.gFindBar._highlightDoc(false);
            window.gFindBar._highlightDoc(true, str);
        }
    },

    /**
     * Clears all search highlighting.
     */
    clear: function () {
        // FIXME: Thunderbird incompatible
        if (config.name == "Muttator")
            return;

        if (window.gFindBar)
            window.gFindBar._highlightDoc(false);
    }
}, {
}, {
    commands: function () {
        // TODO: Remove in favor of :set nohlsearch?
        commands.add(["noh[lsearch]"],
            "Remove the search highlighting",
            function () { finder.clear(); },
            { argCount: "0" });
    },
    mappings: function () {
        var myModes = config.browserModes;
        myModes = myModes.concat([modes.CARET]);

        mappings.add(myModes,
            ["/"], "Search forward for a pattern",
            function () { finder.openPrompt(modes.SEARCH_FORWARD); });

        mappings.add(myModes,
            ["?"], "Search backwards for a pattern",
            function () { finder.openPrompt(modes.SEARCH_BACKWARD); });

        mappings.add(myModes,
            ["n"], "Find next",
            function () { finder.findAgain(false); });

        mappings.add(myModes,
            ["N"], "Find previous",
            function () { finder.findAgain(true); });

        mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
            "Find word under cursor",
            function () {
                this._found = false;
                finder.onSubmit(buffer.getCurrentWord(), false);
            });

        mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
            "Find word under cursor backwards",
            function () {
                this._found = false;
                finder.onSubmit(buffer.getCurrentWord(), true);
            });
    },
    options: function () {
        options.add(["hlsearch", "hls"],
            "Highlight previous search pattern matches",
            "boolean", true, {
                setter: function (value) {
                    try {
                        if (value)
                            finder.highlight();
                        else
                            finder.clear();
                    }
                    catch (e) {}

                    return value;
                }
            });

        options.add(["ignorecase", "ic"],
            "Ignore case in search patterns",
            "boolean", true);

        options.add(["incsearch", "is"],
            "Show where the search pattern matches as it is typed",
            "boolean", true);

        options.add(["linksearch", "lks"],
            "Limit the search to hyperlink text",
            "boolean", false);

        options.add(["smartcase", "scs"],
            "Override the 'ignorecase' option if the pattern contains uppercase characters",
            "boolean", true);
    }
});

// vim: set fdm=marker sw=4 ts=4 et:
<|endoftext|>