<commit_before>"use strict";

module.exports = function(Chart) {

	var helpers = Chart.helpers;

	Chart.defaults.doughnut = {
		animation: {
			//Boolean - Whether we animate the rotation of the Doughnut
			animateRotate: true,
			//Boolean - Whether we animate scaling the Doughnut from the centre
			animateScale: false
		},
		aspectRatio: 1,
		hover: {
			mode: 'single'
		},
		legendCallback: function(chart) {
			var text = [];
			text.push('
');

			if (chart.data.datasets.length) {
				for (var i = 0; i < chart.data.datasets[0].data.length; ++i) {
					text.push('
');
					if (chart.data.labels[i]) {
						text.push(chart.data.labels[i]);
					}
					text.push('
');
				}
			}

			text.push('
');
			return text.join("");
		},
		legend: {
			labels: {
				generateLabels: function(data) {
					if (data.labels.length && data.datasets.length) {

						return data.labels.map(function(label, i) {
							var ds = data.datasets[0];
							var arc = ds.metaData[i];
							var fill = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(ds.backgroundColor, i, this.chart.options.elements.arc.backgroundColor);
							var stroke = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(ds.borderColor, i, this.chart.options.elements.arc.borderColor);
							var bw = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(ds.borderWidth, i, this.chart.options.elements.arc.borderWidth);

							return {
								text: label,
								fillStyle: fill,
								strokeStyle: stroke,
								lineWidth: bw,
								hidden: isNaN(data.datasets[0].data[i]),

								// Extra data used for toggling the correct item
								index: i
							};
						}, this);
					} else {
						return [];
					}
				}
			},
			onClick: function(e, legendItem) {
				helpers.each(this.chart.data.datasets, function(dataset) {
					dataset.metaHiddenData = dataset.metaHiddenData || [];
					var idx = legendItem.index;

					if (!isNaN(dataset.data[idx])) {
						dataset.metaHiddenData[idx] = dataset.data[idx];
						dataset.data[idx] = NaN;
					} else if (!isNaN(dataset.metaHiddenData[idx])) {
						dataset.data[idx] = dataset.metaHiddenData[idx];
					}
				});

				this.chart.update();
			}
		},

		//The percentage of the chart that we cut out of the middle.
		cutoutPercentage: 50,

		//The rotation of the chart, where the first data arc begins.
		rotation: Math.PI * -0.5,

		//The total circumference of the chart.
		circumference: Math.PI * 2.0,

		// Need to override these to give a nice default
		tooltips: {
			callbacks: {
				title: function() {
					return '';
				},
				label: function(tooltipItem, data) {
					return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
				}
			}
		}
	};

	Chart.defaults.pie = helpers.clone(Chart.defaults.doughnut);
	helpers.extend(Chart.defaults.pie, {
		cutoutPercentage: 0
	});

	Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({
		linkScales: function() {
			// no scales for doughnut
		},

		addElements: function() {
			this.getDataset().metaData = this.getDataset().metaData || [];
			helpers.each(this.getDataset().data, function(value, index) {
				this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Arc({
					_chart: this.chart.chart,
					_datasetIndex: this.index,
					_index: index
				});
			}, this);
		},
		addElementAndReset: function(index, colorForNewElement) {
			this.getDataset().metaData = this.getDataset().metaData || [];
			var arc = new Chart.elements.Arc({
				_chart: this.chart.chart,
				_datasetIndex: this.index,
				_index: index
			});

			if (colorForNewElement && helpers.isArray(this.getDataset().backgroundColor)) {
				this.getDataset().backgroundColor.splice(index, 0, colorForNewElement);
			}

			// Reset the point
			this.updateElement(arc, index, true);

			// Add to the points array
			this.getDataset().metaData.splice(index, 0, arc);
		},

		getVisibleDatasetCount: function getVisibleDatasetCount() {
			return helpers.where(this.chart.data.datasets, function(ds) {
				return helpers.isDatasetVisible(ds);
			}).length;
		},

		// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
		getRingIndex: function getRingIndex(datasetIndex) {
			var ringIndex = 0;

			for (var j = 0; j < datasetIndex; ++j) {
				if (helpers.isDatasetVisible(this.chart.data.datasets[j])) {
					++ringIndex;
				}
			}

			return ringIndex;
		},

		update: function update(reset) {
			var availableWidth = this.chart.chartArea.right - this.chart.chartArea.left - this.chart.options.elements.arc.borderWidth;
			var availableHeight = this.chart.chartArea.bottom - this.chart.chartArea.top - this.chart.options.elements.arc.borderWidth;
			var minSize = Math.min(availableWidth, availableHeight);
			var offset = {x: 0, y: 0};

			// If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc
			if (this.chart.options.circumference < Math.PI * 2.0) {
				var startAngle = this.chart.options.rotation % (Math.PI * 2.0);
				startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0);
				var endAngle = startAngle + this.chart.options.circumference;
				var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)};
				var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)};
				var contains0 = (startAngle <= 0 && 0 <= endAngle) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle);
				var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle);
				var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle);
				var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle);
				var cutout = this.chart.options.cutoutPercentage / 100.0;
				var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))};
				var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))};
				var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5};
				minSize = Math.min(availableWidth / size.width, availableHeight / size.height);
				offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5};
			}

			this.chart.outerRadius = Math.max(minSize / 2, 0);
			this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0);
			this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.getVisibleDatasetCount();
			this.chart.offsetX = offset.x * this.chart.outerRadius;
			this.chart.offsetY = offset.y * this.chart.outerRadius;

			this.getDataset().total = 0;
			helpers.each(this.getDataset().data, function(value) {
				if (!isNaN(value)) {
					this.getDataset().total += Math.abs(value);
				}
			}, this);

			this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.getRingIndex(this.index));
			this.innerRadius = this.outerRadius - this.chart.radiusLength;

			helpers.each(this.getDataset().metaData, function(arc, index) {
				this.updateElement(arc, index, reset);
			}, this);
		},
		updateElement: function(arc, index, reset) {
			var centerX = (this.chart.chartArea.left + this.chart.chartArea.right) / 2;
			var centerY = (this.chart.chartArea.top + this.chart.chartArea.bottom) / 2;
			var startAngle = this.chart.options.rotation; // non reset case handled later
			var endAngle = this.chart.options.rotation; // non reset case handled later
			var circumference = reset && this.chart.options.animation.animateRotate ? 0 : this.calculateCircumference(this.getDataset().data[index]) * (this.chart.options.circumference / (2.0 * Math.PI));
			var innerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.innerRadius;
			var outerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.outerRadius;

			helpers.extend(arc, {
				// Utility
				_chart: this.chart.chart,
				_datasetIndex: this.index,
				_index: index,

				// Desired view properties
				_model: {
					x: centerX + this.chart.offsetX,
					y: centerY + this.chart.offsetY,
					startAngle: startAngle,
					endAngle: endAngle,
					circumference: circumference,
					outerRadius: outerRadius,
					innerRadius: innerRadius,

					backgroundColor: arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor),
					hoverBackgroundColor: arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().hoverBackgroundColor, index, this.chart.options.elements.arc.hoverBackgroundColor),
					borderWidth: arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth),
					borderColor: arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor),

					label: helpers.getValueAtIndexOrDefault(this.getDataset().label, index, this.chart.data.labels[index])
				}
			});

			// Set correct angles if not resetting
			if (!reset) {

				if (index === 0) {
					arc._model.startAngle = this.chart.options.rotation;
				} else {
					arc._model.startAngle = this.getDataset().metaData[index - 1]._model.endAngle;
				}

				arc._model.endAngle = arc._model.startAngle + arc._model.circumference;
			}

			arc.pivot();
		},

		draw: function(ease) {
			var easingDecimal = ease || 1;
			helpers.each(this.getDataset().metaData, function(arc, index) {
				arc.transition(easingDecimal).draw();
			});
		},

		setHoverStyle: function(arc) {
			var dataset = this.chart.data.datasets[arc._datasetIndex];
			var index = arc._index;

			arc._model.backgroundColor = arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(arc._model.backgroundColor).saturate(0.5).darken(0.1).rgbString());
			arc._model.borderColor = arc.custom && arc.custom.hoverBorderColor ? arc.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(arc._model.borderColor).saturate(0.5).darken(0.1).rgbString());
			arc._model.borderWidth = arc.custom && arc.custom.hoverBorderWidth ? arc.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, arc._model.borderWidth);
		},

		removeHoverStyle: function(arc) {
			var dataset = this.chart.data.datasets[arc._datasetIndex];
			var index = arc._index;

			arc._model.backgroundColor = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor);
			arc._model.borderColor = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor);
			arc._model.borderWidth = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth);
		},

		calculateCircumference: function(value) {
			if (this.getDataset().total > 0 && !isNaN(value)) {
				return (Math.PI * 2.0) * (value / this.getDataset().total);
			} else {
				return 0;
			}
		}
	});
};<commit_msg>Fixed closing span position<commit_after>"use strict";

module.exports = function(Chart) {

	var helpers = Chart.helpers;

	Chart.defaults.doughnut = {
		animation: {
			//Boolean - Whether we animate the rotation of the Doughnut
			animateRotate: true,
			//Boolean - Whether we animate scaling the Doughnut from the centre
			animateScale: false
		},
		aspectRatio: 1,
		hover: {
			mode: 'single'
		},
		legendCallback: function(chart) {
			var text = [];
			text.push('
');

			if (chart.data.datasets.length) {
				for (var i = 0; i < chart.data.datasets[0].data.length; ++i) {
					text.push('
');
					if (chart.data.labels[i]) {
						text.push(chart.data.labels[i]);
					}
					text.push('
');
				}
			}

			text.push('
');
			return text.join("");
		},
		legend: {
			labels: {
				generateLabels: function(data) {
					if (data.labels.length && data.datasets.length) {

						return data.labels.map(function(label, i) {
							var ds = data.datasets[0];
							var arc = ds.metaData[i];
							var fill = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(ds.backgroundColor, i, this.chart.options.elements.arc.backgroundColor);
							var stroke = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(ds.borderColor, i, this.chart.options.elements.arc.borderColor);
							var bw = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(ds.borderWidth, i, this.chart.options.elements.arc.borderWidth);

							return {
								text: label,
								fillStyle: fill,
								strokeStyle: stroke,
								lineWidth: bw,
								hidden: isNaN(data.datasets[0].data[i]),

								// Extra data used for toggling the correct item
								index: i
							};
						}, this);
					} else {
						return [];
					}
				}
			},
			onClick: function(e, legendItem) {
				helpers.each(this.chart.data.datasets, function(dataset) {
					dataset.metaHiddenData = dataset.metaHiddenData || [];
					var idx = legendItem.index;

					if (!isNaN(dataset.data[idx])) {
						dataset.metaHiddenData[idx] = dataset.data[idx];
						dataset.data[idx] = NaN;
					} else if (!isNaN(dataset.metaHiddenData[idx])) {
						dataset.data[idx] = dataset.metaHiddenData[idx];
					}
				});

				this.chart.update();
			}
		},

		//The percentage of the chart that we cut out of the middle.
		cutoutPercentage: 50,

		//The rotation of the chart, where the first data arc begins.
		rotation: Math.PI * -0.5,

		//The total circumference of the chart.
		circumference: Math.PI * 2.0,

		// Need to override these to give a nice default
		tooltips: {
			callbacks: {
				title: function() {
					return '';
				},
				label: function(tooltipItem, data) {
					return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
				}
			}
		}
	};

	Chart.defaults.pie = helpers.clone(Chart.defaults.doughnut);
	helpers.extend(Chart.defaults.pie, {
		cutoutPercentage: 0
	});

	Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({
		linkScales: function() {
			// no scales for doughnut
		},

		addElements: function() {
			this.getDataset().metaData = this.getDataset().metaData || [];
			helpers.each(this.getDataset().data, function(value, index) {
				this.getDataset().metaData[index] = this.getDataset().metaData[index] || new Chart.elements.Arc({
					_chart: this.chart.chart,
					_datasetIndex: this.index,
					_index: index
				});
			}, this);
		},
		addElementAndReset: function(index, colorForNewElement) {
			this.getDataset().metaData = this.getDataset().metaData || [];
			var arc = new Chart.elements.Arc({
				_chart: this.chart.chart,
				_datasetIndex: this.index,
				_index: index
			});

			if (colorForNewElement && helpers.isArray(this.getDataset().backgroundColor)) {
				this.getDataset().backgroundColor.splice(index, 0, colorForNewElement);
			}

			// Reset the point
			this.updateElement(arc, index, true);

			// Add to the points array
			this.getDataset().metaData.splice(index, 0, arc);
		},

		getVisibleDatasetCount: function getVisibleDatasetCount() {
			return helpers.where(this.chart.data.datasets, function(ds) {
				return helpers.isDatasetVisible(ds);
			}).length;
		},

		// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
		getRingIndex: function getRingIndex(datasetIndex) {
			var ringIndex = 0;

			for (var j = 0; j < datasetIndex; ++j) {
				if (helpers.isDatasetVisible(this.chart.data.datasets[j])) {
					++ringIndex;
				}
			}

			return ringIndex;
		},

		update: function update(reset) {
			var availableWidth = this.chart.chartArea.right - this.chart.chartArea.left - this.chart.options.elements.arc.borderWidth;
			var availableHeight = this.chart.chartArea.bottom - this.chart.chartArea.top - this.chart.options.elements.arc.borderWidth;
			var minSize = Math.min(availableWidth, availableHeight);
			var offset = {x: 0, y: 0};

			// If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc
			if (this.chart.options.circumference < Math.PI * 2.0) {
				var startAngle = this.chart.options.rotation % (Math.PI * 2.0);
				startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0);
				var endAngle = startAngle + this.chart.options.circumference;
				var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)};
				var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)};
				var contains0 = (startAngle <= 0 && 0 <= endAngle) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle);
				var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle);
				var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle);
				var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle);
				var cutout = this.chart.options.cutoutPercentage / 100.0;
				var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))};
				var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))};
				var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5};
				minSize = Math.min(availableWidth / size.width, availableHeight / size.height);
				offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5};
			}

			this.chart.outerRadius = Math.max(minSize / 2, 0);
			this.chart.innerRadius = Math.max(this.chart.options.cutoutPercentage ? (this.chart.outerRadius / 100) * (this.chart.options.cutoutPercentage) : 1, 0);
			this.chart.radiusLength = (this.chart.outerRadius - this.chart.innerRadius) / this.getVisibleDatasetCount();
			this.chart.offsetX = offset.x * this.chart.outerRadius;
			this.chart.offsetY = offset.y * this.chart.outerRadius;

			this.getDataset().total = 0;
			helpers.each(this.getDataset().data, function(value) {
				if (!isNaN(value)) {
					this.getDataset().total += Math.abs(value);
				}
			}, this);

			this.outerRadius = this.chart.outerRadius - (this.chart.radiusLength * this.getRingIndex(this.index));
			this.innerRadius = this.outerRadius - this.chart.radiusLength;

			helpers.each(this.getDataset().metaData, function(arc, index) {
				this.updateElement(arc, index, reset);
			}, this);
		},
		updateElement: function(arc, index, reset) {
			var centerX = (this.chart.chartArea.left + this.chart.chartArea.right) / 2;
			var centerY = (this.chart.chartArea.top + this.chart.chartArea.bottom) / 2;
			var startAngle = this.chart.options.rotation; // non reset case handled later
			var endAngle = this.chart.options.rotation; // non reset case handled later
			var circumference = reset && this.chart.options.animation.animateRotate ? 0 : this.calculateCircumference(this.getDataset().data[index]) * (this.chart.options.circumference / (2.0 * Math.PI));
			var innerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.innerRadius;
			var outerRadius = reset && this.chart.options.animation.animateScale ? 0 : this.outerRadius;

			helpers.extend(arc, {
				// Utility
				_chart: this.chart.chart,
				_datasetIndex: this.index,
				_index: index,

				// Desired view properties
				_model: {
					x: centerX + this.chart.offsetX,
					y: centerY + this.chart.offsetY,
					startAngle: startAngle,
					endAngle: endAngle,
					circumference: circumference,
					outerRadius: outerRadius,
					innerRadius: innerRadius,

					backgroundColor: arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor),
					hoverBackgroundColor: arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().hoverBackgroundColor, index, this.chart.options.elements.arc.hoverBackgroundColor),
					borderWidth: arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth),
					borderColor: arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor),

					label: helpers.getValueAtIndexOrDefault(this.getDataset().label, index, this.chart.data.labels[index])
				}
			});

			// Set correct angles if not resetting
			if (!reset) {

				if (index === 0) {
					arc._model.startAngle = this.chart.options.rotation;
				} else {
					arc._model.startAngle = this.getDataset().metaData[index - 1]._model.endAngle;
				}

				arc._model.endAngle = arc._model.startAngle + arc._model.circumference;
			}

			arc.pivot();
		},

		draw: function(ease) {
			var easingDecimal = ease || 1;
			helpers.each(this.getDataset().metaData, function(arc, index) {
				arc.transition(easingDecimal).draw();
			});
		},

		setHoverStyle: function(arc) {
			var dataset = this.chart.data.datasets[arc._datasetIndex];
			var index = arc._index;

			arc._model.backgroundColor = arc.custom && arc.custom.hoverBackgroundColor ? arc.custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.color(arc._model.backgroundColor).saturate(0.5).darken(0.1).rgbString());
			arc._model.borderColor = arc.custom && arc.custom.hoverBorderColor ? arc.custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.color(arc._model.borderColor).saturate(0.5).darken(0.1).rgbString());
			arc._model.borderWidth = arc.custom && arc.custom.hoverBorderWidth ? arc.custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, arc._model.borderWidth);
		},

		removeHoverStyle: function(arc) {
			var dataset = this.chart.data.datasets[arc._datasetIndex];
			var index = arc._index;

			arc._model.backgroundColor = arc.custom && arc.custom.backgroundColor ? arc.custom.backgroundColor : helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.arc.backgroundColor);
			arc._model.borderColor = arc.custom && arc.custom.borderColor ? arc.custom.borderColor : helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.arc.borderColor);
			arc._model.borderWidth = arc.custom && arc.custom.borderWidth ? arc.custom.borderWidth : helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.arc.borderWidth);
		},

		calculateCircumference: function(value) {
			if (this.getDataset().total > 0 && !isNaN(value)) {
				return (Math.PI * 2.0) * (value / this.getDataset().total);
			} else {
				return 0;
			}
		}
	});
};
<|endoftext|>


<commit_before>define(["angular-ui-router"], function() {

	// Declare app level module which depends on filters, and services
	angular.module('isaac.router', [
        'ui.router',
	])

	.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {

        $urlRouterProvider.when("", "/");
        $urlRouterProvider.otherwise(function($injector, $location) {
            return "/not_found?target=" + $location.url();
        });

        var genericPageState = function(url, id) {
            return {
                url: url,
                resolve: {
                    "page": ["api", function(api) {
                        return api.pages.get({id: id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/generic_page.html",
                        controller: "GenericPageController",
                    }
                }
            };
        }

        var staticPageState = function(url, state, controller) {
            return {
                url: url,
                views: {
                    "body": {
                        templateUrl: "/partials/states/" + state + ".html",
                        controller: controller
                    },
                },
            }
        }

        $stateProvider
            .state('home', staticPageState("/", "home", "HomePageController"))
            .state('about', genericPageState("/about", "about_us_index"))
            .state('events', genericPageState("/events", "events_index"))
            .state('glossary', genericPageState("/glossary", "glossary"))
            .state('cookies', genericPageState("/cookies", "cookie_policy"))
            .state('apply_uni', genericPageState("/apply_uni", "apply_uni"))
            .state('solving_problems', genericPageState("/solving_problems", "solving_problems"))
            .state('extraordinary_problems', genericPageState("/extraordinary_problems", "extraordinary_problems_index"))
	        .state('challenge_problems', genericPageState("/challenge_problems", "challenge_problems_index"))
            .state('bios', genericPageState("/bios", "bios"))
            .state('why_physics', genericPageState("/why_physics", "why_physics"))
	        .state('privacy', genericPageState("/privacy", "privacy_policy"))
            //.state('login', staticPageState("/users/login", "login_page"))

	        .state('contact', {
		        url: "/contact",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/contact.html",
				        controller: "ContactController",
			        }
		        }
	        })

            .state('conceptIndex', {
                url: "/concepts?page",
                resolve: {
                    "conceptList" : ['api', function(api){
                        return api.getConceptList().$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/concept_index.html",
                        controller: "ConceptIndexController",
                    }
                }
            })
            .state('questionIndex', {
                url: "/questions?page",
                resolve: {
                    "pageIndex" :['$stateParams', function($stateParams){
                        return parseInt($stateParams.page || "1") - 1;
                    }],
                    "list" : ['api', 'pageIndex', function(api, pageIndex){
                        return api.getQuestionList(pageIndex).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/question_index.html",
                        controller: "QuestionIndexController",
                    }
                }
            })

            .state('concept', {
                url: "/concepts/:id",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.conceptPages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/concept.html",
                        controller: "ConceptPageController",
                    }
                }
            })

            .state('question', {
                url: "/questions/:id?board",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.questionPages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/question.html",
                        controller: "QuestionPageController",
                    }
                }
            })
            .state('pages', {
                url: "/pages/:id",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.pages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/generic_page.html",
                        controller: ["$scope", "page", function($scope, page) {
                            $scope.title = "Content object: " + page.id;
                            $scope.doc = page;
                        }]
                    }
                }
            })
            .state('contentErrors', {
                url: "/admin/content_errors",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.contentProblems.get().$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/content_error.html",
                        controller: "ContentErrorController",
                    }
                }
            })
            .state('login', {
                url: "/login?target",
                views: {
                    "body": {
                        templateUrl: "/partials/states/login_page.html",
                        controller: "LoginPageController",
                    }
                }
            })
	        .state('resetPassword', {
		        url: "/resetpassword/:token",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/reset_password.html",
				        controller: "ResetPasswordPageController",
			        }
		        }
	        })
            .state('boards', {
                url: "/boards",
                resolve: {
                    requireLogin: "authResolver",
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/my_boards.html",
                        controller: "MyBoardsPageController",
                    }
                }
            })
            .state('board', {
                url: "/board/:id",
                onEnter: ["$stateParams", "$location", function($stateParams, $location) {
                    $location.url("/#" + $stateParams.id);
                    throw "Prevent entering board redirect state."
                }],
            })
            .state('searchResults', {
                url: "/search?query&types&page",
                resolve: {
                    "query" :['$stateParams', function($stateParams){
                        return $stateParams.query;
                    }],

                    "types" :['$stateParams', function($stateParams){
                        if ($stateParams.types != null && $stateParams.types.length > 0) {
                            return $stateParams.types.split(",");
                        } else {
                            return [];
                        }
                    }],

                    "pageIndex" :['$stateParams', function($stateParams){
                        return parseInt($stateParams.page || "1") - 1;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/search_results.html",
                        controller: "SearchController",
                    }
                },
                reloadOnSearch: false,
            })
            .state('logout', {
                url: "/logout",
                resolve: {
                    done: ["auth", function(auth) {
                        window.foo = auth.logout();
                        return window.foo;
                    }],
                },
                onEnter: ["$state", function($state) {
                    document.location.href = "/";
                }]
            })
            .state('shareLink', {
                url: "/s/:shortCode",
                onEnter: ["$stateParams", "api", function($stateParams, api) {
                    var redirectURL = "http://goo.gl/" + $stateParams.shortCode;
                    var doRedirect = function(){
                        document.location.href = redirectURL;
                    }

                    api.logger.log({
                        type : "USE_SHARE_LINK",
                        shortURL : redirectURL,
                    }).$promise.then(function(){
                        doRedirect();
                    }).catch(function(){
                        doRedirect();
                    })
                }]
            })

            .state('404', {
                url: "/not_found?target",
                views: {
                    "body": {
                        templateUrl: "/partials/states/404.html",
                        controller: function($scope, $stateParams) {
                            $scope.target = $stateParams.target;
                        }
                    },
                },
            })

	        .state('accountSettings', {
		        url: "/account?next",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/account_settings.html",
				        controller: "AccountSettingsPageController",
			        }
		        }
	        })

	        .state('register', {
		        url: "/register",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/register.html",
				        controller: "AccountSettingsPageController",
			        }
		        }
	        })

            .state('authCallback', {
                url: "/auth/:provider/callback",
                onEnter: ["$stateParams", "$location", "auth", function($stateParams, $location, auth) {
                    console.debug("Auth callback from", $stateParams.provider, "with params:", $location.search());

                    auth.providerCallback($stateParams.provider, $location.search());
                }]
            })

            .state('authError', {
                url: "/auth_error?errorMessage&statusText",
                views: {
                    "body": {
                        templateUrl: "/partials/states/auth_error.html",
                        controller: "AuthErrorPageController",
                    }
                }
            })
	}])

    .run(['$rootScope', '$state', function($rootScope, $state) {
        $rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
            console.warn("State change error:", error);

            if (error == "require_login")
                $state.go('login', {target: $state.href(toState, toParams)});

            if (error.status == 404)
                $state.go('404', {target: $state.href(toState, toParams)});
        });

    }])

})<commit_msg>Accept password-reset tokens containing the "/" character.<commit_after>define(["angular-ui-router"], function() {

	// Declare app level module which depends on filters, and services
	angular.module('isaac.router', [
        'ui.router',
	])

	.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {

        $urlRouterProvider.when("", "/");
        $urlRouterProvider.otherwise(function($injector, $location) {
            return "/not_found?target=" + $location.url();
        });

        var genericPageState = function(url, id) {
            return {
                url: url,
                resolve: {
                    "page": ["api", function(api) {
                        return api.pages.get({id: id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/generic_page.html",
                        controller: "GenericPageController",
                    }
                }
            };
        }

        var staticPageState = function(url, state, controller) {
            return {
                url: url,
                views: {
                    "body": {
                        templateUrl: "/partials/states/" + state + ".html",
                        controller: controller
                    },
                },
            }
        }

        $stateProvider
            .state('home', staticPageState("/", "home", "HomePageController"))
            .state('about', genericPageState("/about", "about_us_index"))
            .state('events', genericPageState("/events", "events_index"))
            .state('glossary', genericPageState("/glossary", "glossary"))
            .state('cookies', genericPageState("/cookies", "cookie_policy"))
            .state('apply_uni', genericPageState("/apply_uni", "apply_uni"))
            .state('solving_problems', genericPageState("/solving_problems", "solving_problems"))
            .state('extraordinary_problems', genericPageState("/extraordinary_problems", "extraordinary_problems_index"))
	        .state('challenge_problems', genericPageState("/challenge_problems", "challenge_problems_index"))
            .state('bios', genericPageState("/bios", "bios"))
            .state('why_physics', genericPageState("/why_physics", "why_physics"))
	        .state('privacy', genericPageState("/privacy", "privacy_policy"))
            //.state('login', staticPageState("/users/login", "login_page"))

	        .state('contact', {
		        url: "/contact",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/contact.html",
				        controller: "ContactController",
			        }
		        }
	        })

            .state('conceptIndex', {
                url: "/concepts?page",
                resolve: {
                    "conceptList" : ['api', function(api){
                        return api.getConceptList().$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/concept_index.html",
                        controller: "ConceptIndexController",
                    }
                }
            })
            .state('questionIndex', {
                url: "/questions?page",
                resolve: {
                    "pageIndex" :['$stateParams', function($stateParams){
                        return parseInt($stateParams.page || "1") - 1;
                    }],
                    "list" : ['api', 'pageIndex', function(api, pageIndex){
                        return api.getQuestionList(pageIndex).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/question_index.html",
                        controller: "QuestionIndexController",
                    }
                }
            })

            .state('concept', {
                url: "/concepts/:id",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.conceptPages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/concept.html",
                        controller: "ConceptPageController",
                    }
                }
            })

            .state('question', {
                url: "/questions/:id?board",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.questionPages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/question.html",
                        controller: "QuestionPageController",
                    }
                }
            })
            .state('pages', {
                url: "/pages/:id",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.pages.get({id: $stateParams.id}).$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/generic_page.html",
                        controller: ["$scope", "page", function($scope, page) {
                            $scope.title = "Content object: " + page.id;
                            $scope.doc = page;
                        }]
                    }
                }
            })
            .state('contentErrors', {
                url: "/admin/content_errors",
                resolve: {
                    "page": ["api", "$stateParams", function(api, $stateParams) {
                        return api.contentProblems.get().$promise;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/content_error.html",
                        controller: "ContentErrorController",
                    }
                }
            })
            .state('login', {
                url: "/login?target",
                views: {
                    "body": {
                        templateUrl: "/partials/states/login_page.html",
                        controller: "LoginPageController",
                    }
                }
            })
	        .state('resetPassword', {
		        url: "/resetpassword/*token",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/reset_password.html",
				        controller: "ResetPasswordPageController",
			        }
		        }
	        })
            .state('boards', {
                url: "/boards",
                resolve: {
                    requireLogin: "authResolver",
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/my_boards.html",
                        controller: "MyBoardsPageController",
                    }
                }
            })
            .state('board', {
                url: "/board/:id",
                onEnter: ["$stateParams", "$location", function($stateParams, $location) {
                    $location.url("/#" + $stateParams.id);
                    throw "Prevent entering board redirect state."
                }],
            })
            .state('searchResults', {
                url: "/search?query&types&page",
                resolve: {
                    "query" :['$stateParams', function($stateParams){
                        return $stateParams.query;
                    }],

                    "types" :['$stateParams', function($stateParams){
                        if ($stateParams.types != null && $stateParams.types.length > 0) {
                            return $stateParams.types.split(",");
                        } else {
                            return [];
                        }
                    }],

                    "pageIndex" :['$stateParams', function($stateParams){
                        return parseInt($stateParams.page || "1") - 1;
                    }]
                },
                views: {
                    "body": {
                        templateUrl: "/partials/states/search_results.html",
                        controller: "SearchController",
                    }
                },
                reloadOnSearch: false,
            })
            .state('logout', {
                url: "/logout",
                resolve: {
                    done: ["auth", function(auth) {
                        window.foo = auth.logout();
                        return window.foo;
                    }],
                },
                onEnter: ["$state", function($state) {
                    document.location.href = "/";
                }]
            })
            .state('shareLink', {
                url: "/s/:shortCode",
                onEnter: ["$stateParams", "api", function($stateParams, api) {
                    var redirectURL = "http://goo.gl/" + $stateParams.shortCode;
                    var doRedirect = function(){
                        document.location.href = redirectURL;
                    }

                    api.logger.log({
                        type : "USE_SHARE_LINK",
                        shortURL : redirectURL,
                    }).$promise.then(function(){
                        doRedirect();
                    }).catch(function(){
                        doRedirect();
                    })
                }]
            })

            .state('404', {
                url: "/not_found?target",
                views: {
                    "body": {
                        templateUrl: "/partials/states/404.html",
                        controller: function($scope, $stateParams) {
                            $scope.target = $stateParams.target;
                        }
                    },
                },
            })

	        .state('accountSettings', {
		        url: "/account?next",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/account_settings.html",
				        controller: "AccountSettingsPageController",
			        }
		        }
	        })

	        .state('register', {
		        url: "/register",
		        views: {
			        "body": {
				        templateUrl: "/partials/states/register.html",
				        controller: "AccountSettingsPageController",
			        }
		        }
	        })

            .state('authCallback', {
                url: "/auth/:provider/callback",
                onEnter: ["$stateParams", "$location", "auth", function($stateParams, $location, auth) {
                    console.debug("Auth callback from", $stateParams.provider, "with params:", $location.search());

                    auth.providerCallback($stateParams.provider, $location.search());
                }]
            })

            .state('authError', {
                url: "/auth_error?errorMessage&statusText",
                views: {
                    "body": {
                        templateUrl: "/partials/states/auth_error.html",
                        controller: "AuthErrorPageController",
                    }
                }
            })
	}])

    .run(['$rootScope', '$state', function($rootScope, $state) {
        $rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
            console.warn("State change error:", error);

            if (error == "require_login")
                $state.go('login', {target: $state.href(toState, toParams)});

            if (error.status == 404)
                $state.go('404', {target: $state.href(toState, toParams)});
        });

    }])

})<|endoftext|>


<commit_before>angular.module('bauhaus.page.controllers', ['bauhaus.page.services']);

angular.module('bauhaus.page.controllers').controller('PageCtrl', ['$scope', '$routeParams', '$location', 'Page', 'SharedPageType', 'SharedContentType', 'Content', 'PageContent', 'SharedPageTree', 'Slug', function ($scope, $routeParams, $location, Page, SharedPageType, SharedContentType, Content, PageContent, SharedPageTree, Slug) {
    'use strict';

    /* reference to page tree in service */
    $scope.tree            = SharedPageTree.tree;

    /* reference to object of pageTypes configurations in service */
    $scope.pageTypes       = SharedPageType.store;
    /* Contains the configuration of the current page type, is updated when page._type is updated */
    $scope.currentPageType = null;

    /* reference to object of contentType configuations in service */
    $scope.contentTypes    = SharedContentType.store;
    /* Contains name content type selected by user to add, uses first in list as default */
    $scope.newContentType  = null;

    /* current page object */
    $scope.page           = {};
    /* bool which is updated by content watcher, if true page was updated by user */
    $scope.pageHasChanges = false;

    /* array of slots, which contain arrays of content objects */
    $scope.slots             = [];
    /* object that contains all content element, which were updated
       by user in shape hasContentChanges[ _id ] = [slotIndex, index] */
    $scope.contentChanges    = {};

    /* If activated route is automatically generated from title */
    $scope.generateRoute   = false;

    /* Set current page path variable tree service, automatically laods page via watcher */
    if ($routeParams.id) {
        $scope.tree.current.pageId = $routeParams.id;
    }

    /********************
     ** PAGE ************
     ********************/

    /* Helper: Sets current page to given id, used by UI to change current page */
    $scope.changePage = function (id) {
        $location.path('page/' + id);
    };

    /* Helper */
    $scope.contentHasChanges = function () {
        return (Object.keys($scope.contentChanges).length > 0) ? true : false;
    };

    /* Watcher: Load page from server if currentPageId is changed */
    $scope.$watch('tree.current.pageId', function (newVal, oldVal) {
        if (newVal) {
            /* Load new page */
            $scope.page = Page.get({ pageId: newVal }, function (result) {
                $scope.tree.expand(result);
                $scope.pageHasChanges = false;
                $scope.showPageLabel = ($scope.page.label && $scope.page.label.length != null && $scope.page.label.length > 0);
                /* Activate route generation for new pages */
                $scope.generateRoute = ($scope.page.title === '' &&
                                        !$scope.page.public &&
                                        $scope.page.route.length >= 1 &&
                                        $scope.page.route[ $scope.page.route.length - 1 ] === '/') ? true : false;
            });
            // Load contents from server and assign to slots when ready
            $scope.content = PageContent.get({pageId: newVal}, function () {
                // reset slots
                $scope.slots = [];

                // add slots if unexistend
                if ($scope.currentPageType) {
                    while ($scope.currentPageType.slots.length > $scope.slots.length) {
                        $scope.slots.push([]);
                    }
                }
                for (var c in $scope.content) {
                    var contentElement = $scope.content[c];
                    if (contentElement.meta) {
                        // get slot and position index from content element
                        var slot = contentElement.meta.slot;
                        var position = contentElement.meta.position;
                        if (! Array.isArray( $scope.slots[slot] ) ) {
                            $scope.slots[slot] = [];
                        }
                        $scope.slots[slot][position] = contentElement;

                        $scope.watchContent(slot, position);
                    }
                }
            });
        }
    });

    /* Watcher: cache parent route if routeGeneration is activated */
    $scope.$watch('generateRoute', function (newVal) {
        if (newVal === true) {
            $scope.parentRoute = $scope.page.route;
            if ($scope.parentRoute.length >= 1 && $scope.parentRoute[ $scope.parentRoute.length - 1 ] !== '/') {
                /* Add slash to parent route if it doesn't end with one */
                $scope.parentRoute += '/';
            }
        }
    });

    /* Watcher: Slugify title to route if route generation is activated */
    $scope.$watch('page.title', function (newVal) {
        if ($scope.generateRoute) {
            $scope.page.route = $scope.parentRoute + Slug.slugify(newVal);
        }
    });

    /* Watcher: Deactivate route generation if user had changed route */
    $scope.$watch('page.route', function (newVal) {
        if ($scope.generateRoute) {
            if (newVal !== $scope.parentRoute + Slug.slugify($scope.page.title)) {
                $scope.generateRoute = false;
            }
        }
    });

    /* Watcher: Update page update flag, if user updated page */
    $scope.$watch('page', function (newVal, oldVal) {
        if (newVal && newVal._id && oldVal && oldVal._id) {
            $scope.pageHasChanges = true;
        }
    }, true);

    /* Watcher: Change currentPageType if type is changed by user */
    $scope.$watch('page._type', function (newVal) {
        if ($scope.pageTypes.all[newVal]) {
            $scope.currentPageType = $scope.pageTypes.all[newVal];
            // set default tab
            $scope.tab = 'slot:' + $scope.currentPageType.slots[0].name;
            $scope.currentSlot = 0;
            // add slots if unexistend
            while ($scope.currentPageType.slots.length > $scope.slots.length) {
                $scope.slots.push([]);
            }
        }
    });

    /* Set first content type as current */
    $scope.$watch('contentTypes.all', function (newVal, oldVal) {
        for (var type in newVal) {
            $scope.newContentType = type;
            break;
        }
    }, true);

    /** Sends currently viewed page and updated content to server and update tree, to avoid tree reload **/
    $scope.updatePage = function (page) {
        if ($scope.pageHasChanges) {
            Page.put(page, function (result) {
                if (result.path != null) {
                    // Choose corrosponding page in tree to update without reload
                    var pageInTree = $scope.tree.getByPath(result.path, result._id);
                    pageInTree.title = $scope.page.title;
                    pageInTree.route = $scope.page.route;
                    pageInTree.public = $scope.page.public;
                }
                // set route to valid
                $scope.pageEditor.route.$setValidity('routeExists', true);
                $scope.pageHasChanges = false;
            }, function (error) {
                if (error && error.status === 422 && error.data && error.data.route) {
                    $scope.invalidRoute = true;
                    // set route to invalid
                    $scope.pageEditor.route.$setValidity('routeExists', false);
                    console.log($scope.pageEditor);
                    alert('This route does already exist, please choose a different route');
                }
                console.log("error", error);
            });
        }

        $scope.updateContents();
    };

    /* UI: Deletes page at rest service */
    $scope.deletePage = function (page) {
        var pageInTree = $scope.tree.getByPath(page.path, page._id);

        if (pageInTree.children && Object.keys(pageInTree.children).length > 0) {
            alert('You cannot delete this page because it has sub page. Please delete this pages first.');
            return;
        }
        var del = confirm('Do you really want to delete this page?');

        var deletePage = function () {
            Page.delete({pageId: page._id }, function (result) {
                $scope.tree.deleteByPath(page.path, page._id);
                var parentId = $scope.tree.getParentId(page);
                $scope.changePage(parentId);
            });
        };

        if (del) {

            // Iterate over all content elements and delete them before deleting page
            var remainingContentElements = 0;

            /* Delete page directly, if it has no slots */
            if ($scope.slots.length === 0) deletePage();

            for (var s in $scope.slots) {
                var slot = $scope.slots[s];
                remainingContentElements += slot.length;
                for (var c in slot) {
                    var content = slot[c];
                    Content.delete({}, {'_id': content._id}, function (result) {
                        remainingContentElements--;
                        if (remainingContentElements === 0) {
                            // delete page if all content elements were deleted
                            deletePage();
                        }
                    });
                }

                if (remainingContentElements === 0 && $scope.slots.length - 1 == s) {
                    /* delete page if there are no content elements in last slot */
                    deletePage();
                }
            }
        }
    };

    /********************
     ** CONTENT *********
     ********************/

    /* Sends all updated content elements to server */
    $scope.updateContents = function () {
        // iterate over all content elements, which were updated
        var contentToUpdate = Object.keys($scope.contentChanges).length;

        var contentUpdated = 0;
        for (var id in $scope.contentChanges) {
            var position = $scope.contentChanges[ id ];
            var content = $scope.slots[position[0]][position[1]];

            Content.put(content, function (result) {
                // remove content from list of content blocks which should be updated
                contentUpdated++;
                // Count if all content elements to update have been updated, if finished clear list
                if (contentUpdated >= contentToUpdate) {
                    $scope.contentChanges = {};
                }
            });
        }
    };

    /* Register watcher for changes in single content element */
    $scope.watchContent = function (slot, position) {
        $scope.$watch(function () { return $scope.slots[slot][position] }, function (newVal, oldVal) {
            if (newVal && JSON.stringify(oldVal) !== JSON.stringify(newVal) ) {
                var coordinates = [slot, position];
                if ($scope.contentChanges[ newVal._id ] == null) {
                    // add content to list of changed content
                    $scope.contentChanges[ newVal._id ] = [slot, position];
                } else {
                    // update coordinates of content in list of changed content
                    $scope.contentChanges[ newVal._id ] = [slot, position];
                }
            }
        }, true);
    };

    /* UI: Create content element of selected type at server and add to current slot */
    $scope.createContent = function () {
        var slot = $scope.currentSlot;
        var position = $scope.slots[slot].length;
        var content = {
            _page: $scope.tree.current.pageId,
            _type: $scope.newContentType,
            meta: {
                slot: slot,
                position: position
            },
            content: {}
        };
        Content.create(content, function (result) {
            $scope.slots[ slot ].push(result);
            $scope.watchContent(slot, position);
        });
    };

    /* UI: Moves content element up in slot */
    $scope.moveContentUp = function (content) {
        var slot = content.meta.slot,
            oldIndex = content.meta.position,
            newIndex = oldIndex - 1;
        // change positions in content array
        $scope.moveContent(slot, oldIndex, newIndex);
    };

    /* UI: Moves content element down in slot */
    $scope.moveContentDown = function (content) {
        var slot = content.meta.slot,
            oldIndex = content.meta.position,
            newIndex = oldIndex + 1;
        // change positions in content array
        $scope.moveContent(slot, oldIndex, newIndex);
    };

    /* UI: Delete content from server after confirm and update positions of other elements */
    $scope.deleteContent = function (content) {
        var ok = confirm('Do you really want to delete this content element?');
        if (ok) {
            var position = content.meta.position,
                slot = content.meta.slot;
            Content.delete({}, {'_id': content._id}, function (result) {
                $scope.removeContent(slot, position);
                var unregWatcher = $scope.$watch('slots[' + slot + ']', function (newVal) {
                    $scope.updateContents();
                    unregWatcher();
                });

            });
        }
    };

    /* Helper: Removes content element from scope and update positions of other elements */
    $scope.removeContent = function (slotId, position) {
        var slot = $scope.slots[slotId];
        slot.splice(position, 1);

        for (var c in slot) {
            var element = slot[c];
            if (element.meta.position !== c) {
                element.meta.position = c;
            }
        };
    };

    /* Helper: Changes position of of content element in scope */
    $scope.moveContent = function (slotId, oldIndex, newIndex) {
        var slot = $scope.slots[slotId];
        while (oldIndex < 0) {
            oldIndex += slot.length;
        }
        while (newIndex < 0) {
            newIndex += slot.length;
        }
        if (newIndex >= slot.length) {
            var k = new_Index - slot.length;
            while ((k--) + 1) {
                this.push(undefined);
            }
        }
        // change position of moved element
        var content = $scope.slots[slotId][oldIndex];
        content.meta.position = newIndex;
        // change position of element which is moved by other element moving up or down
        var content2 = $scope.slots[slotId][newIndex];
        content2.meta.position = oldIndex;
        slot.splice(newIndex, 0, slot.splice(oldIndex, 1)[0]);
    };
}]);<commit_msg>Replace populated content data by mongo ids before save<commit_after>angular.module('bauhaus.page.controllers', ['bauhaus.page.services']);

angular.module('bauhaus.page.controllers').controller('PageCtrl', ['$scope', '$routeParams', '$location', 'Page', 'SharedPageType', 'SharedContentType', 'Content', 'PageContent', 'SharedPageTree', 'Slug', function ($scope, $routeParams, $location, Page, SharedPageType, SharedContentType, Content, PageContent, SharedPageTree, Slug) {
    'use strict';

    /* reference to page tree in service */
    $scope.tree            = SharedPageTree.tree;

    /* reference to object of pageTypes configurations in service */
    $scope.pageTypes       = SharedPageType.store;
    /* Contains the configuration of the current page type, is updated when page._type is updated */
    $scope.currentPageType = null;

    /* reference to object of contentType configuations in service */
    $scope.contentTypes    = SharedContentType.store;
    /* Contains name content type selected by user to add, uses first in list as default */
    $scope.newContentType  = null;

    /* current page object */
    $scope.page           = {};
    /* bool which is updated by content watcher, if true page was updated by user */
    $scope.pageHasChanges = false;

    /* array of slots, which contain arrays of content objects */
    $scope.slots             = [];
    /* object that contains all content element, which were updated
       by user in shape hasContentChanges[ _id ] = [slotIndex, index] */
    $scope.contentChanges    = {};

    /* If activated route is automatically generated from title */
    $scope.generateRoute   = false;

    /* Set current page path variable tree service, automatically laods page via watcher */
    if ($routeParams.id) {
        $scope.tree.current.pageId = $routeParams.id;
    }

    /********************
     ** PAGE ************
     ********************/

    /* Helper: Sets current page to given id, used by UI to change current page */
    $scope.changePage = function (id) {
        $location.path('page/' + id);
    };

    /* Helper */
    $scope.contentHasChanges = function () {
        return (Object.keys($scope.contentChanges).length > 0) ? true : false;
    };

    /* Watcher: Load page from server if currentPageId is changed */
    $scope.$watch('tree.current.pageId', function (newVal, oldVal) {
        if (newVal) {
            /* Load new page */
            $scope.page = Page.get({ pageId: newVal }, function (result) {
                $scope.tree.expand(result);
                $scope.pageHasChanges = false;
                $scope.showPageLabel = ($scope.page.label && $scope.page.label.length != null && $scope.page.label.length > 0);
                /* Activate route generation for new pages */
                $scope.generateRoute = ($scope.page.title === '' &&
                                        !$scope.page.public &&
                                        $scope.page.route.length >= 1 &&
                                        $scope.page.route[ $scope.page.route.length - 1 ] === '/') ? true : false;
            });
            // Load contents from server and assign to slots when ready
            $scope.content = PageContent.get({pageId: newVal}, function () {
                // reset slots
                $scope.slots = [];

                // add slots if unexistend
                if ($scope.currentPageType) {
                    while ($scope.currentPageType.slots.length > $scope.slots.length) {
                        $scope.slots.push([]);
                    }
                }
                for (var c in $scope.content) {
                    var contentElement = $scope.content[c];
                    if (contentElement.meta) {
                        // get slot and position index from content element
                        var slot = contentElement.meta.slot;
                        var position = contentElement.meta.position;
                        if (! Array.isArray( $scope.slots[slot] ) ) {
                            $scope.slots[slot] = [];
                        }
                        $scope.slots[slot][position] = contentElement;

                        $scope.watchContent(slot, position);
                    }
                }
            });
        }
    });

    /* Watcher: cache parent route if routeGeneration is activated */
    $scope.$watch('generateRoute', function (newVal) {
        if (newVal === true) {
            $scope.parentRoute = $scope.page.route;
            if ($scope.parentRoute.length >= 1 && $scope.parentRoute[ $scope.parentRoute.length - 1 ] !== '/') {
                /* Add slash to parent route if it doesn't end with one */
                $scope.parentRoute += '/';
            }
        }
    });

    /* Watcher: Slugify title to route if route generation is activated */
    $scope.$watch('page.title', function (newVal) {
        if ($scope.generateRoute) {
            $scope.page.route = $scope.parentRoute + Slug.slugify(newVal);
        }
    });

    /* Watcher: Deactivate route generation if user had changed route */
    $scope.$watch('page.route', function (newVal) {
        if ($scope.generateRoute) {
            if (newVal !== $scope.parentRoute + Slug.slugify($scope.page.title)) {
                $scope.generateRoute = false;
            }
        }
    });

    /* Watcher: Update page update flag, if user updated page */
    $scope.$watch('page', function (newVal, oldVal) {
        if (newVal && newVal._id && oldVal && oldVal._id) {
            $scope.pageHasChanges = true;
        }
    }, true);

    /* Watcher: Change currentPageType if type is changed by user */
    $scope.$watch('page._type', function (newVal) {
        if ($scope.pageTypes.all[newVal]) {
            $scope.currentPageType = $scope.pageTypes.all[newVal];
            // set default tab
            $scope.tab = 'slot:' + $scope.currentPageType.slots[0].name;
            $scope.currentSlot = 0;
            // add slots if unexistend
            while ($scope.currentPageType.slots.length > $scope.slots.length) {
                $scope.slots.push([]);
            }
        }
    });

    /* Set first content type as current */
    $scope.$watch('contentTypes.all', function (newVal, oldVal) {
        for (var type in newVal) {
            $scope.newContentType = type;
            break;
        }
    }, true);

    /** Sends currently viewed page and updated content to server and update tree, to avoid tree reload **/
    $scope.updatePage = function (page) {
        if ($scope.pageHasChanges) {
            Page.put(page, function (result) {
                if (result.path != null) {
                    // Choose corrosponding page in tree to update without reload
                    var pageInTree = $scope.tree.getByPath(result.path, result._id);
                    pageInTree.title = $scope.page.title;
                    pageInTree.route = $scope.page.route;
                    pageInTree.public = $scope.page.public;
                }
                // set route to valid
                $scope.pageEditor.route.$setValidity('routeExists', true);
                $scope.pageHasChanges = false;
            }, function (error) {
                if (error && error.status === 422 && error.data && error.data.route) {
                    $scope.invalidRoute = true;
                    // set route to invalid
                    $scope.pageEditor.route.$setValidity('routeExists', false);
                    console.log($scope.pageEditor);
                    alert('This route does already exist, please choose a different route');
                }
                console.log("error", error);
            });
        }

        $scope.updateContents();
    };

    /* UI: Deletes page at rest service */
    $scope.deletePage = function (page) {
        var pageInTree = $scope.tree.getByPath(page.path, page._id);

        if (pageInTree.children && Object.keys(pageInTree.children).length > 0) {
            alert('You cannot delete this page because it has sub page. Please delete this pages first.');
            return;
        }
        var del = confirm('Do you really want to delete this page?');

        var deletePage = function () {
            Page.delete({pageId: page._id }, function (result) {
                $scope.tree.deleteByPath(page.path, page._id);
                var parentId = $scope.tree.getParentId(page);
                $scope.changePage(parentId);
            });
        };

        if (del) {

            // Iterate over all content elements and delete them before deleting page
            var remainingContentElements = 0;

            /* Delete page directly, if it has no slots */
            if ($scope.slots.length === 0) deletePage();

            for (var s in $scope.slots) {
                var slot = $scope.slots[s];
                remainingContentElements += slot.length;
                for (var c in slot) {
                    var content = slot[c];
                    Content.delete({}, {'_id': content._id}, function (result) {
                        remainingContentElements--;
                        if (remainingContentElements === 0) {
                            // delete page if all content elements were deleted
                            deletePage();
                        }
                    });
                }

                if (remainingContentElements === 0 && $scope.slots.length - 1 == s) {
                    /* delete page if there are no content elements in last slot */
                    deletePage();
                }
            }
        }
    };

    /********************
     ** CONTENT *********
     ********************/

     /* Replaces all objects with a field _id by a string of the id */
    function unpopulate (object) {
        var obj = angular.copy(object);
        for (var f in obj) {
            if (obj.hasOwnProperty(f)) {
                var field = obj[f];
                // replace field with object by id
                if (typeof field === 'object' && field._id) {
                    obj[f] = field._id;
                }
                // replace field with array of objects by array of ids
                if (Array.isArray(field) &&
                    field.length > 0 &&
                    typeof field[0] === 'object' &&
                    field[0]._id) {

                    var ids = [];
                    for (var o in field) {
                        if (field[o]._id) {
                            ids.push(field[o]._id);
                        }
                    }
                    obj[f] = ids;
                }
            }
        }
        return obj;
    }

    /* Sends all updated content elements to server */
    $scope.updateContents = function () {
        // iterate over all content elements, which were updated
        var contentToUpdate = Object.keys($scope.contentChanges).length;

        var contentUpdated = 0;
        for (var id in $scope.contentChanges) {
            var position = $scope.contentChanges[ id ];
            var content = angular.copy($scope.slots[position[0]][position[1]]);
            content.content = unpopulate(content.content);

            Content.put(content, function (result) {
                // remove content from list of content blocks which should be updated
                contentUpdated++;
                // Count if all content elements to update have been updated, if finished clear list
                if (contentUpdated >= contentToUpdate) {
                    $scope.contentChanges = {};
                }
            });
        }
    };

    /* Register watcher for changes in single content element */
    $scope.watchContent = function (slot, position) {
        $scope.$watch(function () { return $scope.slots[slot][position] }, function (newVal, oldVal) {
            if (newVal && JSON.stringify(oldVal) !== JSON.stringify(newVal) ) {
                var coordinates = [slot, position];
                if ($scope.contentChanges[ newVal._id ] == null) {
                    // add content to list of changed content
                    $scope.contentChanges[ newVal._id ] = [slot, position];
                } else {
                    // update coordinates of content in list of changed content
                    $scope.contentChanges[ newVal._id ] = [slot, position];
                }
            }
        }, true);
    };

    /* UI: Create content element of selected type at server and add to current slot */
    $scope.createContent = function () {
        var slot = $scope.currentSlot;
        var position = $scope.slots[slot].length;
        var content = {
            _page: $scope.tree.current.pageId,
            _type: $scope.newContentType,
            meta: {
                slot: slot,
                position: position
            },
            content: {}
        };
        Content.create(content, function (result) {
            $scope.slots[ slot ].push(result);
            $scope.watchContent(slot, position);
        });
    };

    /* UI: Moves content element up in slot */
    $scope.moveContentUp = function (content) {
        var slot = content.meta.slot,
            oldIndex = content.meta.position,
            newIndex = oldIndex - 1;
        // change positions in content array
        $scope.moveContent(slot, oldIndex, newIndex);
    };

    /* UI: Moves content element down in slot */
    $scope.moveContentDown = function (content) {
        var slot = content.meta.slot,
            oldIndex = content.meta.position,
            newIndex = oldIndex + 1;
        // change positions in content array
        $scope.moveContent(slot, oldIndex, newIndex);
    };

    /* UI: Delete content from server after confirm and update positions of other elements */
    $scope.deleteContent = function (content) {
        var ok = confirm('Do you really want to delete this content element?');
        if (ok) {
            var position = content.meta.position,
                slot = content.meta.slot;
            Content.delete({}, {'_id': content._id}, function (result) {
                $scope.removeContent(slot, position);
                var unregWatcher = $scope.$watch('slots[' + slot + ']', function (newVal) {
                    $scope.updateContents();
                    unregWatcher();
                });

            });
        }
    };

    /* Helper: Removes content element from scope and update positions of other elements */
    $scope.removeContent = function (slotId, position) {
        var slot = $scope.slots[slotId];
        slot.splice(position, 1);

        for (var c in slot) {
            var element = slot[c];
            if (element.meta.position !== c) {
                element.meta.position = c;
            }
        };
    };

    /* Helper: Changes position of of content element in scope */
    $scope.moveContent = function (slotId, oldIndex, newIndex) {
        var slot = $scope.slots[slotId];
        while (oldIndex < 0) {
            oldIndex += slot.length;
        }
        while (newIndex < 0) {
            newIndex += slot.length;
        }
        if (newIndex >= slot.length) {
            var k = new_Index - slot.length;
            while ((k--) + 1) {
                this.push(undefined);
            }
        }
        // change position of moved element
        var content = $scope.slots[slotId][oldIndex];
        content.meta.position = newIndex;
        // change position of element which is moved by other element moving up or down
        var content2 = $scope.slots[slotId][newIndex];
        content2.meta.position = oldIndex;
        slot.splice(newIndex, 0, slot.splice(oldIndex, 1)[0]);
    };
}]);<|endoftext|>


<commit_before>import View from 'core/view/view';
import Command from 'core/infrastructure/command';
import Shortcut from 'core/infrastructure/shortcut';
import Navigation from 'core/navigation/navigation';
import {GRID_PREFIX} from 'core/definition';

export default class NavigationView extends View {
	constructor(model, markup, apply) {
		super(model);
		this.markup = markup;
		this.document = this.markup.document;
		this.newRow = 0;
		this.newColumn = 0;
		this.oldRow = 0;
		this.oldColumn = 0;
		const shortcut = new Shortcut(this.document, apply);
		const navigation = new Navigation(model);
		shortcut.register('navigation', navigation.commands);

		this.blur = new Command({
			execute: (row, column) => {
				this.rows[row].cells[column].classList.remove(`${GRID_PREFIX}-focus`);
			},
			canExecute: () => {
				return this.rows.length > this.newRow
					&& this.rows[this.newRow].cells.length > this.newColumn;
			}
		});
		this.focus = new Command({
			execute: (row, column) => {
				const cell = this.rows[row].cells[column];
				cell.classList.add(`${GRID_PREFIX}-focus`);
			},
			canExecute: () => {
				return this.rows.length > this.newRow
					&& this.rows[this.newRow].cells.length > this.newColumn;
			}
		});
		this.scrollTo = new Command({
			execute: (row, column) => {
				const rows = this.rows;
				const cell = rows[row.new].cells[column.new];
				const firstCell = rows[0].cells[0];
				const body = markup.body;
				const diff = firstCell.offsetTop - body.offsetTop;
				const scroll = model.layout().scroll;
				const bottom = (row.new > row.old)
					&& (cell.offsetTop + cell.offsetHeight - diff > body.offsetTop + body.offsetHeight + scroll.top);
				const top = (row.new < row.old)
					&& (cell.offsetTop + cell.offsetHeight < body.offsetHeight + scroll.top);
				const right = (column.new > column.old)
					&& (cell.offsetLeft + cell.offsetWidth > body.offsetLeft + body.offsetWidth + scroll.left);
				const left = (column.new < column.old)
					&& (cell.offsetLeft + cell.offsetWidth < body.offsetLeft + body.offsetWidth + scroll.left);
				if (bottom || top || right || left) {
					cell.scrollIntoView();
				}
			}
		});

		model.navigationChanged.watch(e => {
			const nav = model.navigation();
			this.newRow = nav.row == -1 ? 0 : nav.row;
			this.newColumn = nav.column == -1 ? 0 : nav.column;
			this.oldRow = e && e.changes.hasOwnProperty('row') ? e.changes.row.oldValue : this.newRow;
			this.oldColumn = e && e.changes.hasOwnProperty('column') ? e.changes.column.oldValue : this.newColumn;

			if (this.blur.canExecute() && this.oldRow > -1 && this.oldColumn > -1) {
				this.blur.execute(this.oldRow, this.oldColumn);
			}
			if (this.focus.canExecute()) {
				this.focus.execute(this.newRow, this.newColumn);
				this.scrollTo.execute({new: this.newRow, old: this.oldRow}, {new: this.newColumn, old: this.oldColumn});
			}
		});

		model.viewChanged.watch(() => {
			model.navigation({
				column: -1,
				row: -1
			});
		});
	}

	get rows() {
		return this.markup.body.rows;
	}

}<commit_msg>isVisible<commit_after>import View from 'core/view/view';
import Command from 'core/infrastructure/command';
import Shortcut from 'core/infrastructure/shortcut';
import Navigation from 'core/navigation/navigation';
import {GRID_PREFIX} from 'core/definition';

export default class NavigationView extends View {
	constructor(model, markup, apply) {
		super(model);
		this.markup = markup;
		this.document = this.markup.document;
		this.newRow = 0;
		this.newColumn = 0;
		this.oldRow = 0;
		this.oldColumn = 0;
		const shortcut = new Shortcut(this.document, apply);
		const navigation = new Navigation(model);
		shortcut.register('navigation', navigation.commands);

		this.blur = new Command({
			execute: (row, column) => {
				this.rows[row].cells[column].classList.remove(`${GRID_PREFIX}-focus`);
			},
			canExecute: () => {
				return this.rows.length > this.newRow
					&& this.rows[this.newRow].cells.length > this.newColumn;
			}
		});
		this.focus = new Command({
			execute: (row, column) => {
				const cell = this.rows[row].cells[column];
				cell.classList.add(`${GRID_PREFIX}-focus`);
			},
			canExecute: () => {
				return this.rows.length > this.newRow
					&& this.rows[this.newRow].cells.length > this.newColumn;
			}
		});
		this.scrollTo = new Command({
			execute: (row, column) => {
				const rows = this.rows;
				const cell = rows[row].cells[column];
				console.log(this.isVisible(cell, markup.body));
			}
		});

		model.navigationChanged.watch(e => {
			const nav = model.navigation();
			this.newRow = nav.row == -1 ? 0 : nav.row;
			this.newColumn = nav.column == -1 ? 0 : nav.column;
			this.oldRow = e && e.changes.hasOwnProperty('row') ? e.changes.row.oldValue : this.newRow;
			this.oldColumn = e && e.changes.hasOwnProperty('column') ? e.changes.column.oldValue : this.newColumn;

			if (this.blur.canExecute() && this.oldRow > -1 && this.oldColumn > -1) {
				this.blur.execute(this.oldRow, this.oldColumn);
			}
			if (this.focus.canExecute()) {
				this.focus.execute(this.newRow, this.newColumn);
				this.scrollTo.execute(this.newRow, this.newColumn);
			}
		});

		model.viewChanged.watch(() => {
			model.navigation({
				column: -1,
				row: -1
			});
		});
	}

	get rows() {
		return this.markup.body.rows;
	}

	isVisible(inner, outer) {
		inner = inner.getBoundingClientRect();
		outer = outer.getBoundingClientRect();
		console.log(inner, outer);
		return (inner.top <= outer.top) && (outer.top <= inner.bottom)
			&& (inner.top <= outer.bottom) && (outer.bottom <= inner.bottom)
			&& (inner.left <= outer.left) && (outer.left <= inner.left + inner.width)
			&& (inner.left <= outer.left + outer.width) && (outer.right <= inner.left + inner.width)
	}

}<|endoftext|>