<commit_before>/**
 * ownCloud - ocsms
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Loic Blot <loic.blot@unix-experience.fr>
 * @copyright Loic Blot 2014-2015
 */

// Some global vars to improve performances
var selectedConversation = null;
var curPhoneNumber = null;
var curContactName = '';
var lastMsgDate = 0;
var unreadCountCurrentConv = 0;
var unreadCountAllConv = 0;
var unreadCountNotifStep = 12;
var lastUnreadCountAllConv = 0;
var originalTitle = document.title;

$.urlParam = function(name){
	var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
	if (results == null) {
		return null;
	}
	else {
		return results[1] || 0;
	}
};

var refreshConversation = function() {
	// if no conversation selected, then don't fetch page
	if (curPhoneNumber == null) {
		if ($('#app-content-header').is(':visible')) {
			$('#app-content-header').hide();
		}
		return;
	}
	$.getJSON(OC.generateUrl('/apps/ocsms/get/conversation'),
		{
			'phoneNumber': curPhoneNumber,
			"lastDate": lastMsgDate
		},
		function(jsondata, status) {
			var fmt = formatConversation(jsondata);
			conversationBuf = fmt[1];
			if (conversationBuf != '') {
				$('.msg-endtag').before(conversationBuf);
				$('#app-content').scrollTop(1E10);
				// This will blink the tab because there is new messages
				if (document.hasFocus() == false) {
					unreadCountCurrentConv += fmt[0];
					document.title = originalTitle + " (" + unreadCountCurrentConv + ")";
					desktopNotify(unreadCountCurrentConv + " unread message(s) in conversation with " + curContactName);
				}
			}

			setMessageCountInfo(jsondata);

			if ($('#app-content-header').is(':hidden')) {
				$('#app-content-header').show();
			}
		}
	);
};

var checkNewMessages = function() {
	unreadCountAllConv = 0;
	$.getJSON(OC.generateUrl('/apps/ocsms/get/new_messages'),
		{ 'lastDate': lastMsgDate },
		function(jsondata, status) {
			var peerListBuf = '';
			var bufferedContacts = [];

			$.each(jsondata['phonelist'], function(id, val) {
				var fn, peerLabel, idxVal;
				idxVal = id.replace(/\//g,' ');
				idxVal2 = idxVal.replace('/ /g','');
				if (typeof jsondata['contacts'][id] == 'undefined') {
					fn = '';
					peerLabel = idxVal;
				}
				else {
					fn = jsondata['contacts'][id];
					peerLabel = fn;
				}

				if ($.inArray(peerLabel, bufferedContacts) == -1) {
					$("div[mailbox-avatar='" + peerLabel + "']").remove();
					$("a[mailbox-label='" + peerLabel + "']").remove();
					peerListBuf += '

' + peerLabel + ' (' + val + ')
';
					$('#app-mailbox-peers ul').prepend(peerListBuf);
					bufferedContacts.push(peerLabel);

					// Re-set conversation because we reload the element
					if (idxVal == curPhoneNumber) {
						changeSelectedConversation($("a[mailbox-navigation='" + idxVal + "']"));
					}

					unreadCountAllConv += parseInt(val);

					// Now bind the events when we click on the phone number
					$("a[mailbox-navigation='" + idxVal + "']").on('click', function (event) {
						var phoneNumber = $(this).attr('mailbox-navigation');
						OC.Util.History.pushState('phonenumber=' + phoneNumber);

						// phoneNumber must exist
						if (phoneNumber != null) {
							fetchConversation(phoneNumber);
								unreadCountAllConv += val;changeSelectedConversation($(this));
						}
						event.preventDefault();
					});
				}
			});

			/*
			* Decrement notification step counter, but stop it a zero
			* Stop at zero permit to notify instanly the user when
			* there is new messages in all conversations
			*/

			if (unreadCountNotifStep > 0) {
				unreadCountNotifStep--;
			}

			if (unreadCountAllConv > 0) {
				/*
				* We notify user every two minutes for all messages
				* or if unreadCount changes
				*/
				if (unreadCountNotifStep == 0 || lastUnreadCountAllConv != unreadCountAllConv) {
					desktopNotify(unreadCountAllConv + " unread message(s) for all conversations");
					unreadCountNotifStep = 12;
					lastUnreadCountAllConv = unreadCountAllConv;
				}
			}
		}
	);
};

function setMessageCountInfo(jsondata) {
	if (typeof jsondata['msgCount'] != 'undefined') {
		if (jsondata['msgCount'] == 1) {
			$('#ocsms-phone-msg-nb').html(jsondata['msgCount'] + ' message');
		}
		else {
			$('#ocsms-phone-msg-nb').html(jsondata['msgCount'] + ' messages');
		}
	}
	else {
		$('#ocsms-phone-msg-nb').html('');
	}
}

function fetchConversation(phoneNumber) {
	$.getJSON(OC.generateUrl('/apps/ocsms/get/conversation'),
		{
			'phoneNumber': phoneNumber
		},
		function(jsondata, status) {
			var phoneNumberLabel = phoneNumber;

			if (typeof jsondata['phoneNumbers'] != 'undefined') {
				var len = jsondata["phoneNumbers"].length;
				var ctLen = 0;
				phoneNumberLabel = '';

				$.each(jsondata["phoneNumbers"], function(id, val) {
					phoneNumberLabel += val;
					ctLen++;
					if (ctLen != len) {
						phoneNumberLabel += ",";
					}
					phoneNumberLabel += " ";
				});
			}

			conversationBuf = formatConversation(jsondata)[1];
			conversationBuf += '

';
			if (typeof jsondata['contactName'] == 'undefined' || jsondata['contactName'] == '') {
				$('#ocsms-phone-label').html(phoneNumberLabel);
				curContactName = phoneNumberLabel;
				$('#ocsms-phone-opt-number').html('');
			}
			else {
				$('#ocsms-phone-label').html(jsondata['contactName']);
				curContactName = jsondata['contactName'];
				$('#ocsms-phone-opt-number').html(phoneNumberLabel);
			}

			setMessageCountInfo(jsondata);

			if ($('#app-content-header').is(':hidden')) {
				$('#app-content-header').show();
			}

			$('#app-content-wrapper').html(conversationBuf);
			$('#app-content').scrollTop(1E10);

			curPhoneNumber = phoneNumber;
		}
	);
}

// Return (int) msgCount, (str) htmlConversation
function formatConversation(jsondata) {
	// Improve jQuery performance
	var buf = "";
	// Improve JS performance
	var msgClass = '';

	var msgCount = 0;
	var formatedDate = '';
	var formatedHour = '00';
	var formatedMin = '00';
	var months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.',
		'Oct.', 'Nov.', 'Dec.'];

	$.each(jsondata["conversation"], function(id, vals) {
		if (vals["type"] == 1) {
			msgClass = "msg-recv";
		}
		else if (vals["type"] == 2) {
			msgClass = "msg-sent";
		}
		else {
			msgClass = '';
		}

		// Store the greater msg date for refresher
		// Note: we divide by 100 because number compare too large integers
		if ((id/100) > (lastMsgDate/100)) {
			lastMsgDate = id;
		}

		// Multiplicate ID to permit date to use it properly
		msgDate = new Date(id*1);

		formatedHour = msgDate.getHours();
		if (formatedHour < 10) {
			formatedHour = '0' + formatedHour;
		}

		formatedMin = msgDate.getMinutes();
		if (formatedMin < 10) {
			formatedMin = '0' + formatedMin;
		}
		formatedDate = msgDate.getDate() + " " + months[msgDate.getMonth()] + " " +
			formatedHour + ":" + formatedMin;

		buf += '

' +
			vals["msg"] + '

' +
			formatedDate + '

';
		msgCount++;

	});

	return [msgCount,buf];
}

function changeSelectedConversation(item) {
	if (selectedConversation != null) {
		selectedConversation.parent().removeClass('selected');
	}
	selectedConversation = item;
	selectedConversation.parent().addClass('selected');
	selectedConversation.css("font-weight", "normal");
	selectedConversation.html(selectedConversation.attr("mailbox-label"));
}

function fetchInitialPeerList(jsondata) {
	// Use a buffer for better jQuery performance
	var peerListBuf = "";

	var bufferedContacts = [];

	$.each(jsondata['phonelist'], function(id, val) {
		var fn, peerLabel, idxVal;
		idxVal = id.replace(/\//g,' ');
		idxVal2 = idxVal.replace('/ /g','');
		if (typeof jsondata['contacts'][id] == 'undefined') {
			fn = '';
			peerLabel = idxVal;
		}
		else {
			fn = jsondata['contacts'][id];
			peerLabel = fn;
		}
		if ($.inArray(peerLabel, bufferedContacts) == -1) {
			peerListBuf += '

' + peerLabel + '
';
			bufferedContacts.push(peerLabel);
		}
	});

	lastMsgDate = jsondata["lastRead"];

	// Only modify peerList if there is peers
	if (peerListBuf != '') {
		$('#app-mailbox-peers ul').html(peerListBuf);
	}
}

function initDesktopNotifies() {
	if (!("Notification" in window)) {
		return;
	}

	Notification.requestPermission(function (permission) {
		if(!('permission' in Notification)) {
			Notification.permission = permission;
		}
	});
}

function desktopNotify(msg) {
	if (!("Notification" in window)) {
		return;
	}
	else if (Notification.permission === "granted") {
		new Notification("ownCloud SMS - " + msg);
	}
	else if (Notification.permission !== 'denied') {
		Notification.requestPermission(function (permission) {
			if(!('permission' in Notification)) {
				Notification.permission = permission;
			}
			if (permission === "granted") {
				new Notification("ownCloud SMS - " + msg);
			}
		});
	}
}

(function ($, OC) {
	$(document).ready(function () {
		// Register real title
		originalTitle = document.title;

		// Now bind the events when we click on the phone number
		$.getJSON(OC.generateUrl('/apps/ocsms/get/peerlist'), function(jsondata, status) {
			fetchInitialPeerList(jsondata);

			// Now bind the events when we click on the phone number
			$('#app-mailbox-peers').find('a[mailbox-navigation]').on('click', function (event) {
				var phoneNumber = $(this).attr('mailbox-navigation');
				OC.Util.History.pushState('phonenumber=' + phoneNumber);

				// phoneNumber must exist
				if (phoneNumber != null) {
					fetchConversation(phoneNumber);
					changeSelectedConversation($(this));
				}
				event.preventDefault();
			});

			var pnParam = $.urlParam('phonenumber');
			if (pnParam != null) {
				var urlPhoneNumber = decodeURIComponent(pnParam);
				if (urlPhoneNumber != null) {
					fetchConversation(urlPhoneNumber);

					var pObject = $("a[mailbox-navigation='" + urlPhoneNumber + "']");
					if (pObject != null) {
						changeSelectedConversation(pObject);
					}
				}
			}
			// Don't show message headers if no conversation selected
			else {
				if ($('#app-content-header').is(':visible')) {
					$('#app-content-header').hide();
				}
			}

		});
		initDesktopNotifies();
		setInterval(refreshConversation, 10000);
		setInterval(checkNewMessages, 10000);
	});

	// reset count and title
	window.onfocus = function () {
		unreadCountCurrentConv = 0;
		document.title = originalTitle;
	};
})(jQuery, OC);
<commit_msg>Js fix for checkNewMessages<commit_after>/**
 * ownCloud - ocsms
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Loic Blot <loic.blot@unix-experience.fr>
 * @copyright Loic Blot 2014-2015
 */

// Some global vars to improve performances
var selectedConversation = null;
var curPhoneNumber = null;
var curContactName = '';
var lastMsgDate = 0;
var unreadCountCurrentConv = 0;
var unreadCountAllConv = 0;
var unreadCountNotifStep = 12;
var lastUnreadCountAllConv = 0;
var originalTitle = document.title;

$.urlParam = function(name){
	var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
	if (results == null) {
		return null;
	}
	else {
		return results[1] || 0;
	}
};

var refreshConversation = function() {
	// if no conversation selected, then don't fetch page
	if (curPhoneNumber == null) {
		if ($('#app-content-header').is(':visible')) {
			$('#app-content-header').hide();
		}
		return;
	}
	$.getJSON(OC.generateUrl('/apps/ocsms/get/conversation'),
		{
			'phoneNumber': curPhoneNumber,
			"lastDate": lastMsgDate
		},
		function(jsondata, status) {
			var fmt = formatConversation(jsondata);
			conversationBuf = fmt[1];
			if (conversationBuf != '') {
				$('.msg-endtag').before(conversationBuf);
				$('#app-content').scrollTop(1E10);
				// This will blink the tab because there is new messages
				if (document.hasFocus() == false) {
					unreadCountCurrentConv += fmt[0];
					document.title = originalTitle + " (" + unreadCountCurrentConv + ")";
					desktopNotify(unreadCountCurrentConv + " unread message(s) in conversation with " + curContactName);
				}
			}

			setMessageCountInfo(jsondata);

			if ($('#app-content-header').is(':hidden')) {
				$('#app-content-header').show();
			}
		}
	);
};

var checkNewMessages = function() {
	unreadCountAllConv = 0;
	$.getJSON(OC.generateUrl('/apps/ocsms/get/new_messages'),
		{ 'lastDate': lastMsgDate },
		function(jsondata, status) {
			var peerListBuf = '';
			var bufferedContacts = [];

			$.each(jsondata['phonelist'], function(id, val) {
				var fn, peerLabel, idxVal;
				idxVal = id.replace(/\//g,' ');
				idxVal2 = idxVal.replace('/ /g','');
				if (typeof jsondata['contacts'][id] == 'undefined') {
					fn = '';
					peerLabel = idxVal;
				}
				else {
					fn = jsondata['contacts'][id];
					peerLabel = fn;
				}

				if ($.inArray(peerLabel, bufferedContacts) == -1) {
					$("div[mailbox-avatar='" + peerLabel + "']").remove();
					$("a[mailbox-label='" + peerLabel + "']").remove();
					peerListBuf = '

' + peerLabel + ' (' + val + ')
';
					$('#app-mailbox-peers ul').prepend(peerListBuf);
					bufferedContacts.push(peerLabel);

					// Re-set conversation because we reload the element
					if (idxVal == curPhoneNumber) {
						changeSelectedConversation($("a[mailbox-navigation='" + idxVal + "']"));
					}

					unreadCountAllConv += parseInt(val);

					// Now bind the events when we click on the phone number
					$("a[mailbox-navigation='" + idxVal + "']").on('click', function (event) {
						var phoneNumber = $(this).attr('mailbox-navigation');
						OC.Util.History.pushState('phonenumber=' + phoneNumber);

						// phoneNumber must exist
						if (phoneNumber != null) {
							fetchConversation(phoneNumber);
								unreadCountAllConv += val;changeSelectedConversation($(this));
						}
						event.preventDefault();
					});
				}
			});

			/*
			* Decrement notification step counter, but stop it a zero
			* Stop at zero permit to notify instanly the user when
			* there is new messages in all conversations
			*/

			if (unreadCountNotifStep > 0) {
				unreadCountNotifStep--;
			}

			if (unreadCountAllConv > 0) {
				/*
				* We notify user every two minutes for all messages
				* or if unreadCount changes
				*/
				if (unreadCountNotifStep == 0 || lastUnreadCountAllConv != unreadCountAllConv) {
					desktopNotify(unreadCountAllConv + " unread message(s) for all conversations");
					unreadCountNotifStep = 12;
					lastUnreadCountAllConv = unreadCountAllConv;
				}
			}
		}
	);
};

function setMessageCountInfo(jsondata) {
	if (typeof jsondata['msgCount'] != 'undefined') {
		if (jsondata['msgCount'] == 1) {
			$('#ocsms-phone-msg-nb').html(jsondata['msgCount'] + ' message');
		}
		else {
			$('#ocsms-phone-msg-nb').html(jsondata['msgCount'] + ' messages');
		}
	}
	else {
		$('#ocsms-phone-msg-nb').html('');
	}
}

function fetchConversation(phoneNumber) {
	$.getJSON(OC.generateUrl('/apps/ocsms/get/conversation'),
		{
			'phoneNumber': phoneNumber
		},
		function(jsondata, status) {
			var phoneNumberLabel = phoneNumber;

			if (typeof jsondata['phoneNumbers'] != 'undefined') {
				var len = jsondata["phoneNumbers"].length;
				var ctLen = 0;
				phoneNumberLabel = '';

				$.each(jsondata["phoneNumbers"], function(id, val) {
					phoneNumberLabel += val;
					ctLen++;
					if (ctLen != len) {
						phoneNumberLabel += ",";
					}
					phoneNumberLabel += " ";
				});
			}

			conversationBuf = formatConversation(jsondata)[1];
			conversationBuf += '

';
			if (typeof jsondata['contactName'] == 'undefined' || jsondata['contactName'] == '') {
				$('#ocsms-phone-label').html(phoneNumberLabel);
				curContactName = phoneNumberLabel;
				$('#ocsms-phone-opt-number').html('');
			}
			else {
				$('#ocsms-phone-label').html(jsondata['contactName']);
				curContactName = jsondata['contactName'];
				$('#ocsms-phone-opt-number').html(phoneNumberLabel);
			}

			setMessageCountInfo(jsondata);

			if ($('#app-content-header').is(':hidden')) {
				$('#app-content-header').show();
			}

			$('#app-content-wrapper').html(conversationBuf);
			$('#app-content').scrollTop(1E10);

			curPhoneNumber = phoneNumber;
		}
	);
}

// Return (int) msgCount, (str) htmlConversation
function formatConversation(jsondata) {
	// Improve jQuery performance
	var buf = "";
	// Improve JS performance
	var msgClass = '';

	var msgCount = 0;
	var formatedDate = '';
	var formatedHour = '00';
	var formatedMin = '00';
	var months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.',
		'Oct.', 'Nov.', 'Dec.'];

	$.each(jsondata["conversation"], function(id, vals) {
		if (vals["type"] == 1) {
			msgClass = "msg-recv";
		}
		else if (vals["type"] == 2) {
			msgClass = "msg-sent";
		}
		else {
			msgClass = '';
		}

		// Store the greater msg date for refresher
		// Note: we divide by 100 because number compare too large integers
		if ((id/100) > (lastMsgDate/100)) {
			lastMsgDate = id;
		}

		// Multiplicate ID to permit date to use it properly
		msgDate = new Date(id*1);

		formatedHour = msgDate.getHours();
		if (formatedHour < 10) {
			formatedHour = '0' + formatedHour;
		}

		formatedMin = msgDate.getMinutes();
		if (formatedMin < 10) {
			formatedMin = '0' + formatedMin;
		}
		formatedDate = msgDate.getDate() + " " + months[msgDate.getMonth()] + " " +
			formatedHour + ":" + formatedMin;

		buf += '

' +
			vals["msg"] + '

' +
			formatedDate + '

';
		msgCount++;

	});

	return [msgCount,buf];
}

function changeSelectedConversation(item) {
	if (selectedConversation != null) {
		selectedConversation.parent().removeClass('selected');
	}
	selectedConversation = item;
	selectedConversation.parent().addClass('selected');
	selectedConversation.css("font-weight", "normal");
	selectedConversation.html(selectedConversation.attr("mailbox-label"));
}

function fetchInitialPeerList(jsondata) {
	// Use a buffer for better jQuery performance
	var peerListBuf = "";

	var bufferedContacts = [];

	$.each(jsondata['phonelist'], function(id, val) {
		var fn, peerLabel, idxVal;
		idxVal = id.replace(/\//g,' ');
		idxVal2 = idxVal.replace('/ /g','');
		if (typeof jsondata['contacts'][id] == 'undefined') {
			fn = '';
			peerLabel = idxVal;
		}
		else {
			fn = jsondata['contacts'][id];
			peerLabel = fn;
		}
		if ($.inArray(peerLabel, bufferedContacts) == -1) {
			peerListBuf += '

' + peerLabel + '
';
			bufferedContacts.push(peerLabel);
		}
	});

	lastMsgDate = jsondata["lastRead"];

	// Only modify peerList if there is peers
	if (peerListBuf != '') {
		$('#app-mailbox-peers ul').html(peerListBuf);
	}
}

function initDesktopNotifies() {
	if (!("Notification" in window)) {
		return;
	}

	Notification.requestPermission(function (permission) {
		if(!('permission' in Notification)) {
			Notification.permission = permission;
		}
	});
}

function desktopNotify(msg) {
	if (!("Notification" in window)) {
		return;
	}
	else if (Notification.permission === "granted") {
		new Notification("ownCloud SMS - " + msg);
	}
	else if (Notification.permission !== 'denied') {
		Notification.requestPermission(function (permission) {
			if(!('permission' in Notification)) {
				Notification.permission = permission;
			}
			if (permission === "granted") {
				new Notification("ownCloud SMS - " + msg);
			}
		});
	}
}

(function ($, OC) {
	$(document).ready(function () {
		// Register real title
		originalTitle = document.title;

		// Now bind the events when we click on the phone number
		$.getJSON(OC.generateUrl('/apps/ocsms/get/peerlist'), function(jsondata, status) {
			fetchInitialPeerList(jsondata);

			// Now bind the events when we click on the phone number
			$('#app-mailbox-peers').find('a[mailbox-navigation]').on('click', function (event) {
				var phoneNumber = $(this).attr('mailbox-navigation');
				OC.Util.History.pushState('phonenumber=' + phoneNumber);

				// phoneNumber must exist
				if (phoneNumber != null) {
					fetchConversation(phoneNumber);
					changeSelectedConversation($(this));
				}
				event.preventDefault();
			});

			var pnParam = $.urlParam('phonenumber');
			if (pnParam != null) {
				var urlPhoneNumber = decodeURIComponent(pnParam);
				if (urlPhoneNumber != null) {
					fetchConversation(urlPhoneNumber);

					var pObject = $("a[mailbox-navigation='" + urlPhoneNumber + "']");
					if (pObject != null) {
						changeSelectedConversation(pObject);
					}
				}
			}
			// Don't show message headers if no conversation selected
			else {
				if ($('#app-content-header').is(':visible')) {
					$('#app-content-header').hide();
				}
			}

		});
		initDesktopNotifies();
		setInterval(refreshConversation, 10000);
		setInterval(checkNewMessages, 10000);
	});

	// reset count and title
	window.onfocus = function () {
		unreadCountCurrentConv = 0;
		document.title = originalTitle;
	};
})(jQuery, OC);
<|endoftext|>


<commit_before>import React from 'react';
import store from '../state/store';

var ESCAPE_KEY = 27;
var ENTER_KEY = 13;

export default class TodoItem extends React.Component {
	constructor( props ){
		super( props );
		this.state = {
			editText: props.todo.title
		};
	}

	handleUpdate(){
		var todo = this.props.todo;
		store.emit('todo:update', todo, todo.ui.input );
	}

	handleEdit () {
		this.props.todo.ui.set({status: 'editing'});
	}

	handleKeyDown (event) {
		if (event.which === ESCAPE_KEY) {
			todo.ui.set({status: 'ready'});
		} else if (event.which === ENTER_KEY) {
			this.handleUpdate();
		}
	}

	handleChange (event) {
		this.props.todo.ui.set({ input: event.target.value }).now();
	}

	/**
	 * Safely manipulate the DOM after updating the state when invoking
	 * `this.props.onEdit()` in the `handleEdit` method above.
	 * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
	 * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
	 */
	componentDidUpdate (prevProps) {
		var status = this.props.todo.ui.status;
		if (prevProps.todo.ui.status != status && status == 'editing') {
			var node = React.findDOMNode(this.refs.editField);
			node.focus();
			node.setSelectionRange(node.value.length, node.value.length);
		}
	}

	render () {
		var todo = this.props.todo,
			className = todo.ui.status,
			content = ''
		;

		if( todo.model.completed )
			className += ' completed';

		if( ['editing', 'updating'].indexOf( todo.ui.status ) != -1 ){
			content = (

					<input
						ref="editField"
						className="edit"
						value={ todo.ui.input }
						onBlur={this.handleUpdate}
						onChange={this.handleChange}
						onKeyDown={this.handleKeyDown} />
					Saving...

			);
		}
		else {
			content = (

					<input className="toggle"
						type="checkbox"
						checked={ todo.model.completed }
						onChange={ () => store.emit( 'todo:toggle', this.props.todo ) } />

						{ todo.model.title }

					<button className="destroy" onClick={ () => store.emit( 'todo:delete', this.props.todo ) } />
					Deleting...

			)
		}

		return (

				{ content }

		);
	}
};
<commit_msg>Fixes some event handlers<commit_after>import React from 'react';
import store from '../state/store';

var ESCAPE_KEY = 27;
var ENTER_KEY = 13;

export default class TodoItem extends React.Component {
	constructor(props){
		super(props);
		this.state = {
			editText: props.todo.title
		};
	}

	handleUpdate(){
		var todo = this.props.todo;
		store.emit('todo:update', todo, todo.ui.input );
	}

	handleEdit () {
		this.props.todo.ui.set({status: 'editing'});
	}

	handleKeyDown (event) {
		if (event.which === ESCAPE_KEY) {
			todo.ui.set({status: 'ready'});
		} else if (event.which === ENTER_KEY) {
			this.handleUpdate();
		}
	}

	handleChange (event) {
		this.props.todo.ui.set({ input: event.target.value }).now();
	}

	handleToggle() {
		store.emit( 'todo:toggle', this.props.todo );
	}

	handleDelete(){
		store.emit( 'todo:delete', this.props.todo );
	}

	/**
	 * Safely manipulate the DOM after updating the state when invoking
	 * `this.props.onEdit()` in the `handleEdit` method above.
	 * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
	 * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
	 */
	componentDidUpdate (prevProps) {
		var status = this.props.todo.ui.status;
		if (prevProps.todo.ui.status != status && status == 'editing') {
			var node = this.refs.editField;
			node.focus();
			node.setSelectionRange(node.value.length, node.value.length);
		}
	}

	render () {
		var todo = this.props.todo,
			className = todo.ui.status,
			content = ''
		;

		if( todo.model.completed )
			className += ' completed';

		if( ['editing', 'updating'].indexOf( todo.ui.status ) != -1 ){
			content = (

					<input
						ref="editField"
						className="edit"
						value={ todo.ui.input }
						onBlur={ () => this.handleUpdate() }
						onChange={ e => this.handleChange(e) }
						onKeyDown={ e => this.handleKeyDown(e) } />
					Saving...

			);
		}
		else {
			content = (

					<input className="toggle"
						type="checkbox"
						checked={ todo.model.completed }
						onChange={ () => this.handleToggle() } />
					 this.handleEdit() }>
						{ todo.model.title }

					<button className="destroy" onClick={ () => this.handleDelete() } />
					Deleting...

			)
		}

		return (

				{ content }

		);
	}
};
<|endoftext|>


<commit_before>var expect = require('chai').expect;
var nock = require('nock');
var Promise = require('bluebird');
var ClientsManager = require('../src/ClientsManager');
var constants = require('../src/constants');

var ArgumentError = require('../src/exceptions').ArgumentError;
var BASE_API_URL = 'https://' + constants.DOMAINS_BY_REGION.eu;

describe('ClientsManager', function () {

  beforeEach(function () {
    this.token = 'TEST_TOKEN';
    this.clients = new ClientsManager({
      accessToken: this.token,
      baseUrl: BASE_API_URL
    });
  });

  afterEach(function () {
    nock.cleanAll();
  });

  describe('#constructor', function () {
    it('should error when no options are provided', function () {
      expect(ClientsManager).to.throw(ArgumentError, 'Must provide client options');
    });

    it('should throw an error when no base URL is provided', function () {
      var client = ClientsManager.bind(null, {});

      expect(client).to.throw(ArgumentError, 'Must provide a base URL for the API');
    });

    it('should throw an error when the base URL is invalid', function () {
      var client = ClientsManager.bind(null, { baseUrl: '' });

      expect(client).to.throw(ArgumentError, 'The provided base URL is invalid');
    });

    it('should throw an error when no access token is provided', function () {
      var client = ClientsManager.bind(null, { baseUrl: 'http://domain.com' });

      expect(client).to.throw(ArgumentError, 'Must provide an access token');
    });

    it('should throw an error when the token is invalid', function () {
      var options = { baseUrl: 'http://test.com', accessToken: '' };
      var client = ClientsManager.bind(null, options);

      expect(client).to.throw(ArgumentError, 'Invalid access token');
    });
  });

  describe('#create', function () {
    var data = { name: 'Test client' };

    it('should be defined', function () {
      expect(this.clients.create).to.exist;
      expect(this.clients.create).to.be.an.instanceOf(Function);
    });

    it('should perform a POST request to /api/v2/clients', function (done) {
      var request = nock(BASE_API_URL)
        .post('/clients')
        .reply(201, data)

      this.clients
        .create(data)
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should include the token in the Authorization header', function (done) {
      var request = nock(BASE_API_URL)
        .post('/clients')
        .matchHeader('Authorization', 'Bearer ' + this.token)
        .reply(201, data)

      this.clients
        .create(data)
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should include the client data in the request body', function (done) {
      var request = nock(BASE_API_URL)
        .post('/clients', data)
        .reply(201, data)

      this.clients
        .create(data)
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });
  });

  describe('#getAll', function () {

    it('should be defined', function () {
      expect(this.clients.getAll).to.exist;
      expect(this.clients.getAll).to.be.an.instanceOf(Function);
    });

    it('should perform a GET request to /api/v2/clients', function (done) {
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200)

      this.clients
        .getAll()
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should include the token in the Authorization header', function (done) {
      var request = nock(BASE_API_URL)
        .get('/clients')
        .matchHeader('Authorization', 'Bearer ' + this.token)
        .reply(200)

      this.clients
        .getAll()
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should accept a callback', function (done) {
      this.clients.getAll(function () {
        done();
      });
    });

    it('should pass any errors to the callback', function (done) {
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(500);

      this.clients.getAll(function (err) {
        expect(err).to.exist;
        done();
      });
    });

    it('should pass the body of the response to the callback', function (done) {
      var data = [{ test: true }];
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200, data);

      this.clients.getAll(function (err, response) {
        expect(response).to.be.an.instanceOf(Array);
        expect(response.length).to.equal(data.length);
        expect(response[0].test).to.equal(data[0].test);
        done();
      });
    });

    it('should return a promise if no callback is given', function () {
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200);
      var returnValue = this.clients.getAll();

      expect(returnValue).to.be.an.instanceOf(Promise);
    });

    it('should pass any errors to the promise catch handler', function (done) {
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(500);

      this.clients
        .getAll()
        .catch(function (err) {
          expect(err).to.exist;
          done();
        });
    });

    it('should pass the body of the response to the "then" handler', function (done) {
      var data = [{ test: true }];
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200, data);

      this.clients
        .getAll()
        .then(function (clients) {
          expect(clients).to.be.an.instanceOf(Array);
          expect(clients.length).to.equal(data.length);
          expect(clients[0].test).to.equal(data[0].test);
          done();
        });
    });
  });
});

<commit_msg>Add more tests for the ClientsManager class<commit_after>var expect = require('chai').expect;
var nock = require('nock');
var Promise = require('bluebird');
var ClientsManager = require('../src/ClientsManager');

var ArgumentError = require('../src/exceptions').ArgumentError;
var BASE_API_URL = 'https://tenant.auth0.com';

describe('ClientsManager', function () {

  beforeEach(function () {
    this.clients = new ClientsManager({
      headers: {
        authorization: 'Bearer ' + this.token
      },
      baseUrl: BASE_API_URL
    });
  });

  afterEach(function () {
    nock.cleanAll();
  });

  describe('#constructor', function () {
    it('should error when no options are provided', function () {
      expect(ClientsManager)
        .to.throw(ArgumentError, 'Must provide client options');
    });

    it('should throw an error when no base URL is provided', function () {
      var client = ClientsManager.bind(null, {});

      expect(client)
        .to.throw(ArgumentError, 'Must provide a base URL for the API');
    });

    it('should throw an error when the base URL is invalid', function () {
      var client = ClientsManager.bind(null, { baseUrl: '' });

      expect(client)
        .to.throw(ArgumentError, 'The provided base URL is invalid');
    });
  });

  describe('#getAll', function () {

    beforeEach(function () {
      this.request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200);
    })

    it('should be defined', function () {
      expect(this.clients.getAll)
        .to.exist
        .to.be.an.instanceOf(Function);
    });

    it('should accept a callback', function (done) {
      this.clients.getAll(function () {
        done();
      });
    });

    it('should return a promise if no callback is given', function (done) {
      this
        .clients
        .getAll()
        .then(done.bind(null, null))
        .catch(done.bind(null, null));
    });

    it('should pass any errors to the promise catch handler', function (done) {
      nock.cleanAll();

      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(500);

      this
        .clients
        .getAll()
        .catch(function (err) {
          expect(err).to.exist;
          done();
        });
    });

    it('should pass the body of the response to the "then" handler', function (done) {
      nock.cleanAll();

      var data = [{ test: true }];
      var request = nock(BASE_API_URL)
        .get('/clients')
        .reply(200, data);

      this
        .clients
        .getAll()
        .then(function (clients) {
          expect(clients)
            .to.be.an.instanceOf(Array);

          expect(clients.length)
            .to.equal(data.length);

          expect(clients[0].test)
            .to.equal(data[0].test);

          done();
        });
    });

    it('should perform a GET request to /api/v2/clients', function (done) {
      var request = this.request;

      this
        .clients
        .getAll()
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should include the token in the Authorization header', function (done) {
      nock.cleanAll();

      var request = nock(BASE_API_URL)
        .get('/clients')
        .matchHeader('Authorization', 'Bearer ' + this.token)
        .reply(200)

      this
        .clients
        .getAll()
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });

    it('should pass the parameters in the query-string', function (done) {
      nock.cleanAll();

      var request = nock(BASE_API_URL)
        .get('/clients')
        .query({
          include_fields: true,
          fields: 'test'
        })
        .reply(200)

      this
        .clients
        .getAll({ includeFields: true, fields: 'test' })
        .then(function () {
          expect(request.isDone()).to.be.true;
          done();
        });
    });
  });

  describe('#create', function () {
    var data = { name: 'Test client' };

    beforeEach(function () {
      this.request = nock(BASE_API_URL)
        .post('/clients')
        .reply(201, data)
    })

    it('should be defined', function () {
      expect(this.clients.create)
        .to.exist
        .to.be.an.instanceOf(Function);
    });

    it('should accept a callback', function (done) {
      this
        .clients
        .create(data, done.bind(null, null));
    });

    it('should return a promise if no callback is given', function (done) {
      this
        .clients
        .create(data)
        .then(done.bind(null, null))
        .catch(done.bind(null, null));
    });

    it('should perform a POST request to /api/v2/clients', function (done) {
      var request = this.request;

      this
        .clients
        .create(data)
        .then(function () {
          expect(request.isDone())
            .to.be.true;

          done();
        });
    });

    it('should include the token in the Authorization header', function (done) {
      nock.cleanAll();

      var request = nock(BASE_API_URL)
        .post('/clients')
        .matchHeader('Authorization', 'Bearer ' + this.token)
        .reply(201, data)

      this
        .clients
        .create(data)
        .then(function () {
          expect(request.isDone())
            .to.be.true;

          done();
        });
    });

    it('should include the new client data in the request body', function (done) {
      nock.cleanAll();

      var request = nock(BASE_API_URL)
        .post('/clients', data)
        .reply(201, data)

      this
        .clients
        .create(data)
        .then(function () {
          expect(request.isDone())
            .to.be.true;

          done();
        });
    });
  });

  describe('#get', function () {
    beforeEach(function () {
      this.data = {
        id: 5,
        name: 'John Doe',
        email: 'john@doe.com'
      };

      this.request = nock(BASE_API_URL)
        .get('/clients/' + this.data.id)
        .reply(201, this.data);
    })

    it('should be defined', function () {
      expect(this.clients.get)
        .to.exist
        .to.be.an.instanceOf(Function);
    });

    it('should accept a callback', function (done) {
      var params = { id: this.data.id };

      this
        .clients
        .get(params, done.bind(null, null));
    });

    it('should return a promise if no callback is given', function (done) {
      this
        .clients
        .get({ id: this.data.id })
        .then(done.bind(null, null))
        .catch(done.bind(null, null));
    });

    it('should perform a POST request to /api/v2/clients/5', function (done) {
      var request = this.request;

      this
        .clients
        .get({ clientId: this.data.id })
        .then(function () {
          expect(request.isDone())
            .to.be.true;

          done();
        });
    });
  });
});

<|endoftext|>


<commit_before>'use strict';

var React = require('react');
var Reflux = require('reflux');
var d3 = require('d3');

var config = require('../config');
var actions = require('../actions/actions');
var taskObj = require('../mixins/taskobj');
var ActivityStore = require('../stores/activity_store');

module.exports = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },

  mixins: [
    Reflux.connect(ActivityStore, 'activity'),
    Reflux.listenTo(actions.taskActivityLoaded, 'activityLoaded')
  ],

  getInitialState: function() {
    return {
      loadCount: 25
    };
  },

  statics: {
    fetchData: function(params) {
      actions.taskActivity(params.task);
    }
  },

  loadMore: function() {
    this.setState({ loadCount: this.state.loadCount += 25 });
  },

  activityLoaded: function() {
    // Reset the load count when new data has loaded.
    this.setState({ loadCount: 25 });
  },

  render: function() {
    var row;
    var dateDisplay = d3.time.format('%B %-d');
    var timeDisplay = d3.time.format('%-I:%-M%p');

    if (this.state.activity.length) {
      // Show only the first 25 results.
      row = this.state.activity.slice(0, this.state.loadCount).map(function(action, i) {
        if (!action.attributes.user) return;
        var permalink = 'key-' + action.attributes.key;
        var profile = config.userProfileURL + action.attributes.user;
        var editor = (action.attributes.editor) ? action.attributes.editor : '';
        var actionDay = dateDisplay(new Date(action.time * 1000));
        var actionTime = timeDisplay(new Date(action.time * 1000));

        return (
          /* jshint ignore:start */

              {action.attributes.action}

                {action.attributes.user}

              {editor}

              {actionDay}
              {actionTime}

          /* jshint ignore:end */
        );
      });
    } else {
      /* jshint ignore:start */
      row = (No recent activity found.);
      /* jshint ignore:end */
    }

    var taskTitle = taskObj(this.context.router.getCurrentParams().task).title;

    // Load more button
    var loadmore = '';

    if (this.state.activity.length) {
      /* jshint ignore:start */
      if (this.state.loadCount >= this.state.activity.length) {
        loadmore = (<button className='col12 quiet disabled round-bottom'>Activity loaded</button>);
      } else {
        loadmore = (<button onClick={this.loadMore} className='col12 button round-bottom'>Load more</button>);
      }
      /* jshint ignore:end */
    }

    return (
      /* jshint ignore:start */

{taskTitle}

            {row}
            {loadmore}

      /* jshint ignore:end */
    );
  }
});
<commit_msg>fix button<commit_after>'use strict';

var React = require('react');
var Reflux = require('reflux');
var d3 = require('d3');

var config = require('../config');
var actions = require('../actions/actions');
var taskObj = require('../mixins/taskobj');
var ActivityStore = require('../stores/activity_store');

module.exports = React.createClass({
  contextTypes: {
    router: React.PropTypes.func
  },

  mixins: [
    Reflux.connect(ActivityStore, 'activity'),
    Reflux.listenTo(actions.taskActivityLoaded, 'activityLoaded')
  ],

  getInitialState: function() {
    return {
      loadCount: 25
    };
  },

  statics: {
    fetchData: function(params) {
      actions.taskActivity(params.task);
    }
  },

  loadMore: function() {
    this.setState({ loadCount: this.state.loadCount += 25 });
  },

  activityLoaded: function() {
    // Reset the load count when new data has loaded.
    this.setState({ loadCount: 25 });
  },

  render: function() {
    var row;
    var dateDisplay = d3.time.format('%B %-d');
    var timeDisplay = d3.time.format('%-I:%-M%p');

    if (this.state.activity.length) {
      // Show only the first 25 results.
      row = this.state.activity.slice(0, this.state.loadCount).map(function(action, i) {
        if (!action.attributes.user) return;
        var permalink = 'key-' + action.attributes.key;
        var profile = config.userProfileURL + action.attributes.user;
        var editor = (action.attributes.editor) ? action.attributes.editor : '';
        var actionDay = dateDisplay(new Date(action.time * 1000));
        var actionTime = timeDisplay(new Date(action.time * 1000));

        return (
          /* jshint ignore:start */

              {action.attributes.action}

                {action.attributes.user}

              {editor}

              {actionDay}
              {actionTime}

          /* jshint ignore:end */
        );
      });
    } else {
      /* jshint ignore:start */
      row = (No recent activity found.);
      /* jshint ignore:end */
    }

    var taskTitle = taskObj(this.context.router.getCurrentParams().task).title;

    // Load more button
    var loadmore = '';

    if (this.state.activity.length) {
      /* jshint ignore:start */
      if (this.state.loadCount >= this.state.activity.length) {
        loadmore = (<button className='button col12 quiet disabled round-bottom'>Activity loaded</button>);
      } else {
        loadmore = (<button onClick={this.loadMore} className='col12 button round-bottom'>Load more</button>);
      }
      /* jshint ignore:end */
    }

    return (
      /* jshint ignore:start */

{taskTitle}

            {row}
            {loadmore}

      /* jshint ignore:end */
    );
  }
});
<|endoftext|>


<commit_before>// Dependencies
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import i18next from 'i18next';
import InputText from '../input-text';
import DatePicker from 'react-date-picker';
import isArray from 'lodash/isArray';
import uniqueId from 'lodash/uniqueId';
import closest from 'closest';

const isISOString = value => moment(value, moment.ISO_8601).isValid();

class InputDate extends Component {
    constructor(props) {
        super(props);
        const {rawInputValue} = props;
        const state = {
            dropDownDate: isISOString(rawInputValue) ? moment(rawInputValue, moment.ISO_8601) : moment(),
            inputDate: this._formatDate(rawInputValue),
            displayPicker: false
        };
        this.state = state;
        this._inputDateId = uniqueId('input-date-');
    }

    componentWillMount() {
        moment.locale(this.props.locale);
        document.addEventListener('click', this._onDocumentClick);
    }

    componentDidMount() {
        const {drops, showDropdowns} = this.props;
        const {inputDate: startDate} = this.state;
    }

    componentWillReceiveProps({rawInputValue}) {
        this.setState({
            dropDownDate: isISOString(rawInputValue) ? moment(rawInputValue, moment.ISO_8601) : moment(),
            inputDate: this._formatDate(rawInputValue)
        });
    }

    componentWillUnmount() {
        document.removeEventListener('click', this._onDocumentClick);
    }

    _isInputFormatCorrect = value => this._parseInputDate(value).isValid();

    _parseInputDate = inputDate => {
        const {format} = this.props;
        return moment(inputDate, format);
    };

    _formatDate = isoDate => {
        let {format} = this.props;
        if (isISOString(isoDate)) {
            if (isArray(format)) {
                format = format[0];
            }
            return moment(isoDate, moment.ISO_8601).format(format);
        } else {
            return isoDate;
        }
    };

    _onInputChange = (inputDate, fromBlur) => {
        if (this._isInputFormatCorrect(inputDate)) {
            const dropDownDate = this._parseInputDate(inputDate);
            this.setState({dropDownDate, inputDate});
        } else {
            this.setState({inputDate});
        }
        if(fromBlur !== true) {
            this.props.onChange(inputDate);
        }
    };

    _onInputBlur = () => {
        const {onBlur} = this.props;
        const {inputDate} = this.state;
        if(onBlur) onBlur();
        this._onInputChange(inputDate, true);
    };

    _onDropDownChange = (text, date) => {
        if (date._isValid) {
            this.setState({displayPicker: false}, () => {
                this.props.onChange(date.toISOString());
                this._onInputChange(this._formatDate(date.toISOString()), true) // Add 12 hours to avoid skipping a day due to different locales
            });
        }
    };

    _onInputFocus = () => {
        this.setState({displayPicker: true});
    };

    _onDocumentClick = ({target}) => {
        const targetClassAttr = target.getAttribute('class');
        const isTriggeredFromPicker = targetClassAttr ? targetClassAttr.includes('dp-cell') : false; //this is the only way to check the target comes from picker cause at this stage, month and year div are unmounted by React.
        if(!isTriggeredFromPicker) {
            //if target was not triggered inside the date picker, we check it was not triggered by the input
            if (closest(target, `[data-id='${this._inputDateId}']`, true) === undefined) {
                this.setState({displayPicker: false}, () => this._onInputBlur());
            }
        }
    };

    _handleKeyDown = ({key}) => {
        if (key === 'Tab' || key === 'Enter') {
            this.setState({displayPicker: false}, () => this._onInputBlur());
        }
    };

    getValue = () => {
        const {inputDate} = this.state;
        const rawValue = this._isInputFormatCorrect(inputDate) ? this._parseInputDate(inputDate).toISOString() : null;
        return this.props.beforeValueGetter(rawValue);
    };

    validate = () => {
        const {inputDate} = this.state;
        const {isRequired} = this.props;
        if ('' === inputDate || !inputDate) {
            return ({
                isValid: !isRequired,
                message: 'field.required'
            });
        } else {
            return ({
                isValid: this._isInputFormatCorrect(inputDate),
                message: i18next.t('input.date.invalid', {date: inputDate})
            });
        }
    };

    render() {
        const {disabled, error, locale, name, placeholder, valid} = this.props;
        const {dropDownDate, inputDate, displayPicker} = this.state;
        const {_onInputBlur, _onInputChange, _onInputFocus, _onDropDownChange, _onPickerCloserClick, _handleKeyDown} = this;
        const inputProps = { disabled };
        return (

                <InputText error={error} name={name} onChange={_onInputChange} onKeyDown={_handleKeyDown} onFocus={_onInputFocus} placeholder={placeholder} ref='input' rawInputValue={inputDate} valid={valid} {...inputProps} />
                {displayPicker &&

                        <DatePicker
                            date={dropDownDate}
                            hideFooter
                            locale={locale}
                            onChange={_onDropDownChange}
                            ref='picker' />

                }

        );
    }
}

InputDate.displayName = 'InputDate';
InputDate.propTypes =  {
    drops: PropTypes.oneOf(['up', 'down']).isRequired,
    error: PropTypes.string,
    locale: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    beforeValueGetter: PropTypes.func.isRequired,
    placeholder: PropTypes.string,
    showDropdowns: PropTypes.bool.isRequired,
    rawInputValue: (props, propName, componentName) => {
        const prop = props[propName];
        if (prop && !isISOString(prop)) {
            throw new Error(`The date ${prop} provided to the component ${componentName} is not an ISO date. Please provide a valid date string.`);
        }
    },
    validate: PropTypes.func
};
InputDate.defaultProps =  {
    drops: 'down',
    locale: 'en',
    format: 'MM/DD/YYYY',
    beforeValueGetter: value => value,
    /**
    * Default onChange prop, that will log an error.
    */
    onChange() {
        console.error('You did not give an onChange method to an input date, please check your code.');
    },
    showDropdowns: true,
    valid: PropTypes.bool,
    validate: isISOString
};
export default InputDate;
<commit_msg>input-date - fix proptypes<commit_after>// Dependencies
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import i18next from 'i18next';
import InputText from '../input-text';
import DatePicker from 'react-date-picker';
import isArray from 'lodash/isArray';
import uniqueId from 'lodash/uniqueId';
import closest from 'closest';

const isISOString = value => moment(value, moment.ISO_8601).isValid();

class InputDate extends Component {
    constructor(props) {
        super(props);
        const {rawInputValue} = props;
        const state = {
            dropDownDate: isISOString(rawInputValue) ? moment(rawInputValue, moment.ISO_8601) : moment(),
            inputDate: this._formatDate(rawInputValue),
            displayPicker: false
        };
        this.state = state;
        this._inputDateId = uniqueId('input-date-');
    }

    componentWillMount() {
        moment.locale(this.props.locale);
        document.addEventListener('click', this._onDocumentClick);
    }

    componentDidMount() {
        const {drops, showDropdowns} = this.props;
        const {inputDate: startDate} = this.state;
    }

    componentWillReceiveProps({rawInputValue}) {
        this.setState({
            dropDownDate: isISOString(rawInputValue) ? moment(rawInputValue, moment.ISO_8601) : moment(),
            inputDate: this._formatDate(rawInputValue)
        });
    }

    componentWillUnmount() {
        document.removeEventListener('click', this._onDocumentClick);
    }

    _isInputFormatCorrect = value => this._parseInputDate(value).isValid();

    _parseInputDate = inputDate => {
        const {format} = this.props;
        return moment(inputDate, format);
    };

    _formatDate = isoDate => {
        let {format} = this.props;
        if (isISOString(isoDate)) {
            if (isArray(format)) {
                format = format[0];
            }
            return moment(isoDate, moment.ISO_8601).format(format);
        } else {
            return isoDate;
        }
    };

    _onInputChange = (inputDate, fromBlur) => {
        if (this._isInputFormatCorrect(inputDate)) {
            const dropDownDate = this._parseInputDate(inputDate);
            this.setState({dropDownDate, inputDate});
        } else {
            this.setState({inputDate});
        }
        if(fromBlur !== true) {
            this.props.onChange(inputDate);
        }
    };

    _onInputBlur = () => {
        const {onBlur} = this.props;
        const {inputDate} = this.state;
        if(onBlur) onBlur();
        this._onInputChange(inputDate, true);
    };

    _onDropDownChange = (text, date) => {
        if (date._isValid) {
            this.setState({displayPicker: false}, () => {
                this.props.onChange(date.toISOString());
                this._onInputChange(this._formatDate(date.toISOString()), true) // Add 12 hours to avoid skipping a day due to different locales
            });
        }
    };

    _onInputFocus = () => {
        this.setState({displayPicker: true});
    };

    _onDocumentClick = ({target}) => {
        const targetClassAttr = target.getAttribute('class');
        const isTriggeredFromPicker = targetClassAttr ? targetClassAttr.includes('dp-cell') : false; //this is the only way to check the target comes from picker cause at this stage, month and year div are unmounted by React.
        if(!isTriggeredFromPicker) {
            //if target was not triggered inside the date picker, we check it was not triggered by the input
            if (closest(target, `[data-id='${this._inputDateId}']`, true) === undefined) {
                this.setState({displayPicker: false}, () => this._onInputBlur());
            }
        }
    };

    _handleKeyDown = ({key}) => {
        if (key === 'Tab' || key === 'Enter') {
            this.setState({displayPicker: false}, () => this._onInputBlur());
        }
    };

    getValue = () => {
        const {inputDate} = this.state;
        const rawValue = this._isInputFormatCorrect(inputDate) ? this._parseInputDate(inputDate).toISOString() : null;
        return this.props.beforeValueGetter(rawValue);
    };

    validate = () => {
        const {inputDate} = this.state;
        const {isRequired} = this.props;
        if ('' === inputDate || !inputDate) {
            return ({
                isValid: !isRequired,
                message: 'field.required'
            });
        } else {
            return ({
                isValid: this._isInputFormatCorrect(inputDate),
                message: i18next.t('input.date.invalid', {date: inputDate})
            });
        }
    };

    render() {
        const {disabled, error, locale, name, placeholder, valid} = this.props;
        const {dropDownDate, inputDate, displayPicker} = this.state;
        const {_onInputBlur, _onInputChange, _onInputFocus, _onDropDownChange, _onPickerCloserClick, _handleKeyDown} = this;
        const inputProps = { disabled };
        return (

                <InputText error={error} name={name} onChange={_onInputChange} onKeyDown={_handleKeyDown} onFocus={_onInputFocus} placeholder={placeholder} ref='input' rawInputValue={inputDate} valid={valid} {...inputProps} />
                {displayPicker &&

                        <DatePicker
                            date={dropDownDate}
                            hideFooter
                            locale={locale}
                            onChange={_onDropDownChange}
                            ref='picker' />

                }

        );
    }
}

InputDate.displayName = 'InputDate';
InputDate.propTypes =  {
    drops: PropTypes.oneOf(['up', 'down']).isRequired,
    error: PropTypes.string,
    locale: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    beforeValueGetter: PropTypes.func.isRequired,
    placeholder: PropTypes.string,
    showDropdowns: PropTypes.bool.isRequired,
    rawInputValue: (props, propName, componentName) => {
        const prop = props[propName];
        if (prop && !isISOString(prop)) {
            throw new Error(`The date ${prop} provided to the component ${componentName} is not an ISO date. Please provide a valid date string.`);
        }
    },
    validate: PropTypes.func
};
InputDate.defaultProps =  {
    drops: 'down',
    locale: 'en',
    format: 'MM/DD/YYYY',
    beforeValueGetter: value => value,
    /**
    * Default onChange prop, that will log an error.
    */
    onChange() {
        console.error('You did not give an onChange method to an input date, please check your code.');
    },
    showDropdowns: true,
    valid: true,
    validate: isISOString
};
export default InputDate;
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { getDOMWidth } from '../util/findDOMNode'
import { isFunction } from '../util/validateType'

import Wrapper from './Wrapper/wrapper'
import List from './Wrapper/list'
import Thumbs from './Thumbs/thumbs'
import { ArrowLeft, ArrowRight } from './Arrow/arrow'

class Carousel extends Component {

  constructor(props) {
    super(props);

    this.state = {
      options: {
        listWidth: Math.ceil(props.options.listWidth) || 300,
        listHeight: props.options.listHeight || 400
      },
      wrapperIsHover: false,
      actionID: this.props.options.start_actionID || 0
    }
  }

  componentDidMount() {
    const auto_play_speed = this.props.options.auto_play_speed ? this.props.options.auto_play_speed : 1000
    this.timer = setInterval(this._handleAutoPlay.bind(this), auto_play_speed)
  }

  componentWillUnmount() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.urls !== this.props.urls ||
      nextProps.auto_play !== this.props.auto_play ||
      nextProps.use_thumbs !== this.props.use_thumbs ||
      nextProps.use_arrow !== this.props.use_arrow ||
      nextProps.options.thumbsPerPage !== this.props.options.thumbsPerPage ||
      nextState.actionID !== this.state.actionID ||
      nextState.wrapperIsHover !== this.state.wrapperIsHover ||
      nextProps.options.listWidth !== this.props.options.listWidth
  }

  _handleAutoPlay() {
    if (this.state.wrapperIsHover || !this.props.auto_play) return
    if (this.state.actionID + 1 >= this.props.urls.length) {
      this.setState({
        actionID: 0
      })
    } else {
      if (this.props.beforeWrapperMouseOver) {
        if (isFunction(this.props.beforeWrapperMouseOver, 'beforeWrapperMouseOver')) {
          this.props.beforeActionIDChange(this.state.actionID)
        }
      }
      this.setState({
        actionID: this.state.actionID + 1
      }, () => {
        if (this.props.afterWrapperMouseOver) {
          if (isFunction(this.props.afterWrapperMouseOver, 'afterWrapperMouseOver')) {
            this.props.afterActionIDChange(this.state.actionID)
          }
        }
      })
    }
  }

  _handleWrapperMouseOver() {
    const actionUrl = this.props.urls[this.state.actionID]
    if (this.props.beforeWrapperMouseOver) {
      if (isFunction(this.props.beforeWrapperMouseOver, 'beforeWrapperMouseOver')) {
        this.props.beforeWrapperMouseOver(this.state.actionID, actionUrl)
      }
    }
    this.setState({
      wrapperIsHover: true
    }, () => {
      if (this.props.afterWrapperMouseOver) {
        if (isFunction(this.props.afterWrapperMouseOver, 'afterWrapperMouseOver')) {
          this.props.afterWrapperMouseOver(this.state.actionID, actionUrl)
        }
      }
    })
  }

  _handleWrapperMouseLeave() {
    const actionUrl = this.props.urls[this.state.actionID]
    if (this.props.beforeWrapperMouseLeave) {
      if (isFunction(this.props.beforeWrapperMouseLeave, 'beforeWrapperMouseLeave')) {
        this.props.beforeWrapperMouseLeave(this.state.actionID, actionUrl)
      }
    }
    this.setState({
      wrapperIsHover: false
    }, () => {
      if (this.props.afterWrapperMouseLeave) {
        if (isFunction(this.props.afterWrapperMouseLeave, 'afterWrapperMouseLeave')) {
          this.props.afterWrapperMouseLeave(this.state.actionID, actionUrl)
        }
      }
    })
  }

  _handleArrowLeft() {
    if (this.state.actionID - 1 < 0) return
    this._handleChangeThumbsID(this.state.actionID - 1)
  }

  _handleArrowRight() {
    if (this.state.actionID + 1 >= this.props.urls.length) return
    this._handleChangeThumbsID(this.state.actionID + 1)
  }

  _handleTouchChangeActionID(correctX) {
    correctX > 0 ? this._handleArrowRight() : this._handleArrowLeft()
  }

  _handleChangeThumbsID(id) {
    if (this.props.beforeActionIDChange) {
      if (isFunction(this.props.beforeActionIDChange, 'beforeActionIDChange')) {
        this.props.beforeActionIDChange(this.state.actionID)
      }
    }
    this.setState({
      actionID: id
    }, () => {
      if (this.props.afterActionIDChange) {
        if (isFunction(this.props.afterActionIDChange, 'beforeActionIDChange')) {
          this.props.afterActionIDChange(this.state.actionID)
        }
      }
    })
  }

  _renderList(carousel_list_style) {
    const _use_lazy_load = this.props.lazy_load
    return this.props.urls.map((url, idx) => {
      return <List
        width={ Math.ceil(this.props.options.listWidth) }
        height={ this.state.options.listHeight }
        carousel_list_style={ carousel_list_style }
        key={ `cm-carousel-list-${url}-${idx}` }
        idx={ idx }
        url={ _use_lazy_load ? (idx === (this.state.actionID - 1) || idx === (this.state.actionID + 1) || idx === (this.state.actionID) ? url : '') : url } />
    })
  }

  _render_thumbs = (thumbs_style, thumbs_item_style) => {
    if (this.props.use_thumbs) {
      return (
        <Thumbs
          thumbsPerPage={ this.props.options.thumbsPerPage }
          thumbs_style={ thumbs_style }
          thumbs_item_style={ thumbs_item_style }
          actionID={ this.state.actionID }
          listWidth={ Math.ceil(this.props.options.listWidth) }
          urls={ this.props.urls }
          handleChangeThumbsID={ this._handleChangeThumbsID.bind(this) } />
      )
    }
  }

  _render_arrow = () => {
    if (this.props.use_arrow) {
      return (
        [
          <ArrowLeft
            key={ `arrow-left` }
            wrapperIsHover={ this.state.wrapperIsHover }
            handleArrowLeft={ this._handleArrowLeft.bind(this) }
            useLeftArrow={ this.props.use_left_arrow }
            wrapperHeight={ this.props.options.listHeight } />,
          <ArrowRight
            key={ `arrow-right` }
            wrapperIsHover={ this.state.wrapperIsHover }
            handleArrowRight={ this._handleArrowRight.bind(this) }
            useRightArrow={ this.props.use_right_arrow }
            wrapperHeight={ this.props.options.listHeight } />
        ]
      )
    }
  }

  render() {
    const {
      custom_styles
    } = this.props
    const _wrapper_style = {
      width: Math.ceil(this.props.options.listWidth),
      position: 'relative'
    }
    const _carousel_wrapper_style = Object.assign({}, custom_styles ? custom_styles.wrapper : {})
    const _carousel_list_style = Object.assign({}, custom_styles ? custom_styles.list : {})
    const _thumbs_style = Object.assign({}, custom_styles ? custom_styles.thumbs : {})
    const _thumbs_item_style = Object.assign({}, custom_styles ? custom_styles.thumbs_item : {})

    return (
      <div
        style={ _wrapper_style }>
        <Wrapper
          ref={ node => this.wrapper = node }
          listWidth={ Math.ceil(this.props.options.listWidth) }
          listHeight={ this.state.options.listHeight }
          actionID={ this.state.actionID }
          carouse_wrapper_style={ _carousel_wrapper_style }
          styleEase={ this.props.styleEase }
          touch_mode={ this.props.touch_mode }
          onWrapperMouseOver={ this._handleWrapperMouseOver.bind(this) }
          onWrapperMouseLeave={ this._handleWrapperMouseLeave.bind(this) }
          handleTouchChangeActionID={ this._handleTouchChangeActionID.bind(this) }>
          { this._renderList.call(this, _carousel_list_style) }
        </Wrapper>
        { this._render_arrow.call(this) }
        { this._render_thumbs.call(this, _thumbs_style, _thumbs_item_style) }
      </div>
    )
  }
}

export default Carousel
<commit_msg>fixed carousel method<commit_after>import React, { Component } from 'react'
import { getDOMWidth } from '../util/findDOMNode'
import { isFunction } from '../util/validateType'

import Wrapper from './Wrapper/wrapper'
import List from './Wrapper/list'
import Thumbs from './Thumbs/thumbs'
import { ArrowLeft, ArrowRight } from './Arrow/arrow'

class Carousel extends Component {

  constructor(props) {
    super(props);

    this.state = {
      options: {
        listWidth: Math.ceil(props.options.listWidth) || 300,
        listHeight: props.options.listHeight || 400
      },
      wrapperIsHover: false,
      actionID: this.props.options.start_actionID || 0
    }
  }

  componentDidMount() {
    const auto_play_speed = this.props.options.auto_play_speed ? this.props.options.auto_play_speed : 1000
    this.timer = setInterval(this._handleAutoPlay.bind(this), auto_play_speed)
  }

  componentWillUnmount() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  }

  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.urls !== this.props.urls ||
      nextProps.auto_play !== this.props.auto_play ||
      nextProps.use_thumbs !== this.props.use_thumbs ||
      nextProps.use_arrow !== this.props.use_arrow ||
      nextProps.options.thumbsPerPage !== this.props.options.thumbsPerPage ||
      nextState.actionID !== this.state.actionID ||
      nextState.wrapperIsHover !== this.state.wrapperIsHover ||
      nextProps.options.listWidth !== this.props.options.listWidth
  }

  _handleAutoPlay() {
    if (this.state.wrapperIsHover || !this.props.auto_play) return
    if (this.state.actionID + 1 >= this.props.urls.length) {
      this.setState({
        actionID: 0
      })
    } else {
      if (this.props.beforeWrapperMouseOver) {
        if (isFunction(this.props.beforeWrapperMouseOver, 'beforeWrapperMouseOver')) {
          this.props.beforeActionIDChange(this.state.actionID)
        }
      }
      this.setState({
        actionID: this.state.actionID + 1
      }, () => {
        if (this.props.afterWrapperMouseOver) {
          if (isFunction(this.props.afterWrapperMouseOver, 'afterWrapperMouseOver')) {
            this.props.afterActionIDChange(this.state.actionID)
          }
        }
      })
    }
  }

  _handleWrapperMouseOver() {
    const actionUrl = this.props.urls[this.state.actionID]
    if (this.props.beforeWrapperMouseOver) {
      if (isFunction(this.props.beforeWrapperMouseOver, 'beforeWrapperMouseOver')) {
        this.props.beforeWrapperMouseOver(this.state.actionID, actionUrl)
      }
    }
    this.setState({
      wrapperIsHover: true
    }, () => {
      if (this.props.afterWrapperMouseOver) {
        if (isFunction(this.props.afterWrapperMouseOver, 'afterWrapperMouseOver')) {
          this.props.afterWrapperMouseOver(this.state.actionID, actionUrl)
        }
      }
    })
  }

  _handleWrapperMouseLeave() {
    const actionUrl = this.props.urls[this.state.actionID]
    if (this.props.beforeWrapperMouseLeave) {
      if (isFunction(this.props.beforeWrapperMouseLeave, 'beforeWrapperMouseLeave')) {
        this.props.beforeWrapperMouseLeave(this.state.actionID, actionUrl)
      }
    }
    this.setState({
      wrapperIsHover: false
    }, () => {
      if (this.props.afterWrapperMouseLeave) {
        if (isFunction(this.props.afterWrapperMouseLeave, 'afterWrapperMouseLeave')) {
          this.props.afterWrapperMouseLeave(this.state.actionID, actionUrl)
        }
      }
    })
  }

  _handleArrowLeft() {
    if (this.state.actionID - 1 < 0) return
    this._handleChangeThumbsID(this.state.actionID - 1)
  }

  _handleArrowRight() {
    if (this.state.actionID + 1 >= this.props.urls.length) return
    this._handleChangeThumbsID(this.state.actionID + 1)
  }

  _handleTouchChangeActionID(correctX) {
    correctX > 0 ? this._handleArrowRight() : this._handleArrowLeft()
  }

  _handleChangeThumbsID(id) {
    if (this.props.beforeActionIDChange) {
      if (isFunction(this.props.beforeActionIDChange, 'beforeActionIDChange')) {
        this.props.beforeActionIDChange(this.state.actionID)
      }
    }
    this.setState({
      actionID: id
    }, () => {
      if (this.props.afterActionIDChange) {
        if (isFunction(this.props.afterActionIDChange, 'beforeActionIDChange')) {
          this.props.afterActionIDChange(this.state.actionID)
        }
      }
    })
  }

  _renderList(carousel_list_style) {
    const _use_lazy_load = this.props.lazy_load
    return this.props.urls.map((url, idx) => {
      return <List
        width={ Math.ceil(this.props.options.listWidth) }
        height={ this.state.options.listHeight }
        carousel_list_style={ carousel_list_style }
        key={ `cm-carousel-list-${url}-${idx}` }
        idx={ idx }
        url={ _use_lazy_load ? (idx === (this.state.actionID - 1) || idx === (this.state.actionID + 1) || idx === (this.state.actionID) ? url : '') : url } />
    })
  }

  _render_thumbs(thumbs_style, thumbs_item_style) {
    if (this.props.use_thumbs) {
      return (
        <Thumbs
          thumbsPerPage={ this.props.options.thumbsPerPage }
          thumbs_style={ thumbs_style }
          thumbs_item_style={ thumbs_item_style }
          actionID={ this.state.actionID }
          listWidth={ Math.ceil(this.props.options.listWidth) }
          urls={ this.props.urls }
          handleChangeThumbsID={ this._handleChangeThumbsID.bind(this) } />
      )
    }
  }

  _render_arrow() {
    if (this.props.use_arrow) {
      return (
        [
          <ArrowLeft
            key={ `arrow-left` }
            wrapperIsHover={ this.state.wrapperIsHover }
            handleArrowLeft={ this._handleArrowLeft.bind(this) }
            useLeftArrow={ this.props.use_left_arrow }
            wrapperHeight={ this.props.options.listHeight } />,
          <ArrowRight
            key={ `arrow-right` }
            wrapperIsHover={ this.state.wrapperIsHover }
            handleArrowRight={ this._handleArrowRight.bind(this) }
            useRightArrow={ this.props.use_right_arrow }
            wrapperHeight={ this.props.options.listHeight } />
        ]
      )
    }
  }

  render() {
    const {
      custom_styles
    } = this.props
    const _wrapper_style = {
      width: Math.ceil(this.props.options.listWidth),
      position: 'relative'
    }
    const _carousel_wrapper_style = Object.assign({}, custom_styles ? custom_styles.wrapper : {})
    const _carousel_list_style = Object.assign({}, custom_styles ? custom_styles.list : {})
    const _thumbs_style = Object.assign({}, custom_styles ? custom_styles.thumbs : {})
    const _thumbs_item_style = Object.assign({}, custom_styles ? custom_styles.thumbs_item : {})

    return (
      <div
        style={ _wrapper_style }>
        <Wrapper
          ref={ node => this.wrapper = node }
          listWidth={ Math.ceil(this.props.options.listWidth) }
          listHeight={ this.state.options.listHeight }
          actionID={ this.state.actionID }
          carouse_wrapper_style={ _carousel_wrapper_style }
          styleEase={ this.props.styleEase }
          touch_mode={ this.props.touch_mode }
          onWrapperMouseOver={ this._handleWrapperMouseOver.bind(this) }
          onWrapperMouseLeave={ this._handleWrapperMouseLeave.bind(this) }
          handleTouchChangeActionID={ this._handleTouchChangeActionID.bind(this) }>
          { this._renderList.call(this, _carousel_list_style) }
        </Wrapper>
        { this._render_arrow.call(this) }
        { this._render_thumbs.call(this, _thumbs_style, _thumbs_item_style) }
      </div>
    )
  }
}

export default Carousel
<|endoftext|>


<commit_before>(function(window, document, undefined) {
    window.onload = init;
    function init() {
        //get the canvas
        var canvas = document.getElementById("mapcanvas");
        var c = canvas.getContext("2d");
        var cwidth = 960;
        var cheight = 540;
        document.getElementById("paintbtn").onclick = paint;

        function reset(){
            c.clearRect ( 0 , 0 , canvas.width, canvas.height );
            c.beginPath();
        }

        function paint(){
            reset();

            var widthSelect = document.getElementById("width");
            var pathWidth = widthSelect.options[widthSelect.selectedIndex].value;

            var heightSelect = document.getElementById("height");
            var pathHeight = heightSelect.options[heightSelect.selectedIndex].value;

            alert(pathWidth + ", " + pathHeight);

            for(var i = 0; i< pathWidth; i++){
                for(var j = 0; i< pathHeight; j++){
                    c.lineWidth = 1;
                    c.rect(i*30, j*30, 30, 30);
                    c.stroke();
                }
            }
        }
    }
})(window, document, undefined);
<commit_msg>Update script.js<commit_after>(function(window, document, undefined) {
    window.onload = init;
    function init() {
        //get the canvas
        var canvas = document.getElementById("mapcanvas");
        var c = canvas.getContext("2d");
        var cwidth = 960;
        var cheight = 540;
        document.getElementById("paintbtn").onclick = paint;

        function reset(){
            c.clearRect ( 0 , 0 , canvas.width, canvas.height );
            c.beginPath();
        }

        function paint(){
            reset();

            var widthSelect = document.getElementById("width");
            var pathWidth = widthSelect.options[widthSelect.selectedIndex].value;

            var heightSelect = document.getElementById("height");
            var pathHeight = heightSelect.options[heightSelect.selectedIndex].value;

            alert(pathWidth + ", " + pathHeight);

            for(var i = 0; i< pathWidth; i++){
                for(var j = 0; j< pathHeight; j++){
                    c.lineWidth = 1;
                    c.rect(i*30, j*30, 30, 30);
                    c.stroke();
                }
            }
        }
    }
})(window, document, undefined);
<|endoftext|>


<commit_before>import FetchStreamLoader from './fetch-stream-loader.js';

class IOController {

    constructor(url) {
        // TODO: determine stash buffer size according to network speed dynamically
        this._chunkStash = new ArrayBuffer(1024 * 1024 * 2);  // 2MB
        this._enableStash = false;
        this._hasTempBuffer = false;
        this._loader = null;

        if (FetchStreamLoader.isSupported()) {
            this._loader = new FetchStreamLoader(this, url);
            this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
        } else {
            throw 'Your browser doesn\'t support fetch api!';
        }
    }

    get onDataArrival() {
        return this._onDataArrival;
    }

    set onDataArrival(callback) {
        if (typeof callback !== 'function') {
            throw 'onDataArrival must be a callback function!';
        }

        this._onDataArrival = callback;
    }

    hasTempBuffer() {
        return this._hasTempBuffer;
    }

    openStream() {
        this._loader.open();
    }

    abortStream() {
        this._loader.requestAbort();
    }

    seek(bytes) {
        // TODO: seek
        // flush stash buffer
        // set flag to drop later received chunks
        // Re-create loader instance with range param
        // clear flag
    }

    get status() {
        return this._loader._status;
    }

    _onLoaderChunkArrival(chunk, byteStart, receivedLength) {
        // TODO: check/stash/ret_check
        if (this._onDataArrival) {
            this._onDataArrival(chunk, byteStart, receivedLength);
        }
    }

}

export default IOController;<commit_msg>IOController: Add chunk stash & seek logical<commit_after>import {LoaderStatus} from './loader.js';
import FetchStreamLoader from './fetch-stream-loader.js';
import MozChunkedLoader from './xhr-moz-chunked-loader.js';

// Manage IO Loaders
class IOController {

    // TODO: events. callbacks or EventEmitter?

    constructor(url) {
        // TODO: determine stash buffer size according to network speed dynamically
        this._stashBuffer = new ArrayBuffer(1024 * 1024 * 2);  // initial size: 2MB
        this._stashUsed = 0;
        this._stashSize = 1024 * 1024 * 2;
        this._stashByteStart = 0;
        this._enableStash = false;
        this._loader = null;
        this._loaderClass = null;
        this._url = url;
        this._currentSegment = null;
        this._progressSegments = [];

        this._selectLoader();
        this._createLoader();
    }

    destroy() {
        if (this._loader.isWorking()) {
            this._loader.abort();
        }
        this._loader.destroy();
        this._loader = null;
        this._loaderClass = null;
        this._stashBuffer = null;
        this._stashUsed = this._stashSize = this._stashByteStart = 0;
        this._enableStash = false;
        this._currentSegment = null;
        this._progressSegments = null;

        this._onDataArrival = null;
    }

    get status() {
        return this._loader.status;
    }

    // prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number
    get onDataArrival() {
        return this._onDataArrival;
    }

    set onDataArrival(callback) {
        if (typeof callback !== 'function') {
            throw 'onDataArrival must be a callback function!';
        }

        this._onDataArrival = callback;
    }

    get stashBufferEnabled() {
        return this._enableStash;
    }

    set stashBufferEnabled(enable) {
        this._enableStash = enable;
    }

    _selectLoader() {
        if (FetchStreamLoader.isSupported()) {
            this._loaderClass = FetchStreamLoader;
        } else if (MozChunkedLoader.isSupported()) {
            this._loaderClass = MozChunkedLoader;
        } else {
            throw 'Your browser doesn\'t support streaming!';
        }
    }

    _createLoader() {
        this._loader = new this._loaderClass();
        this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
        this._loader.onComplete = this._onLoaderComplete.bind(this);
        this._loader.onError = this._onLoaderError.bind(this);
        console.log('Created loader: ' + this._loader.type);  // FIXME
    }

    open() {
        this._currentSegment = {from: 0, to: -1};
        this._progressSegments = [];
        this._progressSegments.push(this._currentSegment);
        this._loader.open(this._url, {from: 0, to: -1});
    }

    abort() {
        this._loader.abort();
    }

    seek(bytes) {
        if (this._loader.isWorking()) {
            this._loader.abort();
        }
        this._stashUsed = 0;
        this._stashByteStart = 0;

        this._loader.destroy();
        this._loader = null;

        console.log('segments before seek: ' + JSON.stringify(this._progressSegments));

        let segments = this._progressSegments;
        let range = {from: bytes, to: -1};
        let bufferedArea = false;
        let insertIndex = 0;

        for (let i = 0; i < segments.length; i++) {
            if (bytes >= segments[i].from && bytes <= segments[i].to) {
                bufferedArea = true;
                break;
            }

            if (i === segments.length - 1) {
                insertIndex = segments.length;
                break;
            }

            if (bytes > segments[i].to && bytes < segments[i + 1].from) {
                range.to = segments[i + 1].from - 1;
                insertIndex = i + 1;
                break;
            }
        }

        if (bufferedArea) {
            throw 'IOController: Seek target position has been buffered!';
        }

        this._currentSegment = {from: range.from, to: range.to};
        segments.splice(insertIndex, 0, this._currentSegment);

        console.log('segments after seek: ' + JSON.stringify(this._progressSegments));

        this._createLoader();
        this._loader.open(this._url, range);
    }

    _expandBuffer(expectedBytes) {
        let stashNewSize = this._stashSize;
        while (stashNewSize < expectedBytes) {
            stashNewSize *= 2;
        }

        if (stashNewSize === this._stashSize) {
            return;
        }

        let stashNewBuffer = new ArrayBuffer(stashNewSize);

        if (this._stashUsed > 0) {  // copy existing data into new buffer
            let stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed);
            let stashNewArray = new Uint8Array(stashNewBuffer, 0, stashNewSize);
            stashNewArray.set(stashOldArray, 0);
        }

        this._stashBuffer = stashNewBuffer;
        this._stashSize = stashNewSize;
        console.log('expandBuffer: targetSize = ' + this._stashSize);
    }

    _dispatchChunks(chunks, byteStart) {
        console.log('_dispatchChunks: chunkSize = ' + chunks.byteLength + ', byteStart = ' + byteStart);
        this._currentSegment.to = byteStart + chunks.byteLength - 1;
        return this._onDataArrival(chunks, byteStart);
    }

    _onLoaderChunkArrival(chunk, byteStart, receivedLength) {
        if (!this._onDataArrival) {
            throw 'IOController: No existing consumer (onDataArrival) callback!';
        }

        if (!this._enableStash) {
            if (this._stashUsed === 0) {
                // send chunk directly to consumer;
                // check ret value (consumed bytes) and stash unconsumed to stashBuffer
                let consumed = this._dispatchChunks(chunk, byteStart);
                if (consumed < chunk.byteLength) {  // unconsumed data remain.
                    let remain = chunk.byteLength - consumed;
                    if (remain > this._stashSize) {
                        this._expandBuffer(remain);
                    }
                    let stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
                    stashArray.set(new Uint8Array(chunk, consumed), 0);
                    this._stashUsed += remain;
                    this._stashByteStart = byteStart + consumed;
                }
            } else {
                // else: Merge chunk into stashBuffer, and send stashBuffer to consumer.
                if (this._stashUsed + chunk.byteLength > this._stashSize) {
                    this._expandBuffer(this._stashUsed + chunk.byteLength);
                }
                let stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
                stashArray.set(chunk, this._stashUsed);
                this._stashUsed += chunk.byteLength;
                let consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart);
                if (consumed < this._stashUsed && consumed > 0) {  // unconsumed data remain
                    let remainArray = new Uint8Array(this._stashBuffer, consumed);
                    stashArray.set(remainArray, 0);
                    this._stashUsed = remainArray.byteLength;
                    this._stashByteStart = byteStart + consumed;
                }
            }
        } else {  // enable stash
            if (this._stashUsed === 0 && this._stashByteStart === 0) {  // seeked? or init chunk?
                // This is the first chunk after seek action
                this._stashByteStart = byteStart;
            }
            let stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
            if (this._stashUsed + chunk.byteLength <= this._stashSize) {
                stashArray.set(chunk, this._stashUsed);
                this._stashUsed += chunk.byteLength;
            } else {  // stashUsed + chunkSize > stashSize
                if (this._stashUsed > 0) {
                    let buffer = this._stashBuffer.slice(0, this._stashUsed);
                    let consumed = this._dispatchChunks(buffer, this._stashByteStart);
                    if (consumed < buffer.byteLength) {
                        if (consumed > 0) {
                            let remainArray = new Uint8Array(buffer, consumed);
                            stashArray.set(remainArray, 0);
                            this._stashUsed = remainArray.byteLength;
                            this._stashByteStart += consumed;
                        }
                    } else {
                        this._stashUsed = 0;
                        this._stashByteStart += consumed;
                    }
                    if (this._stashUsed + chunk.byteLength > this._stashSize) {
                        this._expandBuffer(this._stashUsed + chunk.byteLength);
                        stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
                    }
                    stashArray.set(chunk, this._stashUsed);
                    this._stashUsed += chunk.byteLength;
                } else {  // stash buffer empty, but chunkSize > stashSize (oh, holy shit)
                    // dispatch directly and stash remain data
                    let consumed = this._dispatchChunks(chunk, byteStart);
                    if (consumed < chunk.byteLength) {
                        let remain = chunk.byteLength - consumed;
                        if (remain > this._stashSize) {
                            this._expandBuffer(remain);
                            stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);
                        }
                        stashArray.set(new Uint8Array(chunk, consumed), 0);
                        this._stashUsed += remain;
                        this._stashByteStart = byteStart + consumed;
                    }
                }
            }
        }

        // TODO: network average speed statistics, stash buffer size adjustments
    }

    _onLoaderComplete(from, to) {
        console.log('IOController: loader complete, from = ' + from + ', to = ' + to);
        console.log(JSON.stringify(this._progressSegments));

        let segments = this._progressSegments;
        for (let i = 0; i < segments.length; i++) {
            if (segments[i].from === from && segments[i].to <= to) {
                segments[i].to = to;
                if (i === segments.length - 1) {
                    break;
                } else if (segments[i + 1].from === to + 1) {
                    // Merge connected segments
                    segments[i].to = segments[i + 1].to;
                    segments.splice(i + 1, 1);
                }
                break;
            }
        }
        console.log('Adjusted segments: ' + JSON.stringify(this._progressSegments));

        // Force-flush stash buffer
        if (this._stashUsed > 0) {
            let buffer = this._stashBuffer.slice(0, this._stashUsed);
            let consumed = this._dispatchChunks(buffer, this._stashByteStart);
            if (consumed < buffer.byteLength) {
                let remain = buffer.byteLength - consumed;
                console.warn('IOController: ' + remain + ' bytes unconsumed data remain when loader completed');
            }
            this._stashUsed = 0;
            this._stashByteStart += buffer.byteLength;
        }

        // TODO: +1s
    }

    _onLoaderError(type, data) {
        // TODO: http reconnect or throw out error.
        // Allow user to re-assign segment url for retrying (callback -> Flv)
        console.log('IOController: loader error, code = ' + data.code + ', msg = ' + data.msg);
    }

}

export default IOController;<|endoftext|>