Source: html/kekule.nativeServices.js

/**
 * @fileoverview
 * Classes and functions related with native services (e.g., file picker dialog).
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /core/kekule.common.js
 * requires /widget/kekule.widget.base.js
 * requires /xbrowsers/kekule.x.js
 */

(function(){
"use strict";

/**
 * The class to call native services (e.g., open file picker dialog).
 * @object
 */
Kekule.NativeServices = {
	/**
	 * Mode const to launch a open file dialog.
	 * @const
	 */
	MODE_OPEN: 'open',
	/**
	 * Mode const to launch a save file dialog.
	 * @const
	 */
	MODE_SAVE: 'save',
	/**
	 * A special file filter that list all file extension set.
	 * @const
	 */
	FILTER_ALL_SUPPORT: 'all',
	/**
	 * A special file filter that list any type of file.
	 * @const
	 */
	FILTER_ANY: 'any',
	/**
	 * Open a file picker dialog.
	 * @param {HTMLDocument} doc Current document.
	 * @param {Func} callback Callback function when dialog is closed, with params (result, file, files).
	 *   If the dialog closed with "OK" button, result should be true and selected file instance(s) will
	 *   be filled in file and files object.
	 * @param {Hash} options Additional options hash, can include:
	 *   {
	 *     mode: String, dialog mode, default is 'open'.
	 *     initialFileName: String,
	 *     filters: Array of filters of dialog, each item is a hash {title, filter},
	 *       e.g. {title: 'Mol2000 file', filter: '.mol'}.
	 *       filter file extension can be a string delimitered by ',', e.g., '.mol,.rd'
	 *     multiple: Bool, whether multi select is allowed.
	 *   }
	 */
	showFilePickerDialog: function(doc, callback, options)
	{
		if (Kekule.NativeServices.doShowFilePickerDialog)
			return Kekule.NativeServices.doShowFilePickerDialog(doc, callback, options);
	},
	/**
	 * Returns whether current service can load data from file.
	 * @param {HTMLDocument} doc
	 * @returns {Bool}
	 */
	canLoadFileData: function(doc)
	{
		return !!KNS.doLoadFileData || !!KNS.doShowFilePickerDialog;
	},
	/**
	 * Show an open file dialog and load data from a file.
	 * @param {HTMLDocument} doc Current document.
	 * @param {Func} callback Callback function when dialog is closed, with param (result, data, fileName).
	 *   If the dialog closed with "Cancel" button, result will be false and data/fileName will be null.
	 * @param {Hash} options Additional options hash, can include:
	 *   {
	 *     initialFileName: String,
	 *     filters: Array of filters of dialog, each item is a hash {title, filter},
	 *       e.g. {title: 'Mol2000 file', filter: '*.mol'}.
	 *     binaryDetector: Func(fileName, file)=>Bool, a function to detect whether current file is in binary mode.
	 *     If this function is not provided, the file will always be read in text mode.
	 *   }
	 */
	loadFileData: function(doc, callback, options)
	{
		if (Kekule.NativeServices.doLoadFileData)
			return Kekule.NativeServices.doLoadFileData(doc, callback, options);

		// else a default implementation using FileReader
		var ops = Object.create(options || {});
		ops.multiple = false;
		ops.mode = KNS.MODE_OPEN;
		var done = function(result, file, files)
		{
			if (!result || !file)  // dialog cancelled
			{
				callback(result, null, null);
			}
			else  // file selected
			{
				if (Kekule.BrowserFeature.fileapi)
				{
					var fileName = file.name;
					var isBinary = ops.binaryDetector && ops.binaryDetector(fileName, file);

					// try open it the file by FileReader
					var reader = new FileReader();
					reader.onload = function(e)
					{
						var content = reader.result;
						/*
						var info = chemObj.getSrcInfo();
						info.fileName = fileName;
						var success = !!chemObj;
						*/
						//console.log('load success', fileName, content);
						if (callback)
							callback(true, content, fileName);
					};
					reader.onerror = function()
					{
						if (callback)
							callback(false);
						Kekule.error(Kekule.$L('ErrorMsg.ERROR_LOADING_FILE' + fileName));
					}

					if (isBinary)
					//reader.readAsBinaryString(file);
						reader.readAsArrayBuffer(file);
					else
						reader.readAsText(file);
				}
				else
				{
					Kekule.error(/*Kekule.ErrorMsg.FILE_API_NOT_SUPPORTED*/Kekule.$L('ErrorMsg.FILE_API_NOT_SUPPORTED'));
				}
			}
		};
		// open file picker
		KNS.showFilePickerDialog(doc, done, ops);
	},
	/**
	 * Returns whether current service can save data to file.
	 * @param {HTMLDocument} doc
	 * @returns {Bool}
	 */
	canSaveFileData: function(doc)
	{
		return !!KNS.doSaveFileData;
	},
	/**
	 * Show an save file dialog and save data to a file.
	 * @param {HTMLDocument} doc Current document.
	 * @param {Variant} data to be saved
	 * @param {Func} callback Callback function when dialog is closed, with param (result, fileName).
	 *   If the dialog closed with "Cancel" button, result will be false and fileName will be null.
	 * @param {Hash} options Additional options hash, can include:
	 *   {
	 *     initialFileName: String,
	 *     filters: Array of filters of dialog, each item is a hash {title, filter},
	 *       e.g. {title: 'Mol2000 file', filter: '*.mol'}.
	 *     binary: Bool, whether the data is in binary format. Default is false.
	 *   }
	 */
	saveFileData: function(doc, data, callback, options)
	{
		if (Kekule.NativeServices.doSaveFileData)
			return Kekule.NativeServices.doSaveFileData(doc, data, callback, options);
	}
};
var KNS = Kekule.NativeServices;

/**
 * Default implementation of some native services functions based on HTML.
 * @object
 * @ignore
 */
Kekule.HtmlNativeServiceImpl = {
	/** @ignore */
	doShowFilePickerDialog: function(doc, callback, options)
	{
		//console.log('showFilePickerDialog', options);
		var elem = Kekule.HtmlNativeServiceImpl._createFileInputElem(doc, callback, options || {});
		elem.click();
	},
	/** @private */
	_createFileInputElem: function(doc, callback, options)
	{
		var result = doc.createElement('input');
		result.setAttribute('type', 'file');
		var ops = options || {};
		if (ops.multiple)
			result.setAttribute('multiple', 'multiple');
		if (ops.filters)
		{
			var filterValues = [];
			for (var i = 0, l = ops.filters.length; i < l; ++i)
			{
				var filterItem = ops.filters[i];
				if (filterItem === Kekule.NativeServices.FILTER_ALL_SUPPORT
					|| filterItem === Kekule.NativeServices.FILTER_ANY)   // these special filters need not to be handled in HTML
					continue;
				var sfilter = filterItem.filter;
				if (sfilter)
				{
					filterValues.push(sfilter);
				}
			}
			var sAllFilter = filterValues.join(',');
			result.setAttribute('accept', sAllFilter);
			//console.log(sFilter);
		}
		// IMPORTANT: some browser need this input file element visible to raise the open dialog
		// so we append it to document and "hidden" it
		var style = result.style;
		style.position = 'absolute';
		style.left = '-100000px';
		style.opacity = 0;

		doc.body.appendChild(result);
		//result.onchange = this.reactInputChangeBind;

		var reactChange = function(e)
		{
			var target = e.getTarget();
			var files = target.files;
			var firstFile = files && files[0];
			//console.log('file input change', target.files);
			if (callback)
				callback(true, firstFile, files);

			// dismiss input element
			Kekule.X.Event.removeListener(target, 'change', reactChange);
			target.ownerDocument.body.focus();
			if (target.parentNode)
			{
				//target.parentNode.removeChild(target);
			}
		};

		Kekule.X.Event.addListener(result, 'change', reactChange/*this.reactInputChangeBind*/);
		return result;
	},

	/** @ignore */
	doSaveFileData: function(doc, data, callback, options)
	{
		var fileName = options.initialFileName || null;
		//if (Kekule.BrowserFeature.downloadHref)
		{
			var dataElem = Kekule.HtmlNativeServiceImpl._createDataElem(doc, data, fileName);
			dataElem.click();  // save file dialog
			dataElem.parentNode.removeChild(dataElem);
		}
		/*
		else // IE, use execCommand to save
		{
			Kekule.HtmlNativeServiceImpl.doSaveFileDataIe(doc, data, fileName);
		}
		*/
		if (callback)
			callback(null, null);  // can not determine the final file name and result.
	},
	/** @private */
	_createDataElem: function(doc, data, fileName)
	{
		var elem = doc.createElement('a');
		var sHref= 'data:application/octet-stream,' + encodeURIComponent(data);
		elem.setAttribute('href', sHref);
		elem.setAttribute('download', fileName || '');
		//elem.innerHTML = 'dasdsdsadasds';
		//window.open(sHref);

		var style = elem.style;
		elem.position = 'absolute';
		elem.left = '-100000px';
		elem.opacity = 0;

		doc.body.appendChild(elem);
		return elem;
	},
	doSaveFileDataIe: function(doc, data, callback, options)
	{
		var fileName = options.initialFileName || null;
		//console.log('save ie');
		if (Kekule.BrowserFeature.blob && window.navigator.msSaveOrOpenBlob)  // IE 10 and above, use blob to save
		{
			var blobObject = new Blob([data]);
			window.navigator.msSaveBlob(blobObject, fileName);
		}
		else
		{
			// data must be string
			var iframeElem = Kekule.HtmlNativeServiceImpl._createAndExecDataIframe(doc, data, fileName);
			// after save remove iframe
			iframeElem.parentNode.removeChild(iframeElem);
		}
		if (callback)
			callback(null, null);  // can not determine the final file name and result.
	},
	/** @private */
	_createAndExecDataIframe: function(doc, data, fileName)
	{
		//console.log('iframe save');
		var result = doc.createElement('iframe');
		//result.style.display = 'none';
		doc.body.appendChild(result);
		var iframe = result.contentWindow || result.contentDocument;
		iframe.document.open("text/html", "replace");
		var s = data.toString();
		iframe.document.write(s);
		iframe.document.close();
		iframe.focus();
		iframe.document.execCommand('SaveAs', true, Kekule.UrlUtils.extractFileCoreName(fileName));  // extension IE don't know will supress the save dialog
		//iframe.document.execCommand('SaveAs', true, fileName);
		return result;
	}
};

// register
if (Kekule.BrowserFeature.fileapi)
	KNS.doShowFilePickerDialog = Kekule.HtmlNativeServiceImpl.doShowFilePickerDialog;
if (Kekule.Browser.IE || window.navigator.msSaveOrOpenBlob)
	KNS.doSaveFileData = Kekule.HtmlNativeServiceImpl.doSaveFileDataIe;
else if (Kekule.BrowserFeature.downloadHref)
	KNS.doSaveFileData = Kekule.HtmlNativeServiceImpl.doSaveFileData;

})();