- Implemented load/save functionality.

- Rearranged the code.
- Updated the build file accordingly
This commit is contained in:
Jos de Jong 2012-10-31 22:16:07 +01:00
parent f8a5aa472d
commit 18d56db063
33 changed files with 1905 additions and 497 deletions

36
app/web/ajax.js Normal file
View File

@ -0,0 +1,36 @@
/**
* ajax
* Utility to perform ajax get and post requests. Supported browsers:
* Chrome, Firefox, Opera, Safari, Internet Explorer 7+.
*/
var ajax = (function () {
function fetch (method, url, body, callback) {
try {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
callback(xhr.responseText, xhr.status);
}
};
xhr.open(method, url, true);
xhr.send(body);
}
catch (err) {
callback(err, 0);
}
}
function get (url, callback) {
fetch('GET', url, null, callback);
}
function post (url, body, callback) {
fetch('POST', url, body, callback)
}
return {
'fetch': fetch,
'get': get,
'post': post
}
})();

View File

@ -17,7 +17,9 @@ span.header-light {
#header {
width: 100%;
height: 40px;
/* TODO
overflow: hidden;
*/
background: #4D4D4D url('img/header_background.png');
color: white;
@ -29,6 +31,99 @@ span.header-light {
border: none;
}
#menu {
position: absolute;
top: 5px;
right: 15px;
font-size: 11pt;
}
#menu ul {
list-style: none;
margin: 0;
padding: 0;
clear: both;
}
#menu ul li {
color: #e6e6e6;
background: none;
border: none;
border-right: 1px solid #737373;
height: 30px;
padding: 0;
margin: 0;
float: left;
position: relative;
text-decoration: none;
}
#menu ul li:first-child {
border-left: 1px solid #737373;
}
#menu ul li:hover {
color: white;
background-color: #737373;
}
#menu ul li ul {
display: none;
}
#menu ul li:hover > ul {
display: block;
}
#menu ul li ul {
position: absolute;
top: 30px;
left: 0;
z-index: 999;
background: #f5f5f5;
border: 1px solid lightgray;
box-shadow: 0 0 15px rgba(128, 128, 128, 0.5);
}
#menu ul li ul li {
color: #737373;
background: none;
border: none;
margin: 0;
padding: 0;
}
#menu ul li ul li:first-child {
border-left: none;
}
#menu ul li ul li:hover {
background-color: white;
color: #737373;
}
#menu a {
padding: 6px 10px;
display: block;
cursor: pointer;
}
#menu ul li ul li a {
width: 80px;
}
#openMenuButton {
font-size: 75%;
margin-left: 2px;
}
#menu #open {
cursor: default;
}
/* TODO: enable the menu with keys (when openMenuButton is active) */
#auto {
width: 100%;
height: 100%;
@ -131,14 +226,23 @@ a.adInfo {
text-decoration: underline;
}
div.error {
color: red;
background-color: #FFC0CB;
border: 1px solid red;
div.error, div.notification {
border-radius: 3px;
padding: 5px;
margin: 5px;
box-shadow: 0 0 15px rgba(128, 128, 128, 0.5);
/* TODO: add some transition effect */
}
div.error {
color: red;
background-color: #FFC0CB;
border: 1px solid red;
}
div.notification {
color: #1a1a1a;
background-color: #FFFFAB;
border: 1px solid #e6d600;
}
pre.error {
margin: 0 0 10px 0;
@ -163,9 +267,9 @@ div.convert-right, div.convert-left {
}
div.convert-right {
background: url('../jsoneditor/img/jsoneditor-icons.png') -168px 0;
background: url('lib/jsoneditor/img/jsoneditor-icons.png') -168px 0;
}
div.convert-left {
background: url('../jsoneditor/img/jsoneditor-icons.png') -192px 0;
background: url('lib/jsoneditor/img/jsoneditor-icons.png') -192px 0;
}

364
app/web/app.js Normal file
View File

@ -0,0 +1,364 @@
/**
* @file app.js
*
* @brief
* JSONEditor is an editor to display and edit JSON data in a treeview.
*
* Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
*
* @license
* This json editor is open sourced with the intention to use the editor as
* a component in your own application. Not to just copy and monetize the editor
* as it is.
*
* 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) 2011-2012 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong, <wjosdejong@gmail.com>
* @date 2012-10-31
*/
var editor = null;
var formatter = null;
var app = {};
/**
* Get the JSON from the formatter and load it in the editor
*/
app.formatterToEditor = function() {
try {
editor.set(formatter.get());
}
catch (err) {
app.notifications.showError(err);
}
};
/**
* Get the JSON from the editor and load it into the formatter
*/
app.editorToFormatter = function () {
try {
formatter.set(editor.get());
}
catch (err) {
app.notifications.showError(err);
}
};
/**
* Load the interface (editor, formatter, splitter)
*/
// TODO: split the method load in multiple methods, it is too large
app.load = function() {
//try {
// notification handler
app.notifications = new Notifications();
// retriever for loading/saving files
app.retriever = new FileRetriever({
scriptUrl: 'fileretriever.php'
});
// default json document
var json = {
"name": "John Smith",
"age": 32,
"employed": true,
"address": {
"street": "701 First Ave.",
"city": "Sunnyvale, CA 95125",
"country": "United States"
},
"children": [
{
"name": "Richard",
"age": 7
},
{
"name": "Susan",
"age": 4
},
{
"name": "James",
"age": 3
}
]
};
// load url if hash contains a url
if (window.Hash) {
var hash = new Hash();
var url = hash.getValue('url');
if (url) {
json = {};
app.openUrl(url);
}
}
// formatter
var container = document.getElementById("jsonformatter");
formatter = new JSONFormatter(container);
formatter.set(json);
formatter.onError = function (err) {
app.notifications.showError(err);
};
// editor
container = document.getElementById("jsoneditor");
editor = new JSONEditor(container);
editor.set(json);
// splitter
var domSplitter = document.getElementById('splitter');
app.splitter = new Splitter({
container: domSplitter,
change: function () {
app.resize();
}
});
// button Formatter-to-Editor
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
var toForm = document.createElement('button');
toForm.id = 'toForm';
toForm.title = 'JSON to Editor';
toForm.className = 'convert';
toForm.innerHTML = '<div class="convert-right"></div>';
toForm.onclick = function () {
this.focus();
app.formatterToEditor();
};
domSplitter.appendChild(toForm);
// button Editor-to-Formatter
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
var toJSON = document.createElement('button');
toJSON.id = 'toJSON';
toJSON.title = 'Editor to JSON';
toJSON.className = 'convert';
toJSON.innerHTML = '<div class="convert-left"></div>';
toJSON.onclick = function () {
this.focus();
app.editorToFormatter();
};
domSplitter.appendChild(toJSON);
// web page resize handler
JSONEditor.Events.addEventListener(window, 'resize', app.resize);
// clear button
var domClear = document.getElementById('clear');
domClear.onclick = app.clearFile;
/* TODO: enable clicking on open to execute the default, "open file"
// open button
var domOpen = document.getElementById('open');
var domOpenMenuButton = document.getElementById('openMenuButton');
domOpen.onclick = function (event) {
event = event || window.event; // for IE8
var target = event.target || event.srcElement;
if (target == domOpenMenuButton ||
(event.offsetX > domOpen.offsetWidth - domOpenMenuButton.offsetWidth)) {
// clicked on the menu button
}
else {
app.openFile();
}
};
*/
// menu button open file
var domMenuOpenFile = document.getElementById('menuOpenFile');
domMenuOpenFile.onclick = function (event) {
app.openFile();
JSONEditor.Events.stopPropagation(event);
JSONEditor.Events.preventDefault(event);
};
// menu button open url
var domMenuOpenUrl = document.getElementById('menuOpenUrl');
domMenuOpenUrl.onclick = function (event) {
app.openUrl();
JSONEditor.Events.stopPropagation(event);
JSONEditor.Events.preventDefault(event);
};
// save button
var domSave = document.getElementById('save');
domSave.onclick = app.saveFile;
// TODO: implement a focus method
formatter.textarea.focus();
/* TODO: use checkChange
// TODO: a nicer method to check for changes
var formatterLastContent;
var editorLastContent;
function checkChange () {
try {
// check for change in formatter
var formatterJSON = formatter.get();
var formatterContent = JSON.stringify(formatterJSON);
if (formatterContent != formatterLastContent) {
formatterLastContent = formatterContent;
editorLastContent = formatterContent;
editor.set(formatterJSON);
}
else {
// check for change in editor
var editorJSON = editor.get();
var editorContent = JSON.stringify(editorJSON);
if (editorContent != editorLastContent) {
editorLastContent = editorContent;
formatterLastContent = editorContent;
formatter.set(editorJSON);
}
}
}
catch (err) {
app.notifications.showError(err);
}
setTimeout(checkChange, 1000);
}
checkChange();
*/
// enforce FireFox to not do spell checking on any input field
document.body.spellcheck = false;
//} catch (err) {
// app.notifications.showError(err);
//}
};
/**
* Callback method called when a file or url is opened.
* @param {Error} err
* @param {String} data
*/
app.openCallback = function (err, data) {
if (!err) {
if (data != undefined) {
formatter.setText(data);
try {
var json = JSONEditor.parse(data);
editor.set(json);
}
catch (err) {
editor.set({});
app.notifications.showError(err);
}
}
}
else {
app.notifications.showError(err);
}
};
/**
* Open a file explorer to select a file and open the file
*/
app.openFile = function() {
// TODO: show a 'loading...' notification
app.retriever.loadFile(app.openCallback);
};
/**
* Open a url. If no url is provided as parameter, a dialog will be opened
* to select a url.
* @param {String} [url]
*/
app.openUrl = function (url) {
// TODO: show a 'loading...' notification
if (!url) {
app.retriever.loadUrlDialog(app.openCallback);
}
else {
app.retriever.loadUrl(url, app.openCallback);
}
};
/**
* Open a file explorer to save the file.
*/
app.saveFile = function () {
// TODO: get data from the most recently changed editor: formatter or editor
// TODO: show a 'saving...' notification
var data = formatter.getText();
app.retriever.saveFile(data, function (err) {
if (err) {
app.notifications.showError(err);
}
});
};
/**
* Clear the current file
*/
app.clearFile = function () {
var json = {};
formatter.set(json);
editor.set(json);
app.retriever.removeUrl();
};
app.resize = function() {
var domEditor = document.getElementById('jsoneditor');
var domFormatter = document.getElementById('jsonformatter');
var domSplitter = document.getElementById('splitter');
var domAd = document.getElementById('ad');
var domAdInfo = document.getElementById('adInfo');
var width = window.innerWidth || document.body.offsetWidth || document.documentElement.offsetWidth;
var height = window.innerHeight || document.body.offsetHeight || document.documentElement.offsetHeight;
var adWidth = domAd ? domAd.clientWidth : 0;
var splitterWidth = domSplitter.clientWidth;
if (adWidth) {
width -= (adWidth + 15); // Not so nice, +15 here for the margin
}
var splitterLeft = width * app.splitter.getValue();
// resize formatter
domFormatter.style.width = Math.round(splitterLeft) + 'px';
// resize editor
// the width has a -1 to prevent the width from being just half a pixel
// wider than the window, causing the content elements to wrap...
domEditor.style.left = Math.round(splitterLeft + splitterWidth) + 'px';
domEditor.style.width = Math.round(width - splitterLeft - splitterWidth - 1) + 'px';
// resize ad text
if (domAdInfo && domAd) {
var infoHeight = domAdInfo.clientHeight;
domAd.style.paddingTop = infoHeight + 'px';
}
};
/**
* Hide the advertisement
*/
app.hideAds = function() {
var domAd = document.getElementById("ad");
if (domAd) {
domAd.parentNode.removeChild(domAd);
app.resize();
}
};

47
app/web/datapolicy.txt Normal file
View File

@ -0,0 +1,47 @@
JSON EDITOR ONLINE DATA POLICY
http://jsoneditoronline.org
This file describes the data policy of JSON Editor Online. When using the
open/save functionality of the editor, files may need to be downloaded via the
server, therefore:
DO NOT LOAD OR SAVE SENSITIVE DATA VIA THE EDITORS OPEN/SAVE FUNCTIONALITY.
Files which are downloaded via the server are sent unsecured and unencrypted.
1. Opening files
If the browser in use supports HTML5 FileReader, files are directly loaded
from disk into the editor. The files are not send to the server.
If HTML5 is not supported, the files are first uploaded to the server and then
downloaded by the editor. The files are deleted from the server as soon as
they are downloaded once. If a file is not downloaded for some reason, it will
be deleted from the server after one hour.
2. Saving files
If the browser in use supports HTML5 a.download, files are directly saved
from the browser to disk. The files are not send to the server.
If HTML5 is not supported, the files are first uploaded to the server and then
downloaded to disk. The files are deleted from the server as soon as they are
downloaded once. If a file is not downloaded for some reason, it will be
deleted from the server after one hour.
3. Opening urls
When opening an url, the editor first opens the url directly. If this fails
due to cross-domain restrictions, the url will be retrieved via the server.
In that case, the retrieved data is sent directly to the browser and is not
stored on the server.
4. Cutting/pasting clipboard data
When cutting and pasting text in the editor using the system clipboard, no
data is sent via the server.

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

54
app/web/fileretriever.css Normal file
View File

@ -0,0 +1,54 @@
div.fileretriever-overlay, div.fileretriever-background {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 999;
}
div.fileretriever-overlay {
background-color: gray;
opacity: 0.2;
filter: alpha(opacity = 20);
}
div.fileretriever-border {
width: 410px;
margin: 100px auto;
padding: 20px;
background-color: white;
border: 1px solid gray;
border-radius: 2px;
}
form.fileretriever-form {
}
div.fileretriever-title {
font-weight: bold;
}
div.fileretriever-contents {
margin: 30px 0;
}
div.fileretriever-buttons {
text-align: right;
}
input.fileretriever-field[type="file"] {
width: 400px;
padding: 3px;
}
input.fileretriever-field[type="text"] {
width: 400px;
border: 1px solid lightgray;
border-radius: 2px;
padding: 3px;
}
input.fileretriever-submit, input.fileretriever-cancel {
margin-left: 10px;
}

526
app/web/fileretriever.js Normal file
View File

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

114
app/web/fileretriever.php Normal file
View File

@ -0,0 +1,114 @@
<?php
/**
* Script to load and save files from the Javascript client to disk and url.
*
* Usage:
*
* POST file.php with a JSON document as body
* Will store the JSON document on disk and return the id of the document.
*
* POST file.php with a JSON document with name "file" as body multipart/form-data
* Will store the JSON document on disk and return the id of the document.
*
* GET file.php?url=....
* Will fetch the url and return it (resolves cross-domain security issues)
*
* GET file.php?id=...
* GET file.php?id=...&filename=...
* Will return the file with the id, and remove the file from disk.
* Optionally specify a filename for the download. Default is 'document.json'
*/
// TODO: neatly handle exceeding of the max size
$tmp = 'tmp'; // directory for temporarily storing the files
$method = $_SERVER['REQUEST_METHOD'];
// make temporary directory to store the file (if not existing)
if (!is_dir(getcwd() . '/' . $tmp)) {
mkdir(getcwd() . '/' . $tmp);
}
/**
* Create a filename from given id
* @param {String} id id of the file
* @return {String} filename path to the file
*/
function getFilename($id) {
global $tmp;
return "$tmp/$id";
}
if ($method == 'GET') {
$filename = isset($_GET['filename']) ? $_GET['filename'] : 'document.json';
if (isset($_GET['url'])) {
// download a file from url and return the file
$url = $_GET['url'];
$body = file_get_contents($url);
if ($body != false) {
header("Content-Disposition: attachment; filename=\"$filename\"");
header('Content-type: application/json');
echo $body;
}
else {
header('HTTP/1.1 404 Not Found');
}
}
else if (isset($_GET['id'])) {
// retrieve the file with given id from disk, return it,
// and remove it from disk
$id = $_GET['id'];
$body = file_get_contents(getFilename($id));
if ($body !== false) {
header("Content-Disposition: attachment; filename=\"$filename\"");
header('Content-type: application/json');
echo $body;
unlink(getFilename($id));
}
else {
header('HTTP/1.1 404 Not Found');
}
}
else {
// TODO: error
}
}
else if ($method == 'POST') {
// retrieve the data, save it on disk with a random id,
// and return the id.
if (isset($_FILES['file'])) {
// read body from uploaded form
$file = $_FILES['file'];
$id = uniqid();
$filename = getFilename($id);
move_uploaded_file($file['tmp_name'], $filename);
echo $id;
}
else {
// read raw body from post request
$body = @file_get_contents('php://input');
if ($body === false) {
$body = '';
}
$id = uniqid();
file_put_contents(getFilename($id), $body);
echo $id;
}
}
// cleanup files older than 1 hour
// http://stackoverflow.com/q/6411451/1262753
if ($dir = opendir($tmp)) {
$now = time();
while (false !== ($file = readdir($dir))) {
$filename = "$tmp/$file";
if (is_file($filename) && filemtime($filename) <= ($now - 60 * 60) ) {
unlink($filename);
}
}
closedir($dir);
}
?>

133
app/web/hash.js Normal file
View File

@ -0,0 +1,133 @@
/**
* @prototype Hash
* This prototype contains methods to manipulate the hash of the web page
*/
function Hash() {}
/**
* get an object value with all parameters in the hash
* @return {Object} query object containing key/values
*/
Hash.prototype.getQuery = function () {
var hash = window.location.hash.substring(1); // skip the # character
var params = hash.split('&');
var query = {};
for (var i = 0, iMax = params.length; i < iMax; i++) {
var keyvalue = params[i].split('=');
if (keyvalue.length == 2) {
var key = decodeURIComponent(keyvalue[0]);
var value = decodeURIComponent(keyvalue[1]);
query[key] = value;
}
}
return query;
};
/**
* Register a callback function which will be called when the hash of the web
* page changes.
* @param {String} key
* @param {function} callback Will be called with the new value as parameter
*/
Hash.prototype.onChange = function (key, callback) {
this.prevHash = '';
var me = this;
if (!me.callbacks) {
me.callbacks = [];
}
me.callbacks.push({
'key': key,
'value': undefined,
'callback': callback
});
function checkForChanges() {
for (var i = 0; i < me.callbacks.length; i++) {
var obj = me.callbacks[i];
var value = me.getValue(obj.key);
var changed = (value !== obj.value);
obj.value = value;
if (changed) {
obj.callback(value);
}
}
}
// source: http://stackoverflow.com/questions/2161906/handle-url-anchor-change-event-in-js
if ('onhashchange' in window) {
window.onhashchange = function () {
checkForChanges();
}
}
else {
// onhashchange event not supported
me.prevHash = window.location.hash;
window.setInterval(function () {
var hash = window.location.hash;
if (hash != me.prevHash) {
me.prevHash = hash;
checkForChanges();
}
}, 500);
}
};
/**
* Set hash parameters
* @param {Object} query object with strings
*/
Hash.prototype.setQuery = function (query) {
var hash = '';
for (var key in query) {
if (query.hasOwnProperty(key)) {
var value = query[key];
if (value != undefined) {
if (hash.length) {
hash += '&';
}
hash += encodeURIComponent(key);
hash += '=';
hash += encodeURIComponent(query[key]);
}
}
}
window.location.hash = (hash.length ? ('#' + hash) : '');
};
/**
* Retrieve a parameter value from the hash
* @param {String} key
* @return {String | undefined} value undefined when the value is not found
*/
Hash.prototype.getValue = function (key) {
var query = this.getQuery();
return query[key];
};
/**
* Set an hash parameter
* @param {String} key
* @param {String} value
*/
Hash.prototype.setValue = function (key, value) {
var query = this.getQuery();
query[key] = value;
this.setQuery(query);
};
/**
* Remove an hash parameter
* @param {String} key
*/
Hash.prototype.removeValue = function (key) {
var query = this.getQuery();
if (query[key]) {
delete query[key];
this.setQuery(query);
}
};

View File

Before

Width:  |  Height:  |  Size: 187 B

After

Width:  |  Height:  |  Size: 187 B

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -37,7 +37,7 @@
Copyright (C) 2011-2012 Jos de Jong, http://jsoneditoronline.org
@author Jos de Jong, <wjosdejong@gmail.com>
@date 2012-10-19
@date 2012-10-31
-->
<meta name="description" content="JSON Editor Online is a web-based tool to view, edit, and format JSON. It shows your data side by side in a clear, editable treeview and in formatted plain text.">
@ -46,23 +46,50 @@
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="interface/interface.css">
<link rel="stylesheet" type="text/css" href="jsoneditor/jsoneditor-min.css">
<link rel="stylesheet" type="text/css" href="app-min.css">
<link rel="stylesheet" type="text/css" href="lib/jsoneditor/jsoneditor-min.css">
<!-- TODO: droid font
<link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
-->
<script type="text/javascript" src="jsoneditor/jsoneditor-min.js"></script>
<script type="text/javascript" src="interface/interface.js"></script>
<script type="text/javascript" src="lib/jsoneditor/jsoneditor-min.js"></script>
<script type="text/javascript" src="app-min.js"></script>
</head>
<body>
<div id="header">
<a href="http://jsoneditoronline.org" class="header">
<img alt="JSON Editor Online" title="JSON Editor Online" src="interface/img/logo.png" id="logo">
<img alt="JSON Editor Online" title="JSON Editor Online" src="img/logo.png" id="logo">
</a>
<div id="menu">
<ul>
<li>
<a id="clear" title="Clear contents">Clear</a>
</li>
<li>
<a id="open" title="Open file from disk">
Open
<span id="openMenuButton" title="Open file from disk or url">
&#x25BC;
</span>
</a>
<ul id="openMenu">
<li>
<a id="menuOpenFile" title="Open file from disk">Open&nbsp;file</a>
</li>
<li>
<a id="menuOpenUrl" title="Open file from url">Open&nbsp;url</a>
</li>
</ul>
</li>
<li>
<a id="save" title="Save file to disk">Save</a>
</li>
</ul>
</div>
<!-- TODO: info, links, faq -->
<!--
<div class="info" style="display:none;">
@ -99,14 +126,14 @@
<div id="jsoneditor"></div>
<script type="text/javascript">
main.load();
main.resize();
app.load();
app.resize();
</script>
<div id="ad" title="advertisement" >
<div id="adInfo">
ADVERTISEMENT<br>
<a class="adInfo" href="javascript: main.hideAds();">hide for now</a><br>
<a class="adInfo" href="javascript: app.hideAds();">hide for now</a><br>
</div>
<div class="adSpace"></div>
@ -136,15 +163,17 @@
&bull;
<a href="https://github.com/wjosdejong/jsoneditoronline" target="_blank" class="footer">Sourcecode</a>
&bull;
<a href="datapolicy.txt" target="_blank" class="footer">Data policy</a>
&bull;
<a href="NOTICE" target="_blank" class="footer">Copyright 2011-2012 Jos de Jong</a>
</div>
</div>
<script type="text/javascript">
main.resize();
app.resize();
</script>
<script type="text/javascript" src="interface/lib/jsonlint/jsonlint.js"></script>
<script type="text/javascript" src="lib/jsonlint/jsonlint.js"></script>
</body>
</html>

View File

@ -0,0 +1,62 @@
JSON Lint
=========
A pure [JavaScript version](http://zaach.github.com/jsonlint/) of the service provided at [jsonlint.com](http://jsonlint.com).
## Command line interface
Install jsonlint with npm to use the command line interface:
npm install jsonlint -g
Validate a file like so:
jsonlint myfile.json
or pipe input into stdin:
cat myfile.json | jsonlint
jsonlint will either report a syntax error with details or pretty print the source if it is valid.
### Options
$ jsonlint -h
usage: jsonlint <file> [options]
file file to parse; otherwise uses stdin
options:
-v, --version print version and exit
-s, --sort-keys sort object keys
-i, --in-place overwrite the file
-t CHAR, --indent CHAR character(s) to use for indentation
-c, --compact compact error display
-V, --validate a JSON schema to use for validation
-e, --environment which specification of JSON Schema the validation file uses
## Module interface
I'm not sure why you wouldn't use the built in `JSON.parse` but you can use jsonlint from a CommonJS module:
var jsonlint = require("jsonlint");
jsonlint.parse('{"creative?": false}');
It returns the parsed object or throws an `Error`.
## Vim Plugins
* [Syntastic](http://www.vim.org/scripts/script.php?script_id=2736)
* [sourcebeautify](http://www.vim.org/scripts/script.php?script_id=4079)
## MIT License
Copyright (C) 2012 Zachary Carter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

150
app/web/notifications.js Normal file
View File

@ -0,0 +1,150 @@
/**
* Utility to display notifications and error messages.
* The messages are displayed on the top center of the web page
* @constructor Notifications
*/
function Notifications () {
this.dom = {};
this.notifications = [];
// TODO: attach the event as soon as there are one or multiple messages displayed,
// remove it as soon as they are all gone
var me = this;
JSONEditor.Events.addEventListener(document, 'keydown', function (event) {
me.onKeyDown(event);
});
}
/**
* Show a notification
* @param {String} message
* @return {Element} messageObject
*/
Notifications.prototype.showNotification = function (message) {
return this.showMessage({
type: 'notification',
message: message,
closeButton: false
});
};
/**
* Show an error message
* @param {Error} error
* @return {Element} messageObject
*/
Notifications.prototype.showError = function (error) {
return this.showMessage({
type: 'error',
message: (error.message || error.toString()),
closeButton: true
});
};
/**
* Show a message
* @param {Object} params Available parameters:
* {String} message
* {String} type 'error', 'notification'
* {Boolean} closeButton
* @return {Element} messageObject
*/
Notifications.prototype.showMessage = function (params) {
var frame = this.dom.frame;
if (!frame) {
var width = 500;
var top = 5;
var windowWidth = document.body.offsetWidth || window.innerWidth;
frame = document.createElement('div');
frame.style.position = 'absolute';
frame.style.left = (windowWidth - width) / 2 + 'px';
frame.style.width = width + 'px';
frame.style.top = top + 'px';
document.body.appendChild(frame);
this.dom.frame = frame;
}
var type = params.type || 'notification';
var closeable = (params.closeButton !== false);
var divMessage = document.createElement('div');
divMessage.className = type;
divMessage.type = type;
divMessage.closeable = closeable;
divMessage.style.position = 'relative';
frame.appendChild(divMessage);
var table = document.createElement('table');
table.style.width = '100%';
divMessage.appendChild(table);
var tbody = document.createElement('tbody');
table.appendChild(tbody);
var tr = document.createElement('tr');
tbody.appendChild(tr);
var tdMessage = document.createElement('td');
tdMessage.innerHTML = params.message || '';
tr.appendChild(tdMessage);
if (closeable) {
var tdClose = document.createElement('td');
tdClose.style.textAlign = 'right';
tdClose.style.verticalAlign = 'top';
tr.appendChild(tdClose);
var closeDiv = document.createElement('button');
closeDiv.innerHTML = '&times;';
closeDiv.title = 'Close message (ESC)';
tdClose.appendChild(closeDiv);
var me = this;
closeDiv.onclick = function () {
me.removeMessage(divMessage);
}
}
return divMessage;
};
/**
* Remove a message from the list with messages
* @param {Element} [message] The HTML DOM of a message
* If undefined, the first closeable message will
* closed.
*/
Notifications.prototype.removeMessage = function (message) {
var frame = this.dom.frame;
if (!message && frame) {
// find the first closable message in the list with displayed messages
var child = frame.firstChild;
while (child && !child.closeable) {
child = child.nextSibling;
}
if (child && child.closeable) {
message = child;
}
}
if (message && message.parentNode == frame) {
message.parentNode.removeChild(message);
}
if (frame && frame.childNodes.length == 0) {
frame.parentNode.removeChild(frame);
delete this.dom.frame;
}
};
/**
* Handle key down event.
* @param {Event} event
* @private
*/
Notifications.prototype.onKeyDown = function (event) {
event = event || window.event;
var keynum = event.which || event.keyCode;
if (keynum == 27) { // ESC
// remove the oldest open and closeable message
this.removeMessage();
JSONEditor.Events.preventDefault(event);
JSONEditor.Events.stopPropagation(event);
}
};

138
app/web/splitter.js Normal file
View File

@ -0,0 +1,138 @@
/**
* A splitter control.
* Turns an existing HTML element into an horizontal splitter control.
* @constructor Splitter
* @param {Object} params Available parameters:
* {Element} container HTML container representing
* the splitter
* {function} [change] Callback method called when
* the splitter value has changed.
* The callback is called with
* the new value as parameter
*/
function Splitter (params) {
if (!params || !params.container) {
throw new Error('params.container undefined in Splitter constructor');
}
var me = this;
JSONEditor.Events.addEventListener(params.container, "mousedown", function (event) {
me.onMouseDown(event);
});
this.container = params.container;
this.onChange = (params.change) ? params.change : function () {};
this.params = {};
}
/**
* Handle mouse down event. Start dragging the splitter.
* @param {Event} event
* @private
*/
Splitter.prototype.onMouseDown = function (event) {
var me = this;
var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
if (!leftButtonDown) {
return;
}
if (!this.params.mousedown) {
this.params.mousedown = true;
this.params.mousemove =
JSONEditor.Events.addEventListener(document, 'mousemove', function (event) {
me.onMouseMove(event);
});
this.params.mouseup =
JSONEditor.Events.addEventListener(document, 'mouseup', function (event) {
me.onMouseUp(event);
});
this.params.screenX = event.screenX;
this.params.value = this.getValue();
}
JSONEditor.Events.preventDefault(event);
};
/**
* Handle on mouse move event. Used to drag the splitter
* @param {Event} event
* @private
*/
Splitter.prototype.onMouseMove = function (event) {
var width = (window.innerWidth || document.body.offsetWidth ||
document.documentElement.offsetWidth);
var diff = event.screenX - this.params.screenX;
var value = this.params.value + diff / width;
value = this.setValue(value);
this.onChange(value);
JSONEditor.Events.preventDefault(event);
};
/**
* Handle on mouse up event
* @param {Event} event
* @private
*/
Splitter.prototype.onMouseUp = function (event) {
if (this.params.mousedown) {
JSONEditor.Events.removeEventListener(document, 'mousemove', this.params.mousemove);
JSONEditor.Events.removeEventListener(document, 'mouseup', this.params.mouseup);
this.params.mousemove = undefined;
this.params.mouseup = undefined;
this.params.mousedown = false;
}
JSONEditor.Events.preventDefault(event);
};
/**
* Set a value for the splitter (UI is not adjusted)
* @param {Number} value A number between 0.1 and 0.9
* @return {Number} value The stored value
*/
Splitter.prototype.setValue = function (value) {
value = Number(value);
if (value < 0.1) {
value = 0.1;
}
if (value > 0.9) {
value = 0.9;
}
this.value = value;
try {
localStorage['splitterValue'] = value;
}
catch (e) {
console.log(e);
}
return value;
};
/**
* Get the splitter value from local storage
* @return {Number} value A value between 0.1 and 0.9
*/
Splitter.prototype.getValue = function () {
var value = this.value;
if (value == undefined) {
// read from localStorage once
try {
if (localStorage['splitterValue'] != undefined) {
value = Number(localStorage['splitterValue']); // read
value = this.setValue(value); // verify and store
}
}
catch (e) {
console.log(e);
}
}
if (value == undefined) {
value = this.setValue(0.5);
}
return value;
};

View File

@ -36,7 +36,7 @@
Copyright (C) 2011-2012 Jos de Jong, http://jsoneditoronline.org
@author Jos de Jong, <wjosdejong@gmail.com>
@date 2012-10-19
@date 2012-10-31
-->
<meta name="description" content="JSON Editor Online is a web-based tool to view, edit, and format JSON. It shows your data side by side in a clear, editable treeview and in formatted plain text.">
@ -45,25 +45,65 @@
<link rel="shortcut icon" href="favicon.ico">
<link href="interface/interface.css" rel="stylesheet" type="text/css">
<link href="jsoneditor/jsoneditor.css" rel="stylesheet" type="text/css">
<link href="app.css" rel="stylesheet" type="text/css">
<link href="fileretriever.css" rel="stylesheet" type="text/css">
<link href="../../jsoneditor/jsoneditor.css" rel="stylesheet" type="text/css">
<!-- TODO: droid font
<link href='http://fonts.googleapis.com/css?family=Droid+Sans+Mono' rel='stylesheet' type='text/css'>
-->
<script type="text/javascript" src="jsoneditor/jsoneditor.js"></script>
<script type="text/javascript" src="interface/interface.js"></script>
<script type="text/javascript" src="interface/lib/jsonlint/jsonlint.js"></script>
<script type="text/javascript" src="../../jsoneditor/jsoneditor.js"></script>
<script type="text/javascript" src="hash.js"></script>
<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript" src="fileretriever.js"></script>
<script type="text/javascript" src="notifications.js"></script>
<script type="text/javascript" src="splitter.js"></script>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript" src="lib/jsonlint/jsonlint.js"></script>
<style type="text/css">
div.convert-right {
background: url('../../jsoneditor/img/jsoneditor-icons.png') -168px 0;
}
div.convert-left {
background: url('../../jsoneditor/img/jsoneditor-icons.png') -192px 0;
}
</style>
</head>
<body spellcheck="false" >
<body>
<div id="header" >
<a href="http://jsoneditoronline.org" class="header">
<img alt="JSON Editor Online" title="JSON Editor Online" src="interface/img/logo.png" id="logo">
<img alt="JSON Editor Online" title="JSON Editor Online" src="img/logo.png" id="logo">
</a>
<div id="menu">
<ul>
<li>
<a id="clear" title="Clear contents">Clear</a>
</li>
<li>
<a id="open" title="Open file from disk">
Open
<span id="openMenuButton" title="Open file from disk or url">
&#x25BC;
</span>
</a>
<ul id="openMenu">
<li>
<a id="menuOpenFile" title="Open file from disk">Open&nbsp;file</a>
</li>
<li>
<a id="menuOpenUrl" title="Open file from url">Open&nbsp;url</a>
</li>
</ul>
</li>
<li>
<a id="save" title="Save file to disk">Save</a>
</li>
</ul>
</div>
<!-- TODO: info, links, faq -->
<!--
@ -101,17 +141,14 @@
<div id="jsoneditor"></div>
<script type="text/javascript">
main.load();
main.resize();
app.load();
app.resize();
</script>
<div id="ad" title="advertisement" >
<div id="adInfo">
ADVERTISEMENT<br>
<a class="adInfo" href="javascript: main.hideAds();">hide for now</a><br>
<div id="removeAds">
<a class="adInfo" href="javascript: main.removeAds()">get rid of the ads</a>
</div>
<a class="adInfo" href="javascript: app.hideAds();">hide for now</a><br>
<div id="chromeAppInfo" style='display: none;'>
<br>If you want to get rid of the ads,
you can buy the Chrome App.
@ -148,16 +185,18 @@
<div id="footer-inner">
<a href="http://jsoneditoronline.org" class="footer">JSON Editor Online 1.6.0</a>
&bull;
<a href="changelog.txt" target="_blank" class="footer">Changelog</a>
<a href="../../changelog.txt" target="_blank" class="footer">Changelog</a>
&bull;
<a href="https://github.com/wjosdejong/jsoneditoronline" target="_blank" class="footer">Sourcecode</a>
&bull;
<a href="NOTICE" target="_blank" class="footer">Copyright 2011-2012 Jos de Jong</a>
<a href="datapolicy.txt" target="_blank" class="footer">Data policy</a>
&bull;
<a href="../../NOTICE" target="_blank" class="footer">Copyright 2011-2012 Jos de Jong</a>
</div>
</div>
<script type="text/javascript">
main.resize();
app.resize();
</script>
</body>

View File

@ -7,11 +7,12 @@
<property name="root" location="" />
<property name="lib" location="build/lib" />
<property name="web" location="build/web" />
<property name="web_app" location="build/app/web" />
<property name="chrome_app" location="build/app/chrome" />
<property name="web_app_src" location="app/web" />
<property name="compressor" value="tools/yuicompressor-2.4.7.jar" />
<target name="minify" description="minify jsoneditor libraries">
<target name="minify_lib" description="minify jsoneditor library">
<java jar="${compressor}" dir="jsoneditor" fork="true" failonerror="true">
<arg value="-o"/>
<arg value="jsoneditor-min.js"/>
@ -24,7 +25,7 @@
</java>
</target>
<target name="zip" depends="minify" description="create zipped jsoneditor libraries">
<target name="zip_lib" depends="minify_lib" description="create zip file with the jsoneditor library">
<mkdir dir="${lib}" />
<!-- create a zip file with non-minified jsoneditor -->
@ -56,41 +57,75 @@
</zip>
</target>
<target name="build_site" depends="minify" description="copy all files for the site to the build directory">
<delete dir="${web}" />
<mkdir dir="${web}" />
<copy file="LICENSE" todir="${web}" />
<copy file="NOTICE" todir="${web}" />
<copy file="README.md" todir="${web}" />
<copy file="robots.txt" todir="${web}" />
<copy file="changelog.txt" todir="${web}" />
<copy file="index.html" todir="${web}" />
<copy file="favicon.ico" todir="${web}" />
<copy file="interface/interface.js" todir="${web}/interface" />
<copy file="interface/interface.css" todir="${web}/interface" />
<copy file="interface/lib/jsonlint/jsonlint.js" todir="${web}/interface/lib/jsonlint" />
<copy file="interface/img/logo.png" todir="${web}/interface/img" />
<copy file="interface/img/header_background.png" todir="${web}/interface/img" />
<copy file="jsoneditor/jsoneditor-min.js" todir="${web}/jsoneditor" />
<copy file="jsoneditor/jsoneditor-min.css" todir="${web}/jsoneditor" />
<copy file="jsoneditor/img/jsoneditor-icons.png" todir="${web}/jsoneditor/img" />
<target name="build_web_app" depends="minify_lib" description="copy all files for the web application to the build directory">
<delete dir="${web_app}" />
<mkdir dir="${web_app}" />
<!-- concatenate the javascript and css app files -->
<concat destfile="${web_app}/app.js">
<fileset dir="${web_app_src}" includes="hash.js"/>
<fileset dir="${web_app_src}" includes="ajax.js"/>
<fileset dir="${web_app_src}" includes="fileretriever.js"/>
<fileset dir="${web_app_src}" includes="notifications.js"/>
<fileset dir="${web_app_src}" includes="splitter.js"/>
<fileset dir="${web_app_src}" includes="app.js"/>
</concat>
<concat destfile="${web_app}/app.css">
<fileset dir="${web_app_src}" includes="fileretriever.css"/>
<fileset dir="${web_app_src}" includes="app.css"/>
</concat>
<!-- copy all other files and libraries-->
<copy file="changelog.txt" todir="${web_app}" />
<copy file="README.md" todir="${web_app}" />
<copy file="LICENSE" todir="${web_app}" />
<copy file="NOTICE" todir="${web_app}" />
<copy file="${web_app_src}/robots.txt" todir="${web_app}" />
<copy file="${web_app_src}/datapolicy.txt" todir="${web_app}" />
<copy file="${web_app_src}/index.html" todir="${web_app}" />
<copy file="${web_app_src}/favicon.ico" todir="${web_app}" />
<copy file="${web_app_src}/fileretriever.php" todir="${web_app}" />
<copy file="${web_app_src}/img/logo.png" todir="${web_app}/img" />
<copy file="${web_app_src}/img/header_background.png" todir="${web_app}/img" />
<copy file="${web_app_src}/lib/jsonlint/jsonlint.js" todir="${web_app}/lib/jsonlint" />
<copy file="jsoneditor/jsoneditor-min.js" todir="${web_app}/lib/jsoneditor" />
<copy file="jsoneditor/jsoneditor-min.css" todir="${web_app}/lib/jsoneditor" />
<copy file="jsoneditor/img/jsoneditor-icons.png" todir="${web_app}/lib/jsoneditor/img" />
<!-- minify the javascript files -->
<java jar="${compressor}" dir="${web_app}" fork="true" failonerror="true">
<arg value="-o"/>
<arg value="app-min.js"/>
<arg value="app.js"/>
</java>
<java jar="${compressor}" dir="${web_app}" fork="true" failonerror="true">
<arg value="-o"/>
<arg value="app-min.css"/>
<arg value="app.css"/>
</java>
<!-- delete non-minified app files -->
<delete file="${web_app}/app.js" />
<delete file="${web_app}/app.css" />
</target>
<target name="build_chrome_app" depends="minify" description="copy all files for the chrome app to the build directory">
<target name="build_chrome_app" depends="minify_lib" description="copy and zip all files for the chrome app">
<!-- hosted app -->
<delete dir="${chrome_app}" />
<mkdir dir="${chrome_app}" />
<copy file="app/chrome/manifest.json" todir="${chrome_app}" />
<copy file="interface/img/icon_16.png" todir="${chrome_app}" />
<copy file="interface/img/icon_128.png" todir="${chrome_app}" />
</target>
<copy file="${web_app_src}/img/icon_16.png" todir="${chrome_app}" />
<copy file="${web_app_src}/img/icon_128.png" todir="${chrome_app}" />
<target name="zip_chrome_app" depends="build_chrome_app" description="zip the chrome application">
<zip destfile="build/app/chrome_app.zip">
<zip destfile="build/app/chrome.zip">
<fileset dir="${chrome_app}" />
</zip>
<delete dir="${chrome_app}" />
</target>
<target name="main" depends="minify, zip, build_site, build_chrome_app, zip_chrome_app" />
<target name="main" depends="minify_lib, zip_lib, build_web_app, build_chrome_app" />
</project>

1
build/.gitignore vendored
View File

@ -1,2 +1 @@
web
app

View File

@ -4,7 +4,9 @@ http://jsoneditoronline.org
<not yet released>, version 1.6.0
- Improved error messages, using JSONLint.
- Added feature to the web application to load and save files to disk and from
url.
- Improved error messages in the web application using JSONLint.
- Made the web application pass the W3C markup validation service.

View File

@ -1,431 +0,0 @@
/**
* @file interface.js
*
* @brief
* JsonEditor is an editor to display and edit JSON data in a treeview.
*
* Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
*
* @license
* This json editor is open sourced with the intention to use the editor as
* a component in your own application. Not to just copy and monetize the editor
* as it is.
*
* 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) 2011-2012 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong, <wjosdejong@gmail.com>
* @date 2012-10-19
*/
var editor = null;
var formatter = null;
var main = {};
/**
* Get the JSON from the formatter and load it in the editor
*/
main.formatterToEditor = function() {
try {
editor.set(formatter.get());
}
catch (err) {
main.showError(err);
}
};
/**
* Get the JSON from the editor and load it into the formatter
*/
main.editorToFormatter = function () {
try {
formatter.set(editor.get());
}
catch (err) {
main.showError(err);
}
};
main.eventParams = {};
/**
* Handle key down event.
* @param {Event} event
*/
main.onKeyDown = function (event) {
event = event || window.event;
var keynum = event.which || event.keyCode;
if (keynum == 27) { // ESC
// remove the oldest open error message
main.removeError();
}
};
/**
* Handle mouse down event. Start dragging the splitter.
* @param {Event} event
*/
main.onMouseDown = function (event) {
var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
if (!leftButtonDown) {
return;
}
if (!main.eventParams.mousedown) {
main.eventParams.mousedown = true;
main.eventParams.mousemove =
JSONEditor.Events.addEventListener(document, 'mousemove', main.onMouseMove);
main.eventParams.mouseup =
JSONEditor.Events.addEventListener(document, 'mouseup', main.onMouseUp);
main.eventParams.screenX = event.screenX;
main.eventParams.splitterValue = main.getSplitterValue();
}
JSONEditor.Events.preventDefault(event);
};
/**
* Handle on mouse move event. Used to drag the spitter
* @param {Event} event
*/
main.onMouseMove = function (event) {
var width = (window.innerWidth || document.body.offsetWidth ||
document.documentElement.offsetWidth);
var diff = event.screenX - main.eventParams.screenX;
var value = main.eventParams.splitterValue + diff / width;
main.setSplitterValue(value);
main.resize();
JSONEditor.Events.preventDefault(event);
};
/**
* Handle on mouse up event
* @param {Event} event
*/
main.onMouseUp = function (event) {
if (main.eventParams.mousedown) {
JSONEditor.Events.removeEventListener(document, 'mousemove', main.eventParams.mousemove);
JSONEditor.Events.removeEventListener(document, 'mouseup', main.eventParams.mouseup);
main.eventParams.mousemove = undefined;
main.eventParams.mouseup = undefined;
main.eventParams.mousedown = false;
}
JSONEditor.Events.preventDefault(event);
};
/**
* Set a value for the splitter (UI is not adjusted)
* @param {Number} value A number between 0.1 and 0.9
* @return {Number} value The stored value
*/
main.setSplitterValue = function (value) {
value = Number(value);
if (value < 0.1) {
value = 0.1;
}
if (value > 0.9) {
value = 0.9;
}
main.splitterValue = value;
try {
localStorage['splitterValue'] = value;
}
catch (e) {
console.log(e);
}
return value;
};
/**
* Get the splitter value from local storage
* @return {Number} value A value between 0.1 and 0.9
*/
main.getSplitterValue = function () {
var value = main.splitterValue;
if (value == undefined) {
// read from localStorage once
try {
if (localStorage['splitterValue'] != undefined) {
value = Number(localStorage['splitterValue']); // read
value = main.setSplitterValue(value); // verify and store
}
}
catch (e) {
console.log(e);
}
}
if (value == undefined) {
value = main.setSplitterValue(0.5);
}
if (value == undefined) {
value = 0.5;
}
return value;
};
/**
* Load the interface (editor, formatter, splitter)
*/
main.load = function() {
var json = {
"name": "John Smith",
"age": 32,
"employed": true,
"address": {
"street": "701 First Ave.",
"city": "Sunnyvale, CA 95125",
"country": "United States"
},
"children": [
{
"name": "Richard",
"age": 7
},
{
"name": "Susan",
"age": 4
},
{
"name": "James",
"age": 3
}
]
};
try {
// formatter
var container = document.getElementById("jsonformatter");
formatter = new JSONFormatter(container);
formatter.set(json);
formatter.onError = function (err) {
main.showError(err);
};
// editor
container = document.getElementById("jsoneditor");
editor = new JSONEditor(container);
editor.set(json);
// splitter
var domSplitter = document.getElementById('splitter');
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
var toForm = document.createElement('button');
toForm.id = 'toForm';
toForm.title = 'JSON to Editor';
toForm.className = 'convert';
toForm.innerHTML = '<div class="convert-right"></div>';
toForm.onclick = function () {
this.focus();
main.formatterToEditor();
};
domSplitter.appendChild(toForm);
domSplitter.appendChild(document.createElement('br'));
domSplitter.appendChild(document.createElement('br'));
var toJSON = document.createElement('button');
toJSON.id = 'toJSON';
toJSON.title = 'Editor to JSON';
toJSON.className = 'convert';
toJSON.innerHTML = '<div class="convert-left"></div>';
toJSON.onclick = function () {
this.focus();
main.editorToFormatter();
};
domSplitter.appendChild(toJSON);
JSONEditor.Events.addEventListener(domSplitter, "mousedown", main.onMouseDown);
JSONEditor.Events.addEventListener(window, 'keydown', main.onKeyDown);
// resize
JSONEditor.Events.addEventListener(window, 'resize', main.resize);
// TODO: implement a focus method
formatter.textarea.focus();
// TODO: a nicer method to check for changes
var formatterLastContent;
var editorLastContent;
function checkChange () {
try {
// check for change in formatter
var formatterJSON = formatter.get();
var formatterContent = JSON.stringify(formatterJSON);
if (formatterContent != formatterLastContent) {
formatterLastContent = formatterContent;
editorLastContent = formatterContent;
editor.set(formatterJSON);
}
else {
// check for change in editor
var editorJSON = editor.get();
var editorContent = JSON.stringify(editorJSON);
if (editorContent != editorLastContent) {
editorLastContent = editorContent;
formatterLastContent = editorContent;
formatter.set(editorJSON);
}
}
}
catch (err) {
main.showError(err);
}
setTimeout(checkChange, 1000);
}
/* TODO: use checkChange
checkChange();
*/
// enforce FireFox to not do spell checking on any input field
document.body.spellcheck = false;
} catch (err) {
main.showError(err);
}
};
main.resize = function() {
var domEditor = document.getElementById('jsoneditor');
var domFormatter = document.getElementById('jsonformatter');
var domSplitter = document.getElementById('splitter');
var domAd = document.getElementById('ad');
var domAdInfo = document.getElementById('adInfo');
var width = window.innerWidth || document.body.offsetWidth || document.documentElement.offsetWidth;
var height = window.innerHeight || document.body.offsetHeight || document.documentElement.offsetHeight;
var adWidth = domAd ? domAd.clientWidth : 0;
var splitterWidth = domSplitter.clientWidth;
if (adWidth) {
width -= (adWidth + 15); // Not so nice, +15 here for the margin
}
var splitterLeft = width * main.getSplitterValue();
// resize formatter
domFormatter.style.width = Math.round(splitterLeft) + 'px';
// resize editor
// the width has a -1 to prevent the width from being just half a pixel
// wider than the window, causing the content elements to wrap...
domEditor.style.left = Math.round(splitterLeft + splitterWidth) + 'px';
domEditor.style.width = Math.round(width - splitterLeft - splitterWidth - 1) + 'px';
//editor.onResize(); // TODO
// resize ad text
if (domAdInfo && domAd) {
var infoHeight = domAdInfo.clientHeight;
domAd.style.paddingTop = infoHeight + 'px';
}
};
main.errorFrame = undefined;
/**
* Show an error message top center of the interface
* @param {Error} error
*/
main.showError = function (error) {
if (!error) {
return;
}
if (!main.errorFrame) {
var width = 500;
var top = 5;
var windowWidth = document.body.offsetWidth || window.innerWidth;
main.errorFrame = document.createElement('div');
main.errorFrame.style.position = 'absolute';
main.errorFrame.style.left = (windowWidth - width) / 2 + 'px';
main.errorFrame.style.width = width + 'px';
main.errorFrame.style.top = top + 'px';
document.body.appendChild(main.errorFrame);
}
var divError = document.createElement('div');
divError.className = 'error';
divError.style.position = 'relative';
main.errorFrame.appendChild(divError);
var table = document.createElement('table');
table.style.width = '100%';
divError.appendChild(table);
var tbody = document.createElement('tbody');
table.appendChild(tbody);
var tr = document.createElement('tr');
tbody.appendChild(tr);
var tdMessage = document.createElement('td');
tdMessage.innerHTML = error.message || error.toString();
tr.appendChild(tdMessage);
var tdClose = document.createElement('td');
tdClose.style.textAlign = 'right';
tdClose.style.verticalAlign = 'top';
tr.appendChild(tdClose);
var closeDiv = document.createElement('button');
closeDiv.innerHTML = '&times;';
closeDiv.title = 'Close error message';
tdClose.appendChild(closeDiv);
closeDiv.onclick = function () {
main.removeError(divError);
}
};
/**
* Remove an error from the list with errors
* @param {Element} [error] The HTML DOM of an error message
* If undefined, the first error message will be
* removed.
*/
main.removeError = function (error) {
if (!error && main.errorFrame) {
error = main.errorFrame.firstChild;
}
if (error && error.parentNode == main.errorFrame) {
error.parentNode.removeChild(error);
}
if (main.errorFrame && main.errorFrame.childNodes.length == 0) {
main.errorFrame.parentNode.removeChild(main.errorFrame);
main.errorFrame = undefined;
}
};
main.hideAds = function() {
var domAd = document.getElementById("ad");
if (domAd) {
domAd.parentNode.removeChild(domAd);
main.resize();
}
};
main.removeAds = function () {
var domRemoveAds = document.getElementById('removeAds');
if (domRemoveAds) {
domRemoveAds.style.display = 'none';
}
main.resize();
};

View File

@ -27,7 +27,7 @@
* Copyright (c) 2011-2012 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong, <wjosdejong@gmail.com>
* @date 2012-10-19
* @date 2012-10-31
*/
@ -3105,6 +3105,13 @@ JSONFormatter.prototype.getText = function() {
return this.textarea.value;
};
/**
* Set the text contents of the JSONFormatter
* @param {String} text
*/
JSONFormatter.prototype.setText = function(text) {
this.textarea.value = text;
};
/**
* Set a callback method for the onchange event

View File

@ -7,7 +7,7 @@
<meta name="keywords" content="json, editor, couchdb, online, javascript, javascript object notation, treeview, open source, free">
<meta name="author" content="Jos de Jong">
<link rel="shortcut icon" href="../favicon.ico">
<link rel="shortcut icon" href="../app/web/favicon.ico">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"></script>
<script type="text/javascript" src="../jsoneditor/jsoneditor.js"></script>