/** * @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, * @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)')); } } };