Commit 38d56377 authored by Rebecca Blundell's avatar Rebecca Blundell Committed by Rebecca Blundell

Bug 1635503: Add JSON editor for SmartEvidence

This adds a visual JSON editor for SmartEvidence
frameworks allowing site administrators to create
such frameworks more easily by copying and pasting
rather than needing to know the JSON file syntax.

behatnotneeded

Change-Id: Ief74375a6c5d23ab05e12f08d07dd3209e89c948
parent 2c82fcdd
The MIT License (MIT)
Copyright (c) 2013 Jeremy Dorn
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.
JSONeditor in Mahara
=================
Website: https://github.com/json-editor/json-editor/
Version: 1.3
This library is used by SmartEvidence, to provide a user-friendly way
of editing a framework, without relying on a json upload.
In order to work, the JSONeditor needs a dist folder with jsoneditor.js and
jsoneditor.js.map in it.
It's not in the github repo for some reason, but can be found here:
https://www.jsdelivr.com/package/npm/@json-editor/json-editor?path=dist&version=1.3.0
Changes:
Removed .github, tests and docs folders; Gruntfile.js, package.json,
package-lock.json, .gitattributes, .gitignore, npmignore, .npmrc and travis.yml
From src/iconlibs, removed bootstrap2.js, foundation2-3.js, materialicons.js
src/styles
src/templates - all but default
src/themes bootstrap2.js, foundation.js, materialize.js
To allow changing of the text on the Edit JSON button:
in src/defaults.js, added:
/**
* Title on Edit JSON buttons
*/
button_edit : "Edit raw"
in src/dist/jsoneditor.js
replaced this.editjson_button = this.getButton('JSON','edit','Edit JSON');
with this.editjson_button = this.getButton('','edit', this.translate('button_edit'));
added button_edit: "Edit raw", to the JSONEditor.defualts.languages.en JSON
To stop the editor displaying undefined in the Standard Elements header when the parent
id is undefined:
in src/dist/jsoneditor.js - added the asterisked line to the following:
// The compiled function
return function(vars) {
var ret = template+"";
var r;
for(i=0; i<l; i++) {
r = replacements[i];
ret = ret.replace(r.s, r.r(vars));
}
* ret = ret.replace(/undefined\./gi, '');
return ret;
Because Mahara requires just an initial capital letter on titles, added the asterisked
lines to the JSONEditor.defaults.translate function:
if(variables) {
for(var i=0; i<variables.length; i++) {
string = string.replace(new RegExp('\\{\\{'+i+'}}','g'),variables[i]);
* string = string.toLowerCase();
* string = string.charAt(0).toUpperCase() + string.slice(1);
}
}
This diff is collapsed.
/*jshint loopfunc: true */
/* Simple JavaScript Inheritance
* By John Resig http://ejohn.org/
* MIT Licensed.
*/
// Inspired by base2 and Prototype
var Class;
(function(){
var initializing = false, fnTest = /xyz/.test(function(){window.postMessage("xyz");}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
Class = function(){};
// Create a new Class that inherits from this class
Class.extend = function extend(prop) {
var _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
initializing = false;
// Copy the properties over onto the new prototype
for (var name in prop) {
// Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
this._super = _super[name];
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
// The dummy class constructor
function Class() {
// All construction is actually done in the init method
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
// Populate our constructed prototype object
Class.prototype = prototype;
// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;
// And make this class extendable
Class.extend = extend;
return Class;
};
return Class;
})();
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
JSONEditor.defaults.editors.arraySelectize = JSONEditor.AbstractEditor.extend({
build: function() {
this.title = this.theme.getFormInputLabel(this.getTitle());
this.title_controls = this.theme.getHeaderButtonHolder();
this.title.appendChild(this.title_controls);
this.error_holder = document.createElement('div');
if(this.schema.description) {
this.description = this.theme.getDescription(this.schema.description);
}
this.input = document.createElement('select');
this.input.setAttribute('multiple', 'multiple');
var group = this.theme.getFormControl(this.title, this.input, this.description);
this.container.appendChild(group);
this.container.appendChild(this.error_holder);
window.jQuery(this.input).selectize({
delimiter: false,
createOnBlur: true,
create: true
});
},
postBuild: function() {
var self = this;
this.input.selectize.on('change', function(event) {
self.refreshValue();
self.onChange(true);
});
},
destroy: function() {
this.empty(true);
if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
this._super();
},
empty: function(hard) {},
setValue: function(value, initial) {
var self = this;
// Update the array's value, adding/removing rows when necessary
value = value || [];
if(!(Array.isArray(value))) value = [value];
this.input.selectize.clearOptions();
this.input.selectize.clear(true);
value.forEach(function(item) {
self.input.selectize.addOption({text: item, value: item});
});
this.input.selectize.setValue(value);
this.refreshValue(initial);
},
refreshValue: function(force) {
this.value = this.input.selectize.getValue();
},
showValidationErrors: function(errors) {
var self = this;
// Get all the errors that pertain to this editor
var my_errors = [];
var other_errors = [];
$each(errors, function(i,error) {
if(error.path === self.path) {
my_errors.push(error);
}
else {
other_errors.push(error);
}
});
// Show errors for this editor
if(this.error_holder) {
if(my_errors.length) {
var message = [];
this.error_holder.innerHTML = '';
this.error_holder.style.display = '';
$each(my_errors, function(i,error) {
self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
});
}
// Hide error area
else {
this.error_holder.style.display = 'none';
}
}
}
});
JSONEditor.defaults.editors.base64 = JSONEditor.AbstractEditor.extend({
getNumColumns: function() {
return 4;
},
setFileReaderListener: function (fr_multiple) {
var self = this;
fr_multiple.addEventListener("load", function(event) {
if (self.count == self.current_item_index) {
// Overwrite existing file by default, leave other properties unchanged
self.value[self.count][self.key] = event.target.result;
} else {
var temp_object = {};
// Create empty object
for (var key in self.parent.schema.properties) {
temp_object[key] = "";
}
// Set object media file
temp_object[self.key] = event.target.result;
self.value.splice(self.count, 0, temp_object); // insert new file object
}
// Increment using the listener and not the 'for' loop as the listener will be processed asynchronously
self.count += 1;
// When all files have been processed, update the value of the editor
if (self.count == (self.total+self.current_item_index)) {
self.arrayEditor.setValue(self.value);
}
});
},
build: function() {
var self = this;
this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
// Input that holds the base64 string
this.input = this.theme.getFormInputField('hidden');
this.container.appendChild(this.input);
// Don't show uploader if this is readonly
if(!this.schema.readOnly && !this.schema.readonly) {
if(!window.FileReader) throw "FileReader required for base64 editor";
// File uploader
this.uploader = this.theme.getFormInputField('file');
// Set attribute of file input field to 'multiple' if:
// 'multiple' key has been set to 'true' in the schema
// and the parent object is of type 'object'
// and the parent of the parent type has been set to 'array'
if (self.schema.options && self.schema.options.multiple && self.schema.options.multiple == true && self.parent && self.parent.schema.type == 'object' && self.parent.parent && self.parent.parent.schema.type == 'array') {
this.uploader.setAttribute('multiple', '');
}
this.uploader.addEventListener('change',function(e) {
e.preventDefault();
e.stopPropagation();
if(this.files && this.files.length) {
// Check the amount of files uploaded.
// If 1, use the regular upload, otherwise use the multiple upload method
if (this.files.length>1 && self.schema.options && self.schema.options.multiple && self.schema.options.multiple == true && self.parent && self.parent.schema.type == 'object' && self.parent.parent && self.parent.parent.schema.type == 'array') {
// Load editor of parent.parent to get the array
self.arrayEditor = self.jsoneditor.getEditor(self.parent.parent.path);
// Check the current value of this editor
self.value = self.arrayEditor.getValue();
// Set variables for amount of files, index of current array item and
// count value containing current status of processed files
self.total = this.files.length;
self.current_item_index = parseInt(self.parent.key);
self.count = self.current_item_index;
for (var i = 0; i < self.total; i++) {
var fr_multiple = new FileReader();
self.setFileReaderListener(fr_multiple);
fr_multiple.readAsDataURL(this.files[i]);
}
} else {
var fr = new FileReader();
fr.onload = function(evt) {
self.value = evt.target.result;
self.refreshPreview();
self.onChange(true);
fr = null;
};
fr.readAsDataURL(this.files[0]);
}
}
});
}
this.preview = this.theme.getFormInputDescription(this.schema.description);
this.container.appendChild(this.preview);
this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview, this.infoButton);
this.container.appendChild(this.control);
},
refreshPreview: function() {
if(this.last_preview === this.value) return;
this.last_preview = this.value;
this.preview.innerHTML = '';
if(!this.value) return;
var mime = this.value.match(/^data:([^;,]+)[;,]/);
if(mime) mime = mime[1];
if(!mime) {
this.preview.innerHTML = '<em>Invalid data URI</em>';
}
else {
this.preview.innerHTML = '<strong>Type:</strong> '+mime+', <strong>Size:</strong> '+Math.floor((this.value.length-this.value.split(',')[0].length-1)/1.33333)+' bytes';
if(mime.substr(0,5)==="image") {
this.preview.innerHTML += '<br>';
var img = document.createElement('img');
img.style.maxWidth = '100%';
img.style.maxHeight = '100px';
img.src = this.value;
this.preview.appendChild(img);
}
}
},
enable: function() {
if(!this.always_disabled) {
if(this.uploader) this.uploader.disabled = false;
this._super();
}
},
disable: function(always_disabled) {
if(always_disabled) this.always_disabled = true;
if(this.uploader) this.uploader.disabled = true;
this._super();
},
setValue: function(val) {
if(this.value !== val) {
this.value = val;
this.input.value = this.value;
this.refreshPreview();
this.onChange();
}
},
destroy: function() {
if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);
this._super();
}
});
JSONEditor.defaults.editors.checkbox = JSONEditor.AbstractEditor.extend({
setValue: function(value,initial) {
this.value = !!value;
this.input.checked = this.value;
this.onChange();
},
register: function() {
this._super();
if(!this.input) return;
this.input.setAttribute('name',this.formname);
},
unregister: function() {
this._super();
if(!this.input) return;
this.input.removeAttribute('name');
},
getNumColumns: function() {
return Math.min(12,Math.max(this.getTitle().length/7,2));
},
build: function() {
var self = this;
if(!this.options.compact) {
this.label = this.header = this.theme.getCheckboxLabel(this.getTitle());
}
if(this.schema.description) this.description = this.theme.getFormInputDescription(this.schema.description);
if(this.options.infoText) this.infoButton = this.theme.getInfoButton(this.options.infoText);
if(this.options.compact) this.container.classList.add('compact');
this.input = this.theme.getCheckbox();
this.control = this.theme.getFormControl(this.label, this.input, this.description, this.infoButton);
if(this.schema.readOnly || this.schema.readonly) {
this.always_disabled = true;
this.input.disabled = true;
}
this.input.addEventListener('change',function(e) {
e.preventDefault();
e.stopPropagation();
self.value = this.checked;
self.onChange(true);
});
this.container.appendChild(this.control);
},
enable: function() {
if(!this.always_disabled) {
this.input.disabled = false;
this._super();
}
},
disable: function(always_disabled) {
if(always_disabled) this.always_disabled = true;
this.input.disabled = true;
this._super();
},
destroy: function() {
if(this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
if(this.description && this.description.parentNode) this.description.parentNode.removeChild(this.description);
if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
this._super();
},
showValidationErrors: function (errors) {
var self = this;
if (this.jsoneditor.options.show_errors === "always") {}
else if (!this.is_dirty && this.previous_error_setting === this.jsoneditor.options.show_errors) {
return;
}
this.previous_error_setting = this.jsoneditor.options.show_errors;
var messages = [];
$each(errors, function (i, error) {
if (error.path === self.path) {
messages.push(error.message);
}
});
this.input.controlgroup = this.control;
if (messages.length) {
this.theme.addInputError(this.input, messages.join('. ') + '.');
}
else {
this.theme.removeInputError(this.input);
}
}
});
/*
Edtended handling of date, time and datetime-local type fields.
Works with both string and integer data types. (default only support string type)
Adds support for setting "placeholder" through options.
Has optional support for using flatpickr datepicker.
All flatpickr options is supported with a few minor differences.
- "enableTime" and "noCalendar" are set automatically, based on the data type.
- Extra config option "errorDateFormat". If this is set, it will replace the format displayed in error messages.
- It is not possible to use "inline" and "wrap" options together.
- When using the "wrap" option, "toggle" and "clear" buttons are automatically added to markup. 2 extra boolean options ("showToggleButton" and "showClearButton") are available to control which buttons to display. Note: not all frameworks supports this. (Works in: Bootstrap and Foundation)
- When using the "inline" option, an extra boolean option ("inlineHideInput") is available to hide the original input field.
- If "mode" is set to either "multiple" or "range", only string data type is supported. Also the result from these is returned as a string not an array.
ToDo:
- Add support for "required" attribute. (Maybe this should be done on a general scale, as support for other input attributes are also missing, such as "placeholder")
- Test if validation works with "required" fields. (Not sure if I have to put this into custom validator, or if it's handled elsewhere. UPDATE required attribute is currently not supported at ALL!)
- Improve Handling of flatpicker "multiple" and "range" modes. (Currently the values are just added as string values, but the optimal scenario would be to save those as array if possible)
*/
JSONEditor.defaults.editors.datetime = JSONEditor.defaults.editors.string.extend({
build: function () {
this._super();
if(!this.input) return;
// Add required and placeholder text if available
if (this.options.placeholder !== undefined) this.input.setAttribute('placeholder', this.options.placeholder);
if(window.flatpickr && typeof this.options.flatpickr == 'object') {
// Make sure that flatpickr settings matches the input type
this.options.flatpickr.enableTime = this.schema.format == 'date' ? false : true;
this.options.flatpickr.noCalendar = this.schema.format == 'time' ? true : false;
// Curently only string can contain range or multiple values
if (this.schema.type == 'integer') this.options.flatpickr.mode = 'single';
// Attribute for flatpicker
this.input.setAttribute('data-input','');
var input = this.input;
if (this.options.flatpickr.wrap === true) {
// Create buttons for input group
var buttons = [];
if (this.options.flatpickr.showToggleButton !== false) {
var toggleButton = this.getButton('',this.schema.format == 'time' ? 'time' :'calendar', this.translate('flatpickr_toggle_button'));
// Attribute for flatpicker
toggleButton.setAttribute('data-toggle','');
buttons.push(toggleButton);
}
if (this.options.flatpickr.showClearButton !== false) {
var clearButton = this.getButton('','clear', this.translate('flatpickr_clear_button'));
// Attribute for flatpicker
clearButton.setAttribute('data-clear','');
buttons.push(clearButton);
}
// Save position of input field
var parentNode = this.input.parentNode, nextSibling = this.input.nextSibling;
var buttonContainer = this.theme.getInputGroup(this.input, buttons);
if (buttonContainer !== undefined) {
// Make sure "inline" option is turned off
this.options.flatpickr.inline = false;
// Insert container at same position as input field
parentNode.insertBefore(buttonContainer, nextSibling);
input = buttonContainer;
}
else {
this.options.flatpickr.wrap = false;
}
}
this.flatpickr = window.flatpickr(input, this.options.flatpickr);
if (this.options.flatpickr.inline === true && this.options.flatpickr.inlineHideInput === true) {
this.input.setAttribute('type','hidden');
}
}
},
getValue: function