jsoneditor/app/web/fileretriever.js

527 lines
18 KiB
JavaScript

/**
* @file fileretriever.js
*
* FileRetriever manages client side loading and saving of files.
* It requires a server script (fileretriever.php). Loading and saving
* files is done purely clientside using HTML5 techniques when supported
* by the browser.
*
* Requires ajax.js and optionally hash.js.
*
* Supported browsers: Chrome, Firefox, Opera, Safari,
* Internet Explorer 8+.
*
* Example usage:
* var retriever = new FileRetriever({
* 'serverUrl': 'fileretriever.php'
* });
* retriever.loadFile(function (err, data) {
* console.log('file loaded:', data);
* });
* retriever.loadUrl(function (err, data) {
* console.log('url loaded:', data);
* });
* retriever.saveFile("some text");
*
* @constructor FileRetriever
* @param {String} options Available options:
* {string} serverUrl Server side script for
* handling files, for
* example "fileretriever.php"
* {Number} [maxSize] Maximum allowed file size
* in bytes. (this should
* be the same as maximum
* size allowed by the server
* side script). Default is
* 1024 * 1024 bytes.
* {Boolean} [html5] Use HTML5 solutions
* to load/save files when
* supported by the browser.
* True by default.
* {Boolean} [hash] Use hash to store the last opened
* filename or url. True by default.
*
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Copyright (c) 2012 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong, <wjosdejong@gmail.com>
* @date 2012-10-31
*/
var FileRetriever = function (options) {
// set options and variables
options = options || {};
this.options = {
maxSize: ((options.maxSize != undefined) ? options.maxSize : 1024 * 1024),
html5: ((options.html5 != undefined) ? options.html5 : true)
};
this.scriptUrl = options.scriptUrl || 'fileretriever.php';
this.defaultFilename = 'document.json';
this.loadCallback = function () {};
this.dom = {};
var me = this;
if (options.hash !== false) {
if (window.Hash) {
this.hash = new Hash();
}
else {
console.log('WARNING: missing resource hash.js');
}
}
// make an element invisible
function hide(elem) {
elem.style.visibility = 'hidden';
elem.style.position = 'absolute';
elem.style.left = '-1000px';
elem.style.top = '-1000px';
elem.style.width = '0';
elem.style.height = '0';
}
// create an iframe to save a file
var downloadIframe = document.createElement('iframe');
hide(downloadIframe);
document.body.appendChild(downloadIframe);
this.dom.downloadIframe = downloadIframe;
// create an iframe for uploading files
// the iframe must have an unique name, allowing multiple
// FileRetrievers. The name is needed as target for the uploadForm
var uploadIframe = document.createElement('iframe');
uploadIframe.name = 'fileretriever-upload-' + Math.round(Math.random() * 1E15);
hide(uploadIframe);
document.body.appendChild(uploadIframe);
uploadIframe.onload = function () {
// when a downloaded file is retrieved, send a callback with
// the retrieved data
var id = uploadIframe.contentWindow.document.body.innerHTML;
var url = me.scriptUrl + '?id=' + id + '&filename=' + me.getFilename();
//console.log('uploadIframe.load ', id, ' ', url)
ajax.get(url, function (data, status) {
//console.log('ajax.get ', url, ' ', data, ' ', status);
if (status == 200) {
me.loadCallback(null, data);
}
else {
//console.log('Error loading file ' + url, status, data);
var err = new Error('Error loading file ' + me.getFilename());
me.loadCallback(err, null);
}
});
};
this.dom.uploadIframe = uploadIframe;
// create a form to upload a file
var uploadForm = document.createElement('form');
uploadForm.action = this.scriptUrl;
uploadForm.method = 'POST';
uploadForm.enctype = 'multipart/form-data';
uploadForm.target = uploadIframe.name;
hide(uploadForm);
this.dom.form = uploadForm;
var domFile = document.createElement('input');
domFile.type = 'file';
domFile.name = 'file';
domFile.onchange = function () {
setTimeout(function () { // Timeout needed for IE
var filename = domFile.value;
//console.log('load file:' + filename + '.');
if (filename.length) {
if (me.options.html5 && window.File && window.FileReader) {
// load file via HTML5 FileReader (no size limits)
var file = domFile.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var data = event.target.result;
me.loadCallback(null, data);
};
// Read in the image file as a data URL.
reader.readAsText(file);
}
else {
// load by uploading to server
// TODO: how to check the file size? (on older browsers)
//console.log('submitting...');
uploadForm.submit();
}
}
else {
// cancel
me.loadCallback(null, null);
}
}, 0);
};
uploadForm.appendChild(domFile);
this.dom.file = domFile;
document.body.appendChild(uploadForm);
};
/**
* Delete all HTML DOM elements created by the FileRetriever.
* The FileRetriever cannot be used after its DOM elements are deleted.
*/
FileRetriever.prototype.remove = function () {
var dom = this.dom;
for (var prop in dom) {
if (dom.hasOwnProperty(prop)) {
var elem = dom[prop];
if (elem.parentNode) {
elem.parentNode.removeChild(elem);
}
}
}
this.dom = {};
};
/**
* get a filename from a path or url.
* For example "http://site.com/files/example.json" will return "example.json"
* @param {String} path A filename, path, or url
* @return {String} filename
* @private
*/
FileRetriever.prototype._getFilename = function (path) {
// http://stackoverflow.com/a/423385/1262753
return path ? path.replace(/^.*[\\\/]/, '') : '';
};
/**
* Set the last url
* @param {String} url
*/
FileRetriever.prototype.setUrl = function (url) {
this.url = url;
if (this.hash) {
this.hash.setValue('url', url);
}
};
/**
* Get last filename
* @return {String} filename
*/
FileRetriever.prototype.getFilename = function () {
if (this.hash) {
var url = this.hash.getValue('url');
if (url) {
return this._getFilename(url) || this.defaultFilename;
}
}
return this.defaultFilename;
};
/**
* Get the last url
* @return {String | undefined} url
*/
FileRetriever.prototype.getUrl = function () {
if (this.hash) {
var url = this.hash.getValue('url');
if (url) {
this.url = url;
}
}
return this.url;
};
/**
* Remove last url from hash
*/
FileRetriever.prototype.removeUrl = function () {
if (this.hash) {
var url = this.hash.removeValue('url');
}
};
/**
* Load a url
* @param {String} url The url to be retrieved
* @param {function} callback Callback method, called with parameters:
* {Error} error
* {string} data
*/
FileRetriever.prototype.loadUrl = function (url, callback) {
// set current filename (will be used when saving a file again)
this.setUrl(url);
// try to fetch to the url directly (may result in a cross-domain error)
var scriptUrl = this.scriptUrl;
ajax.get(url, function(data, status) {
if (status == 200) {
// success. great. no cross-domain error
callback(null, data);
}
else {
// cross-domain error (or other). retrieve the url via the server
var indirectUrl = scriptUrl + '?url=' + encodeURIComponent(url);
var err;
ajax.get(indirectUrl, function(data, status) {
if (status == 200) {
callback(null, data);
}
else if (status == 404) {
console.log('Error: url "' + url + '" not found', status, data);
err = new Error('Error: url "' + url + '" not found');
callback(err, null);
}
else {
console.log('Error: failed to load url "' + url + '"', status, data);
err = new Error('Error: failed to load url "' + url + '"');
callback(err, null);
}
});
}
});
};
/**
* Load a file from disk.
* A file explorer will be opened to select a file and press ok.
* In case of Internet Explorer, an upload form will be shown where the
* user has to select a file via a file explorer after that click load.
* @param {function} callback Callback method, called with parameters:
* {Error} error
* {string} data
*/
FileRetriever.prototype.loadFile = function (callback) {
this.removeUrl();
var isIE = (navigator.appName == 'Microsoft Internet Explorer');
if (!isIE) {
// immediate file upload
this.loadCallback = callback || function () {};
this.dom.file.click();
}
else {
// immediate file upload not supported in IE thanks to all the
// security limitations. A form is needed with a manual submit.
this.loadFileDialog(callback);
}
};
/**
* Show a dialog to select and load a file.
* Needed to load files on Internet Explorer.
* @param {function} callback Callback method, called with parameters:
* {Error} error
* {String} data
*/
FileRetriever.prototype.loadFileDialog = function (callback) {
this.removeUrl();
this.loadCallback = callback;
this.prompt({
title: 'Open file',
titleSubmit: 'Open',
inputType: 'file',
inputName: 'file',
formAction: this.scriptUrl,
formMethod: 'POST',
formTarget: this.dom.uploadIframe.name
});
};
/**
* Show a dialog to select and load an url.
* @param {function} callback Callback method, called with parameters:
* {Error} error
* {String} data
*/
FileRetriever.prototype.loadUrlDialog = function (callback) {
var me = this;
this.prompt({
title: 'Open url',
titleSubmit: 'Open',
inputType: 'text',
inputName: 'url',
inputDefault: this.getUrl(),
callback: function (url) {
if (url) {
me.loadUrl(url, callback);
}
}
});
};
/**
* Show a prompt.
* The propmt can either:
* - Post a form when formTarget, formAction, and formMethod are provided
* - Call the callback method "callback" with the entered value as parameter.
* This happens when a callback parameter is provided.
* @param {Object} params Available parameters:
* {String} title
* {String} titleSubmit
* {String} titleCancel
* {String} inputType
* {String} inputName
* {String} inputDefault
* {String} formTarget
* {String} formAction
* {String} formMethod
* {function} callback
*/
FileRetriever.prototype.prompt = function (params) {
var removeDialog = function () {
// remove the form
if (background.parentNode) {
background.parentNode.removeChild(background);
}
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
JSONEditor.Events.removeEventListener(document, 'keydown', onKeyDown);
};
var onCancel = function () {
removeDialog();
if(params.callback) {
params.callback(null);
}
};
var onKeyDown = JSONEditor.Events.addEventListener(document, 'keydown', function (event) {
event = event || window.event;
var keynum = event.which || event.keyCode;
if (keynum == 27) { // ESC
onCancel();
JSONEditor.Events.preventDefault(event);
JSONEditor.Events.stopPropagation(event);
}
});
var overlay = document.createElement('div');
overlay.className = 'fileretriever-overlay';
document.body.appendChild(overlay);
var form = document.createElement('form');
form.className = 'fileretriever-form';
form.target = params.formTarget || '';
form.action = params.formAction || '';
form.method = params.formMethod || 'POST';
form.enctype = 'multipart/form-data';
form.encoding = 'multipart/form-data'; // needed for IE8 and older
form.onsubmit = function () {
if (field.value) {
setTimeout(function () {
// remove after the submit has taken place!
removeDialog();
}, 0);
if (params.callback) {
params.callback(field.value);
return false;
}
else {
return true;
}
}
else {
alert('Enter a ' + params.inputName + ' first...');
return false;
}
};
var title = document.createElement('div');
title.className = 'fileretriever-title';
title.appendChild(document.createTextNode(params.title || 'Dialog'));
form.appendChild(title);
var field = document.createElement('input');
field.className = 'fileretriever-field';
field.type = params.inputType || 'text';
field.name = params.inputName || 'text';
field.value = params.inputDefault || '';
var contents = document.createElement('div');
contents.className = 'fileretriever-contents';
contents.appendChild(field);
form.appendChild(contents);
var cancel = document.createElement('input');
cancel.className = 'fileretriever-cancel';
cancel.type = 'button';
cancel.value = params.titleCancel || 'Cancel';
cancel.onclick = onCancel;
var submit = document.createElement('input');
submit.className = 'fileretriever-submit';
submit.type = 'submit';
submit.value = params.titleSubmit || 'Ok';
var buttons = document.createElement('div');
buttons.className = 'fileretriever-buttons';
buttons.appendChild(cancel);
buttons.appendChild(submit);
form.appendChild(buttons);
var border = document.createElement('div');
border.className = 'fileretriever-border';
border.appendChild(form);
var background = document.createElement('div');
background.className = 'fileretriever-background';
background.appendChild(border);
document.body.appendChild(background);
field.focus();
field.select();
};
/**
* Save data to disk
* @param {String} data
* @param {function} [callback] Callback when the file is saved, called
* with parameter:
* {Error} error
*/
FileRetriever.prototype.saveFile = function (data, callback) {
callback = callback || function () {};
// create an anchor to save files to disk (if supported by the browser)
var a = document.createElement('a');
if (this.options.html5 && a.download != undefined) {
// save file directly using a data URL
a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(data);
a.download = this.getFilename();
a.click();
callback()
}
else {
// save file by uploading it to the server and then downloading
// it via an iframe
var me = this;
if (data.length < this.options.maxSize) {
ajax.post(me.scriptUrl, data, function(id, status) {
if (status == 200) {
var iframe = me.dom.downloadIframe;
iframe.src = me.scriptUrl + '?id=' + id + '&filename=' + me.getFilename();
callback();
// TODO: give a callback after the file is saved (iframe load?), not before
}
else {
callback(new Error('Error saving file'));
}
});
}
else {
callback(new Error('Maximum allowed file size exceeded (' +
this.options.maxSize + ' bytes)'));
}
}
};