<NME> webview.h
<BEF> ADDFILE
<MSG> Merge branch 'dropmann-temp_development' into pimped

<DFF> @@ -0,0 +1,27 @@
+#ifndef WEBVIEW_H
+#define WEBVIEW_H
+
+#include <QWebView>
+
+#include "common.h"
+
+class WebView : public QWebView
+{
+
+	Q_OBJECT
+
+public:
+	explicit WebView(QWidget *parent = 0);
+
+signals:
+	void scrollValueChanged();
+
+protected:
+	virtual void paintEvent(QPaintEvent *event) OVERRIDE;
+
+private:
+	int _lastVerticalValue;
+	int _lastHorizontalValue;
+};
+
+#endif // WEBVIEW_H

{'commit': 'diff --git a/JASP-Common/exporters/jaspexporter.cpp b/JASP-Common/exporters/jaspexporter.cpp\nindex 3ed20943e..7d66bc944 100644\n--- a/JASP-Common/exporters/jaspexporter.cpp\n+++ b/JASP-Common/exporters/jaspexporter.cpp\n@@ -200,13 +200,13 @@ void JASPExporter::saveDataArchive(archive *a, DataSetPackage *package, boost::f\n \tarchive_entry_free(entry);\n \n \t//Take out for the time being\n-\t/*if (package->hasAnalyses)\n+\tif (package->hasAnalyses)\n \t{\n \t\t//Create new entry for archive: HTML results\n \t\tstring html = package->analysesHTML;\n \t\tint htmlSize = html.size();\n \t\tentry = archive_entry_new();\n-\t\tstring dd3 = string("results.html");\n+\t\tstring dd3 = string("preview.html");\n \t\tarchive_entry_set_pathname(entry, dd3.c_str());\n \t\tarchive_entry_set_size(entry, htmlSize);\n \t\tarchive_entry_set_filetype(entry, AE_IFREG);\n@@ -218,7 +218,7 @@ void JASPExporter::saveDataArchive(archive *a, DataSetPackage *package, boost::f\n \t\t\tthrow runtime_error("Can\'t save jasp archive writing ERROR");\n \n \t\tarchive_entry_free(entry);\n-\t}*/\n+\t}\n }\n \n void JASPExporter::saveJASPArchive(archive *a, DataSetPackage *package, boost::function<void (const std::string &, int)> progressCallback)\ndiff --git a/JASP-Common/tempfiles.cpp b/JASP-Common/tempfiles.cpp\nindex ef569464c..9415f64d7 100644\n--- a/JASP-Common/tempfiles.cpp\n+++ b/JASP-Common/tempfiles.cpp\n@@ -16,6 +16,7 @@ using namespace boost;\n long _tempfiles_sessionId;\n string _tempfiles_sessionDirName;\n string _tempfiles_statusFileName;\n+string _tempfiles_clipboard;\n int _tempfiles_nextFileId;\n \n void tempfiles_init(long sessionId)\n@@ -36,6 +37,14 @@ void tempfiles_init(long sessionId)\n \n \t_tempfiles_statusFileName = ss.str();\n \n+\tstringstream ss2;\n+\tss2 << Dirs::tempDir();\n+\tss2 << "/";\n+\tss2 << "clipboard";\n+\n+\t_tempfiles_clipboard = ss2.str();\n+\n+\n \tfilesystem::path sessionPath = Utils::osPath(_tempfiles_sessionDirName);\n \n \tfilesystem::remove_all(sessionPath, error);\n@@ -44,6 +53,11 @@ void tempfiles_init(long sessionId)\n \tnowide::fstream f;\n \tf.open(_tempfiles_statusFileName.c_str(), ios_base::out);\n \tf.close();\n+\n+\tfilesystem::path clipboardPath = Utils::osPath(_tempfiles_clipboard);\n+\n+\t//if ( ! filesystem::exists(clipboardPath, error))\n+\t//\tfilesystem::create_directories(clipboardPath, error);\n }\n \n void tempfiles_attach(long sessionId)\n@@ -141,10 +155,37 @@ void tempfiles_deleteOrphans()\n \n \n void tempfiles_heartbeat()\n+\n {\n \tUtils::touch(_tempfiles_statusFileName);\n }\n \n+void tempfiles_purgeClipboard()\n+{\n+\tsystem::error_code error;\n+\n+\tfilesystem::path clipboardPath = Utils::osPath(_tempfiles_clipboard);\n+\n+\tfilesystem::remove_all(clipboardPath, error);\n+}\n+\n+string tempfiles_createSpecific_clipboard(const string &filename)\n+{\n+\tstringstream ss;\n+\tsystem::error_code error;\n+\n+\tss << _tempfiles_clipboard << "/" << filename;\n+\n+\tstring fullPath = ss.str();\n+\tfilesystem::path path = Utils::osPath(fullPath);\n+\n+\tfilesystem::path dirPath = path.parent_path();\n+\n+\tif (filesystem::exists(dirPath, error) == false || error)\n+\t\tfilesystem::create_directories(dirPath, error);\n+\n+\treturn fullPath;\n+}\n \n string tempfiles_createSpecific(const string &dir, const string &filename)\n {\ndiff --git a/JASP-Common/tempfiles.h b/JASP-Common/tempfiles.h\nindex 8423c1c1f..64d75825a 100644\n--- a/JASP-Common/tempfiles.h\n+++ b/JASP-Common/tempfiles.h\n@@ -8,6 +8,9 @@ void tempfiles_init(long sessionId);\n void tempfiles_attach(long sessionId);\n void tempfiles_heartbeat();\n \n+std::string tempfiles_createSpecific_clipboard(const std::string &filename);\n+void tempfiles_purgeClipboard();\n+\n void tempfiles_create(const std::string &extension, int id, std::string &root, std::string &relativePath);\n void tempfiles_createSpecific(const std::string &name, int id, std::string &root, std::string &relativePath);\n std::string tempfiles_createSpecific(const std::string &dir, const std::string &filename);\ndiff --git a/JASP-Desktop/JASP-Desktop.pro b/JASP-Desktop/JASP-Desktop.pro\nindex 11d631a41..c6a82af65 100644\n--- a/JASP-Desktop/JASP-Desktop.pro\n+++ b/JASP-Desktop/JASP-Desktop.pro\n@@ -130,9 +130,10 @@ SOURCES += main.cpp\\\n     activitylog.cpp \\\n     lrnamreply.cpp \\\n     lrnam.cpp \\\n-    analysisforms/r11tlearnform.cpp \\\n+    widgets/webview.cpp \\\n     widgets/button.cpp \\\n-    ribbons/ribbonr11tlearn.cpp\n+\tanalysisforms/r11tlearnform.cpp \\\n+\tribbons/ribbonr11tlearn.cpp\n \n HEADERS  += \\\n \tanalyses.h \\\n@@ -231,9 +232,10 @@ HEADERS  += \\\n     lrnamreply.h \\\n     lrnam.h \\\n     widgets/groupbox.h \\\n-    analysisforms/r11tlearnform.h \\\n     widgets/button.h \\\n-    ribbons/ribbonr11tlearn.h\n+    widgets/webview.h \\\n+\tanalysisforms/r11tlearnform.h \\\n+\tribbons/ribbonr11tlearn.h\n \n FORMS    += \\\n     backstageform.ui \\\n@@ -408,7 +410,11 @@ OTHER_FILES += \\\n \thtml/js/analysis.js \\\n     resources/icons/variable-nominal-text.svg \\\n     html/css/images/waiting.svg \\\n-    resources/icons/analysis-classical-sem.svg\n+    resources/icons/analysis-classical-sem.svg \\\n+    html/js/jaspwidget.js \\\n+    html/js/backbone-min-1.1.2.js \\\n+    html/css/images/resizer.png \\\n+    html/css/images/arrowsmalldownbtn.png\n \n HELP_PATH = $${PWD}/../Docs/help\n LIBRARY_PATH = $${PWD}/../Library\ndiff --git a/JASP-Desktop/asyncloader.cpp b/JASP-Desktop/asyncloader.cpp\nindex de4e19a09..abb454561 100644\n--- a/JASP-Desktop/asyncloader.cpp\n+++ b/JASP-Desktop/asyncloader.cpp\n@@ -72,6 +72,18 @@ void AsyncLoader::saveTask(const QString &filename, DataSetPackage *package)\n \tQString tempFilename = filename + tq(".tmp");\n \ttry\n \t{\n+\t\tint maxSleepTime = 2000;\n+\t\tint sleepTime = 100;\n+\t\tint delay = 0;\n+\t\twhile (package->analysesHTML.empty())\n+\t\t{\n+\t\t\tif (delay > maxSleepTime)\n+\t\t\t\tbreak;\n+\n+\t\t\tsleep(sleepTime);\n+\t\t\tdelay += sleepTime;\n+\t\t}\n+\n \t\tJASPExporter::saveDataSet(fq(tempFilename), package, boost::bind(&AsyncLoader::progressHandler, this, _1, _2));\n \n \t\tif ( ! Utils::renameOverwrite(fq(tempFilename), fq(filename)))\n@@ -92,3 +104,14 @@ void AsyncLoader::saveTask(const QString &filename, DataSetPackage *package)\n \t\temit saveFail(e.what());\n \t}\n }\n+\n+void AsyncLoader::sleep(int ms)\n+{\n+\n+#ifdef __WIN32__\n+\tSleep(uint(ms));\n+#else\n+\tstruct timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 };\n+\tnanosleep(&ts, NULL);\n+#endif\n+}\ndiff --git a/JASP-Desktop/asyncloader.h b/JASP-Desktop/asyncloader.h\nindex 564af1491..39a0e3427 100644\n--- a/JASP-Desktop/asyncloader.h\n+++ b/JASP-Desktop/asyncloader.h\n@@ -36,6 +36,7 @@ private slots:\n \n private:\n \n+\tvoid sleep(int ms);\n \tvoid progressHandler(std::string status, int progress);\n \tQThread _thread;\n \tDataSetLoader _loader;\ndiff --git a/JASP-Desktop/html/css/images/arrowsmalldownbtn.png b/JASP-Desktop/html/css/images/arrowsmalldownbtn.png\nnew file mode 100644\nindex 000000000..f7519d81b\nBinary files /dev/null and b/JASP-Desktop/html/css/images/arrowsmalldownbtn.png differ\ndiff --git a/JASP-Desktop/html/css/images/resizer.png b/JASP-Desktop/html/css/images/resizer.png\nnew file mode 100644\nindex 000000000..af075d36f\nBinary files /dev/null and b/JASP-Desktop/html/css/images/resizer.png differ\ndiff --git a/JASP-Desktop/html/css/theme-jasp.css b/JASP-Desktop/html/css/theme-jasp.css\nindex 50c33d823..a96f6426c 100644\n--- a/JASP-Desktop/html/css/theme-jasp.css\n+++ b/JASP-Desktop/html/css/theme-jasp.css\n@@ -157,9 +157,9 @@ svg > text\n \tfont-size: 120% ;\n }\n \n-.jasp-image {\n+.jasp-image, .jasp-images {\n \n-\tfloat: left ;\n+\tdisplay: inline-block;\n }\n \n .jasp-image {\n@@ -239,35 +239,16 @@ svg > text\n \n div.toolbar\n {\n-\tpadding-left : 1em ;\n \tdisplay: inline-block ;\n+\tposition: relative;\n }\n \n-div.toolbar-button\n-{\n-\twidth: 16px ;\n-\theight: 16px ;\n-\tbackground-size : 100% ;\n-\tcursor : pointer ;\n-\tdisplay: inline-block ;\n-\t\n-\tmargin-left: 3px ;\n-\tborder: 1px solid transparent ;\n-}\n \n div.toolbar-button:active {\n \n \tborder: 1px solid silver ;\n }\n \n-div.copy {\n-\tbackground : url(images/copy.png) no-repeat;\n-}\n-\n-div.cite {\n-\tbackground : url(images/cite.svg) no-repeat;\n-}\n-\n div.status\n {\n \twidth: 16px ;\n@@ -302,6 +283,8 @@ div.status.waiting {\n \tz-index: 100 ;\n }\n \n+\n+\n .jasp-image-holder .error-message-positioner {\n \n \tposition: absolute ;\n@@ -346,3 +329,49 @@ td.squash-left\n \tcolor: #3CAAE7 ;\n \n }\n+\n+.jasp-hide {\n+    display: none;\n+\tvisibility: hidden ;\n+}\n+\n+.jasp-resize {\n+    background: top left no-repeat url(images/resizer.png);\n+    right: 5px;\n+    cursor: nw-resize;\n+\tbottom: 16px;\n+\tposition: absolute;\n+    width: 16px;\n+    height: 16px;\n+}\n+\n+.jasp-toolbar h1, .jasp-toolbar h2, .jasp-toolbar h3, .jasp-toolbar div , .jasp-toolbar span {\n+\tdisplay: inline-block;\n+\tvertical-align: middle;\n+}\n+\n+div.toolbar-button\n+{\n+\twidth: 16px ;\n+\theight: 16px ;\n+\tbackground-size : 100% ;\n+\tcursor : pointer ;\n+\tdisplay : inline-block;\n+\tvertical-align:middle;\n+\t\n+\tmargin-left: 6px ;\n+\tborder: 1px solid transparent ;\n+}\n+\n+.jasp-menu {\n+    background: top left no-repeat url(images/arrowsmalldownbtn.png); \n+}\n+\n+.jasp-toolbar {\n+\tdisplay: block ;\n+\tcursor: pointer;\n+}\n+\n+.jasp-menu-selected {\n+\tbackground-color : #BFD9F7 ;\n+}\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/html.qrc b/JASP-Desktop/html/html.qrc\nindex d54c92e72..ca6094d79 100644\n--- a/JASP-Desktop/html/html.qrc\n+++ b/JASP-Desktop/html/html.qrc\n@@ -37,5 +37,9 @@\n         <file>css/images/cite.svg</file>\n         <file>css/images/running-large.gif</file>\n         <file>css/images/waiting-large.png</file>\n+        <file>js/backbone-min-1.1.2.js</file>\n+        <file>js/jaspwidgets.js</file>\n+        <file>css/images/arrowsmalldownbtn.png</file>\n+        <file>css/images/resizer.png</file>\n     </qresource>\n </RCC>\ndiff --git a/JASP-Desktop/html/index.html b/JASP-Desktop/html/index.html\nindex 27e41c21f..b324473c4 100644\n--- a/JASP-Desktop/html/index.html\n+++ b/JASP-Desktop/html/index.html\n@@ -9,6 +9,8 @@\n \t<script type="text/javascript" src="js/jquery-1.9.1.js"></script>\n \t<script type="text/javascript" src="js/jquery-ui-1.10.1.custom.js"></script>\n \t<script type="text/javascript" src="js/underscore-min.js"></script>\n+\t<script type="text/javascript" src="js/backbone-min-1.1.2.js"></script>\n+\t<script type="text/javascript" src="js/jaspwidgets.js"></script>\n \n \t<script type="text/javascript" src="js/table.js"></script>\n \t<script type="text/javascript" src="js/tables.js"></script>\ndiff --git a/JASP-Desktop/html/js/analysis.js b/JASP-Desktop/html/js/analysis.js\nindex c537cdb94..4ebd62036 100644\n--- a/JASP-Desktop/html/js/analysis.js\n+++ b/JASP-Desktop/html/js/analysis.js\n@@ -1,107 +1,186 @@\n-$.widget("jasp.analysis", {\n+JASPWidgets.Analysis = Backbone.Model.extend({\n+\tdefaults: {\n+\t\tid: -1,\n+\t\tresults: {},\n+\t\tstatus: "waiting",\n+\t\toptionschanged: []\n+\t}\n+});\n+\n+\n+JASPWidgets.Analyses = Backbone.Collection.extend({\n+\tmodel: JASPWidgets.Analysis\n+});\n+\n+\n+JASPWidgets.AnalysisView = JASPWidgets.View.extend({\n+\tviews: [],\n \n-\toptions: {\n-\t\tid : -1,\n-\t\tresults : { },\n-        status : "waiting",\n-        optionschanged : [ ]\n+\tinitialize: function () {\n+\n+\t\tthis.toolbar = new JASPWidgets.Toolbar({ className: "jasp-toolbar" })\n+\t\tthis.toolbar.setParent(this);\n \t},\n-\t_create: function () {\n-\t\tthis.element.addClass("jasp-analysis")\n-\t\tthis.refresh()\n+\n+\tevents: {\n+\t\t\'mouseenter\': \'_hoveringStart\',\n+\t\t\'mouseleave\': \'_hoveringEnd\',\n \t},\n-\t_setOptions: function (options) {\n \n-\t\tthis._super(options)\t\t\n-\t\tthis.refresh()\n+\t_hoveringStart: function (e) {\n+\t\tthis.toolbar.setVisibility(true);\n \t},\n-\t_render : function($element, result, status, metaEntry) {\n-\t\n-\t\tif (metaEntry.type == "title") {\n+\n+\t_hoveringEnd: function (e) {\n+\t\tthis.toolbar.setVisibility(false);\n+\t},\n+\n+\tannotateMenuClicked: function () {\n+\n+\t\treturn true;\n+\t},\n+\n+\tcopyMenuClicked: function () {\n \t\t\n-\t\t\t$element.append("<h1>" + result + "</h1>")\n+\t\treturn this.exportBegin({\n+\t\t\tformat: JASPWidgets.ExportProperties.format.html,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.copy,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.temporary\n+\t\t}, this.views);\n+\t},\n+\n+\texportMenuClicked: function () {\n+\n+\t\treturn this.exportBegin({\n+\t\t\tformat: JASPWidgets.ExportProperties.format.html,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.save,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.embedded\n+\t\t}, this.views);\n+\t},\n+\n+\texportBegin: function (exportParams) {\n+\t\tif (exportParams == undefined)\n+\t\t\texportParams = JASPWidgets.Exporter.params();\n+\t\telse if (exportParams.error)\n+\t\t\treturn false;\n+\t\t\n+\t\treturn JASPWidgets.Exporter.begin(this, exportParams, true);\n+\t},\n+\n+\texportComplete: function (exportParams, html) {\n+\t\tif (!exportParams.error)\n+\t\t\tpushHTMLToClipboard(html);\n+\t},\n+\n+\tmenuName: "Analysis",\n+\n+\trenderChildren: function ($element, result, status, metaEntry) {\n+\n+\t\tif (metaEntry.type == "title") {\n+\n+\t\t\tthis.toolbar.titleTag = "h1";\n+\t\t\tthis.toolbar.title = result;\n+\t\t\tthis.toolbar.render();\n+\t\t\t$element.append(this.toolbar.$el);\n \t\t}\n \t\telse if (metaEntry.type == "h1") {\n-\t\t\n+\n \t\t\t$element.append("<h2>" + result + "</h2>")\n \t\t}\n \t\telse if (metaEntry.type == "h2") {\n-\t\t\n+\n \t\t\t$element.append("<h3>" + result + "</h3>")\n \t\t}\n \t\telse {\n \n-\t\t\tvar item\n+\t\t\tif (!_.has(result, "status"))\n+\t\t\t\tresult.status = status;\n \n-\t\t\tif (_.isArray(result)) {\n-\t\t\n-\t\t\t\titem = $(\'<div></div>\')\n-\t\t\t\t\t.appendTo($element)\n-\t\t\t\t\t[metaEntry.type]({ items : result, status : status })\n-\t\t\n-\t\t\t}\n-\t\t\telse {\n+\t\t\tvar item = new JASPWidgets[metaEntry.type](result);\n+\n+\t\t\titem.on("CustomOptions:changed", function (options) {\n+\n+\t\t\t\tthis.trigger("optionschanged", this.model.get("id"), options)\n \n-\t\t\t\tif ( ! _.has(result, "status"))\n-\t\t\t\t\tresult.status = status\n+\t\t\t}, this);\n+\n+\t\t\tvar itemView;\n+\t\t\tif (_.isArray(result)) {\n \n-\t\t\t\titem = $(\'<div></div>\')\n-\t\t\t\t\t.appendTo($element)\n-\t\t\t\t\t[metaEntry.type](result)\n+\t\t\t\titem.each(function (subItem) {\n+\t\t\t\t\tif (!_.has(subItem, "status"))\n+\t\t\t\t\t\tsubItem.set("status", this.status);\n+\t\t\t\t}, result);\n+\t\t\t\titemView = new JASPWidgets[metaEntry.type + "View"]({ className: "jasp-" + metaEntry.type, collection: item });\n \t\t\t}\n-\t\t\n-\t\t\tvar self = this\n+\t\t\telse\n+\t\t\t\titemView = new JASPWidgets[metaEntry.type + "View"]({ className: "jasp-" + metaEntry.type, model: item });\n \n-\t\t\titem.bind("imagesitemoptionschanged", function(event, data) {\n+\t\t\tthis.listenTo(itemView, "toolbar:showMenu", function (obj, options) {\n \n-\t\t\t\tdata = { id : self.options.id, options : data }\n-\t\t\t\tself._trigger("optionschanged", null, data)\n-\t\t\n-\t\t\t})\n-\t\t\n-\t\t\titem.bind("imageitemoptionschanged", function(event, data) {\n+\t\t\t\tthis.trigger("toolbar:showMenu", obj, options);\n+\t\t\t});\n \n-\t\t\t\tdata = { id : self.options.id, options : data }\n-\t\t\t\tself._trigger("optionschanged", null, data)\n-\t\t\n-\t\t\t})\n-\t\t\n+\t\t\tthis.views.push(itemView);\n+\n+\t\t\titemView.render();\n+\t\t\t$element.append(itemView.$el);\n \t\t}\n-\t\n+\n \t},\n-\trefresh: function () {\n-\t\t\n-\t\tthis.element.text("")\n-\t\t\n+\n+\trender: function () {\n+\n+\t\tthis.destroyViews();\n+\n+\t\tthis.toolbar.$el.detach();\n+\n+\t\tthis.$el.empty();\n+\n \t\tvar $innerElement = $(\'<div class="jasp-analysis-inner"></div>\')\n \n-        if (this.options.results.error) {\n-        \n-        \tvar error = this.options.results.errorMessage\n-        \t\n-        \terror = error.replace(/\\n/g, \'<br>\')\n-        \terror = error.replace(/  /g, \'&nbsp;&nbsp;\')\n-        \n-            $innerElement.append(\'<div class="error-message-box ui-state-error"><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>\' + error + \'</div>\')\n-            \n-        }\n-\t\telse if (this.options.results[".meta"]) {\n-\t\t\n-\t\t\tvar results = this.options.results\n+\t\tvar results = this.model.get("results");\n+\t\tif (results.error) {\n+\n+\t\t\tvar error = results.errorMessage\n+\n+\t\t\terror = error.replace(/\\n/g, \'<br>\')\n+\t\t\terror = error.replace(/  /g, \'&nbsp;&nbsp;\')\n+\n+\t\t\t$innerElement.append(\'<div class="error-message-box ui-state-error"><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>\' + error + \'</div>\')\n+\n+\t\t}\n+\t\telse if (results[".meta"]) {\n+\n \t\t\tvar meta = results[".meta"]\n \n \t\t\tfor (var i = 0; i < meta.length; i++) {\n-\t\t\t\n+\n \t\t\t\tif (_.has(results, meta[i].name))\n-\t\t\t\t\tthis._render($innerElement, results[meta[i].name], this.options.status, meta[i])\n+\t\t\t\t\tthis.renderChildren($innerElement, results[meta[i].name], this.model.get("status"), meta[i])\n \t\t\t}\n-\t\t\t\n+\n \t\t}\n \n-\t\tthis.element.append($innerElement)\n-\t\t\n+\t\tthis.$el.append($innerElement)\n+\n+\t\treturn this;\n+\t},\n+\tunselect: function () {\n+\t\tthis.$el.removeClass("selected");\n+\t\t//this.$el.addClass("unselected")\n+\t},\n+\tselect: function () {\n+\t\tthis.$el.addClass("selected")\n+\t\t//this.$el.removeClass("unselected");\n+\t},\n+\tdestroyViews: function() {\n+\t\tfor (var i = 0; i < this.views.length; i++)\n+\t\t\tthis.views[i].close();\n+\n+\t\tthis.views = [];\n \t},\n-\t_destroy: function () {\n-\t\tthis.element.removeClass("jasp-analysis").text("")\n+\tonClose: function () {\n+\t\tthis.destroyViews();\n \t}\n-})\n+});\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/backbone-min-1.1.2.js b/JASP-Desktop/html/js/backbone-min-1.1.2.js\nnew file mode 100644\nindex 000000000..8ea4b13d3\n--- /dev/null\n+++ b/JASP-Desktop/html/js/backbone-min-1.1.2.js\n@@ -0,0 +1,2 @@\n+(function(t,e){if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,s){t.Backbone=e(t,s,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore");e(t,exports,i)}else{t.Backbone=e(t,{},t._,t.jQuery||t.Zepto||t.ender||t.$)}})(this,function(t,e,i,r){var s=t.Backbone;var n=[];var a=n.push;var o=n.slice;var h=n.splice;e.VERSION="1.1.2";e.$=r;e.noConflict=function(){t.Backbone=s;return this};e.emulateHTTP=false;e.emulateJSON=false;var u=e.Events={on:function(t,e,i){if(!c(this,"on",t,[e,i])||!e)return this;this._events||(this._events={});var r=this._events[t]||(this._events[t]=[]);r.push({callback:e,context:i,ctx:i||this});return this},once:function(t,e,r){if(!c(this,"once",t,[e,r])||!e)return this;var s=this;var n=i.once(function(){s.off(t,n);e.apply(this,arguments)});n._callback=e;return this.on(t,n,r)},off:function(t,e,r){var s,n,a,o,h,u,l,f;if(!this._events||!c(this,"off",t,[e,r]))return this;if(!t&&!e&&!r){this._events=void 0;return this}o=t?[t]:i.keys(this._events);for(h=0,u=o.length;h<u;h++){t=o[h];if(a=this._events[t]){this._events[t]=s=[];if(e||r){for(l=0,f=a.length;l<f;l++){n=a[l];if(e&&e!==n.callback&&e!==n.callback._callback||r&&r!==n.context){s.push(n)}}}if(!s.length)delete this._events[t]}}return this},trigger:function(t){if(!this._events)return this;var e=o.call(arguments,1);if(!c(this,"trigger",t,e))return this;var i=this._events[t];var r=this._events.all;if(i)f(i,e);if(r)f(r,arguments);return this},stopListening:function(t,e,r){var s=this._listeningTo;if(!s)return this;var n=!e&&!r;if(!r&&typeof e==="object")r=this;if(t)(s={})[t._listenId]=t;for(var a in s){t=s[a];t.off(e,r,this);if(n||i.isEmpty(t._events))delete this._listeningTo[a]}return this}};var l=/\\s+/;var c=function(t,e,i,r){if(!i)return true;if(typeof i==="object"){for(var s in i){t[e].apply(t,[s,i[s]].concat(r))}return false}if(l.test(i)){var n=i.split(l);for(var a=0,o=n.length;a<o;a++){t[e].apply(t,[n[a]].concat(r))}return false}return true};var f=function(t,e){var i,r=-1,s=t.length,n=e[0],a=e[1],o=e[2];switch(e.length){case 0:while(++r<s)(i=t[r]).callback.call(i.ctx);return;case 1:while(++r<s)(i=t[r]).callback.call(i.ctx,n);return;case 2:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a);return;case 3:while(++r<s)(i=t[r]).callback.call(i.ctx,n,a,o);return;default:while(++r<s)(i=t[r]).callback.apply(i.ctx,e);return}};var d={listenTo:"on",listenToOnce:"once"};i.each(d,function(t,e){u[e]=function(e,r,s){var n=this._listeningTo||(this._listeningTo={});var a=e._listenId||(e._listenId=i.uniqueId("l"));n[a]=e;if(!s&&typeof r==="object")s=this;e[t](r,s,this);return this}});u.bind=u.on;u.unbind=u.off;i.extend(e,u);var p=e.Model=function(t,e){var r=t||{};e||(e={});this.cid=i.uniqueId("c");this.attributes={};if(e.collection)this.collection=e.collection;if(e.parse)r=this.parse(r,e)||{};r=i.defaults({},r,i.result(this,"defaults"));this.set(r,e);this.changed={};this.initialize.apply(this,arguments)};i.extend(p.prototype,u,{changed:null,validationError:null,idAttribute:"id",initialize:function(){},toJSON:function(t){return i.clone(this.attributes)},sync:function(){return e.sync.apply(this,arguments)},get:function(t){return this.attributes[t]},escape:function(t){return i.escape(this.get(t))},has:function(t){return this.get(t)!=null},set:function(t,e,r){var s,n,a,o,h,u,l,c;if(t==null)return this;if(typeof t==="object"){n=t;r=e}else{(n={})[t]=e}r||(r={});if(!this._validate(n,r))return false;a=r.unset;h=r.silent;o=[];u=this._changing;this._changing=true;if(!u){this._previousAttributes=i.clone(this.attributes);this.changed={}}c=this.attributes,l=this._previousAttributes;if(this.idAttribute in n)this.id=n[this.idAttribute];for(s in n){e=n[s];if(!i.isEqual(c[s],e))o.push(s);if(!i.isEqual(l[s],e)){this.changed[s]=e}else{delete this.changed[s]}a?delete c[s]:c[s]=e}if(!h){if(o.length)this._pending=r;for(var f=0,d=o.length;f<d;f++){this.trigger("change:"+o[f],this,c[o[f]],r)}}if(u)return this;if(!h){while(this._pending){r=this._pending;this._pending=false;this.trigger("change",this,r)}}this._pending=false;this._changing=false;return this},unset:function(t,e){return this.set(t,void 0,i.extend({},e,{unset:true}))},clear:function(t){var e={};for(var r in this.attributes)e[r]=void 0;return this.set(e,i.extend({},t,{unset:true}))},hasChanged:function(t){if(t==null)return!i.isEmpty(this.changed);return i.has(this.changed,t)},changedAttributes:function(t){if(!t)return this.hasChanged()?i.clone(this.changed):false;var e,r=false;var s=this._changing?this._previousAttributes:this.attributes;for(var n in t){if(i.isEqual(s[n],e=t[n]))continue;(r||(r={}))[n]=e}return r},previous:function(t){if(t==null||!this._previousAttributes)return null;return this._previousAttributes[t]},previousAttributes:function(){return i.clone(this._previousAttributes)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=this;var r=t.success;t.success=function(i){if(!e.set(e.parse(i,t),t))return false;if(r)r(e,i,t);e.trigger("sync",e,i,t)};q(this,t);return this.sync("read",this,t)},save:function(t,e,r){var s,n,a,o=this.attributes;if(t==null||typeof t==="object"){s=t;r=e}else{(s={})[t]=e}r=i.extend({validate:true},r);if(s&&!r.wait){if(!this.set(s,r))return false}else{if(!this._validate(s,r))return false}if(s&&r.wait){this.attributes=i.extend({},o,s)}if(r.parse===void 0)r.parse=true;var h=this;var u=r.success;r.success=function(t){h.attributes=o;var e=h.parse(t,r);if(r.wait)e=i.extend(s||{},e);if(i.isObject(e)&&!h.set(e,r)){return false}if(u)u(h,t,r);h.trigger("sync",h,t,r)};q(this,r);n=this.isNew()?"create":r.patch?"patch":"update";if(n==="patch")r.attrs=s;a=this.sync(n,this,r);if(s&&r.wait)this.attributes=o;return a},destroy:function(t){t=t?i.clone(t):{};var e=this;var r=t.success;var s=function(){e.trigger("destroy",e,e.collection,t)};t.success=function(i){if(t.wait||e.isNew())s();if(r)r(e,i,t);if(!e.isNew())e.trigger("sync",e,i,t)};if(this.isNew()){t.success();return false}q(this,t);var n=this.sync("delete",this,t);if(!t.wait)s();return n},url:function(){var t=i.result(this,"urlRoot")||i.result(this.collection,"url")||M();if(this.isNew())return t;return t.replace(/([^\\/])$/,"$1/")+encodeURIComponent(this.id)},parse:function(t,e){return t},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return!this.has(this.idAttribute)},isValid:function(t){return this._validate({},i.extend(t||{},{validate:true}))},_validate:function(t,e){if(!e.validate||!this.validate)return true;t=i.extend({},this.attributes,t);var r=this.validationError=this.validate(t,e)||null;if(!r)return true;this.trigger("invalid",this,r,i.extend(e,{validationError:r}));return false}});var v=["keys","values","pairs","invert","pick","omit"];i.each(v,function(t){p.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.attributes);return i[t].apply(i,e)}});var g=e.Collection=function(t,e){e||(e={});if(e.model)this.model=e.model;if(e.comparator!==void 0)this.comparator=e.comparator;this._reset();this.initialize.apply(this,arguments);if(t)this.reset(t,i.extend({silent:true},e))};var m={add:true,remove:true,merge:true};var y={add:true,remove:false};i.extend(g.prototype,u,{model:p,initialize:function(){},toJSON:function(t){return this.map(function(e){return e.toJSON(t)})},sync:function(){return e.sync.apply(this,arguments)},add:function(t,e){return this.set(t,i.extend({merge:false},e,y))},remove:function(t,e){var r=!i.isArray(t);t=r?[t]:i.clone(t);e||(e={});var s,n,a,o;for(s=0,n=t.length;s<n;s++){o=t[s]=this.get(t[s]);if(!o)continue;delete this._byId[o.id];delete this._byId[o.cid];a=this.indexOf(o);this.models.splice(a,1);this.length--;if(!e.silent){e.index=a;o.trigger("remove",o,this,e)}this._removeReference(o,e)}return r?t[0]:t},set:function(t,e){e=i.defaults({},e,m);if(e.parse)t=this.parse(t,e);var r=!i.isArray(t);t=r?t?[t]:[]:i.clone(t);var s,n,a,o,h,u,l;var c=e.at;var f=this.model;var d=this.comparator&&c==null&&e.sort!==false;var v=i.isString(this.comparator)?this.comparator:null;var g=[],y=[],_={};var b=e.add,w=e.merge,x=e.remove;var E=!d&&b&&x?[]:false;for(s=0,n=t.length;s<n;s++){h=t[s]||{};if(h instanceof p){a=o=h}else{a=h[f.prototype.idAttribute||"id"]}if(u=this.get(a)){if(x)_[u.cid]=true;if(w){h=h===o?o.attributes:h;if(e.parse)h=u.parse(h,e);u.set(h,e);if(d&&!l&&u.hasChanged(v))l=true}t[s]=u}else if(b){o=t[s]=this._prepareModel(h,e);if(!o)continue;g.push(o);this._addReference(o,e)}o=u||o;if(E&&(o.isNew()||!_[o.id]))E.push(o);_[o.id]=true}if(x){for(s=0,n=this.length;s<n;++s){if(!_[(o=this.models[s]).cid])y.push(o)}if(y.length)this.remove(y,e)}if(g.length||E&&E.length){if(d)l=true;this.length+=g.length;if(c!=null){for(s=0,n=g.length;s<n;s++){this.models.splice(c+s,0,g[s])}}else{if(E)this.models.length=0;var k=E||g;for(s=0,n=k.length;s<n;s++){this.models.push(k[s])}}}if(l)this.sort({silent:true});if(!e.silent){for(s=0,n=g.length;s<n;s++){(o=g[s]).trigger("add",o,this,e)}if(l||E&&E.length)this.trigger("sort",this,e)}return r?t[0]:t},reset:function(t,e){e||(e={});for(var r=0,s=this.models.length;r<s;r++){this._removeReference(this.models[r],e)}e.previousModels=this.models;this._reset();t=this.add(t,i.extend({silent:true},e));if(!e.silent)this.trigger("reset",this,e);return t},push:function(t,e){return this.add(t,i.extend({at:this.length},e))},pop:function(t){var e=this.at(this.length-1);this.remove(e,t);return e},unshift:function(t,e){return this.add(t,i.extend({at:0},e))},shift:function(t){var e=this.at(0);this.remove(e,t);return e},slice:function(){return o.apply(this.models,arguments)},get:function(t){if(t==null)return void 0;return this._byId[t]||this._byId[t.id]||this._byId[t.cid]},at:function(t){return this.models[t]},where:function(t,e){if(i.isEmpty(t))return e?void 0:[];return this[e?"find":"filter"](function(e){for(var i in t){if(t[i]!==e.get(i))return false}return true})},findWhere:function(t){return this.where(t,true)},sort:function(t){if(!this.comparator)throw new Error("Cannot sort a set without a comparator");t||(t={});if(i.isString(this.comparator)||this.comparator.length===1){this.models=this.sortBy(this.comparator,this)}else{this.models.sort(i.bind(this.comparator,this))}if(!t.silent)this.trigger("sort",this,t);return this},pluck:function(t){return i.invoke(this.models,"get",t)},fetch:function(t){t=t?i.clone(t):{};if(t.parse===void 0)t.parse=true;var e=t.success;var r=this;t.success=function(i){var s=t.reset?"reset":"set";r[s](i,t);if(e)e(r,i,t);r.trigger("sync",r,i,t)};q(this,t);return this.sync("read",this,t)},create:function(t,e){e=e?i.clone(e):{};if(!(t=this._prepareModel(t,e)))return false;if(!e.wait)this.add(t,e);var r=this;var s=e.success;e.success=function(t,i){if(e.wait)r.add(t,e);if(s)s(t,i,e)};t.save(null,e);return t},parse:function(t,e){return t},clone:function(){return new this.constructor(this.models)},_reset:function(){this.length=0;this.models=[];this._byId={}},_prepareModel:function(t,e){if(t instanceof p)return t;e=e?i.clone(e):{};e.collection=this;var r=new this.model(t,e);if(!r.validationError)return r;this.trigger("invalid",this,r.validationError,e);return false},_addReference:function(t,e){this._byId[t.cid]=t;if(t.id!=null)this._byId[t.id]=t;if(!t.collection)t.collection=this;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(e&&t==="change:"+e.idAttribute){delete this._byId[e.previous(e.idAttribute)];if(e.id!=null)this._byId[e.id]=e}this.trigger.apply(this,arguments)}});var _=["forEach","each","map","collect","reduce","foldl","inject","reduceRight","foldr","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","max","min","toArray","size","first","head","take","initial","rest","tail","drop","last","without","difference","indexOf","shuffle","lastIndexOf","isEmpty","chain","sample"];i.each(_,function(t){g.prototype[t]=function(){var e=o.call(arguments);e.unshift(this.models);return i[t].apply(i,e)}});var b=["groupBy","countBy","sortBy","indexBy"];i.each(b,function(t){g.prototype[t]=function(e,r){var s=i.isFunction(e)?e:function(t){return t.get(e)};return i[t](this.models,s,r)}});var w=e.View=function(t){this.cid=i.uniqueId("view");t||(t={});i.extend(this,i.pick(t,E));this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()};var x=/^(\\S+)\\s*(.*)$/;var E=["model","collection","el","id","attributes","className","tagName","events"];i.extend(w.prototype,u,{tagName:"div",$:function(t){return this.$el.find(t)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();this.stopListening();return this},setElement:function(t,i){if(this.$el)this.undelegateEvents();this.$el=t instanceof e.$?t:e.$(t);this.el=this.$el[0];if(i!==false)this.delegateEvents();return this},delegateEvents:function(t){if(!(t||(t=i.result(this,"events"))))return this;this.undelegateEvents();for(var e in t){var r=t[e];if(!i.isFunction(r))r=this[t[e]];if(!r)continue;var s=e.match(x);var n=s[1],a=s[2];r=i.bind(r,this);n+=".delegateEvents"+this.cid;if(a===""){this.$el.on(n,r)}else{this.$el.on(n,a,r)}}return this},undelegateEvents:function(){this.$el.off(".delegateEvents"+this.cid);return this},_ensureElement:function(){if(!this.el){var t=i.extend({},i.result(this,"attributes"));if(this.id)t.id=i.result(this,"id");if(this.className)t["class"]=i.result(this,"className");var r=e.$("<"+i.result(this,"tagName")+">").attr(t);this.setElement(r,false)}else{this.setElement(i.result(this,"el"),false)}}});e.sync=function(t,r,s){var n=T[t];i.defaults(s||(s={}),{emulateHTTP:e.emulateHTTP,emulateJSON:e.emulateJSON});var a={type:n,dataType:"json"};if(!s.url){a.url=i.result(r,"url")||M()}if(s.data==null&&r&&(t==="create"||t==="update"||t==="patch")){a.contentType="application/json";a.data=JSON.stringify(s.attrs||r.toJSON(s))}if(s.emulateJSON){a.contentType="application/x-www-form-urlencoded";a.data=a.data?{model:a.data}:{}}if(s.emulateHTTP&&(n==="PUT"||n==="DELETE"||n==="PATCH")){a.type="POST";if(s.emulateJSON)a.data._method=n;var o=s.beforeSend;s.beforeSend=function(t){t.setRequestHeader("X-HTTP-Method-Override",n);if(o)return o.apply(this,arguments)}}if(a.type!=="GET"&&!s.emulateJSON){a.processData=false}if(a.type==="PATCH"&&k){a.xhr=function(){return new ActiveXObject("Microsoft.XMLHTTP")}}var h=s.xhr=e.ajax(i.extend(a,s));r.trigger("request",r,h,s);return h};var k=typeof window!=="undefined"&&!!window.ActiveXObject&&!(window.XMLHttpRequest&&(new XMLHttpRequest).dispatchEvent);var T={create:"POST",update:"PUT",patch:"PATCH","delete":"DELETE",read:"GET"};e.ajax=function(){return e.$.ajax.apply(e.$,arguments)};var $=e.Router=function(t){t||(t={});if(t.routes)this.routes=t.routes;this._bindRoutes();this.initialize.apply(this,arguments)};var S=/\\((.*?)\\)/g;var H=/(\\(\\?)?:\\w+/g;var A=/\\*\\w+/g;var I=/[\\-{}\\[\\]+?.,\\\\\\^$|#\\s]/g;i.extend($.prototype,u,{initialize:function(){},route:function(t,r,s){if(!i.isRegExp(t))t=this._routeToRegExp(t);if(i.isFunction(r)){s=r;r=""}if(!s)s=this[r];var n=this;e.history.route(t,function(i){var a=n._extractParameters(t,i);n.execute(s,a);n.trigger.apply(n,["route:"+r].concat(a));n.trigger("route",r,a);e.history.trigger("route",n,r,a)});return this},execute:function(t,e){if(t)t.apply(this,e)},navigate:function(t,i){e.history.navigate(t,i);return this},_bindRoutes:function(){if(!this.routes)return;this.routes=i.result(this,"routes");var t,e=i.keys(this.routes);while((t=e.pop())!=null){this.route(t,this.routes[t])}},_routeToRegExp:function(t){t=t.replace(I,"\\\\$&").replace(S,"(?:$1)?").replace(H,function(t,e){return e?t:"([^/?]+)"}).replace(A,"([^?]*?)");return new RegExp("^"+t+"(?:\\\\?([\\\\s\\\\S]*))?$")},_extractParameters:function(t,e){var r=t.exec(e).slice(1);return i.map(r,function(t,e){if(e===r.length-1)return t||null;return t?decodeURIComponent(t):null})}});var N=e.History=function(){this.handlers=[];i.bindAll(this,"checkUrl");if(typeof window!=="undefined"){this.location=window.location;this.history=window.history}};var R=/^[#\\/]|\\s+$/g;var O=/^\\/+|\\/+$/g;var P=/msie [\\w.]+/;var C=/\\/$/;var j=/#.*$/;N.started=false;i.extend(N.prototype,u,{interval:50,atRoot:function(){return this.location.pathname.replace(/[^\\/]$/,"$&/")===this.root},getHash:function(t){var e=(t||this).location.href.match(/#(.*)$/);return e?e[1]:""},getFragment:function(t,e){if(t==null){if(this._hasPushState||!this._wantsHashChange||e){t=decodeURI(this.location.pathname+this.location.search);var i=this.root.replace(C,"");if(!t.indexOf(i))t=t.slice(i.length)}else{t=this.getHash()}}return t.replace(R,"")},start:function(t){if(N.started)throw new Error("Backbone.history has already been started");N.started=true;this.options=i.extend({root:"/"},this.options,t);this.root=this.options.root;this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.options.pushState&&this.history&&this.history.pushState);var r=this.getFragment();var s=document.documentMode;var n=P.exec(navigator.userAgent.toLowerCase())&&(!s||s<=7);this.root=("/"+this.root+"/").replace(O,"/");if(n&&this._wantsHashChange){var a=e.$(\'<iframe src="javascript:0" tabindex="-1">\');this.iframe=a.hide().appendTo("body")[0].contentWindow;this.navigate(r)}if(this._hasPushState){e.$(window).on("popstate",this.checkUrl)}else if(this._wantsHashChange&&"onhashchange"in window&&!n){e.$(window).on("hashchange",this.checkUrl)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}this.fragment=r;var o=this.location;if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){this.fragment=this.getFragment(null,true);this.location.replace(this.root+"#"+this.fragment);return true}else if(this._hasPushState&&this.atRoot()&&o.hash){this.fragment=this.getHash().replace(R,"");this.history.replaceState({},document.title,this.root+this.fragment)}}if(!this.options.silent)return this.loadUrl()},stop:function(){e.$(window).off("popstate",this.checkUrl).off("hashchange",this.checkUrl);if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);N.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getFragment(this.getHash(this.iframe))}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){t=this.fragment=this.getFragment(t);return i.any(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!N.started)return false;if(!e||e===true)e={trigger:!!e};var i=this.root+(t=this.getFragment(t||""));t=t.replace(j,"");if(this.fragment===t)return;this.fragment=t;if(t===""&&i!=="/")i=i.slice(0,-1);if(this._hasPushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,i)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getFragment(this.getHash(this.iframe))){if(!e.replace)this.iframe.document.open().close();this._updateHash(this.iframe.location,t,e.replace)}}else{return this.location.assign(i)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new N;var U=function(t,e){var r=this;var s;if(t&&i.has(t,"constructor")){s=t.constructor}else{s=function(){return r.apply(this,arguments)}}i.extend(s,r,e);var n=function(){this.constructor=s};n.prototype=r.prototype;s.prototype=new n;if(t)i.extend(s.prototype,t);s.__super__=r.prototype;return s};p.extend=g.extend=$.extend=w.extend=N.extend=U;var M=function(){throw new Error(\'A "url" property or function must be specified\')};var q=function(t,e){var i=e.error;e.error=function(r){if(i)i(t,r,e);t.trigger("error",t,r,e)}};return e});\n+//# sourceMappingURL=backbone-min.map\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/image.js b/JASP-Desktop/html/js/image.js\nindex 103c4514b..1212c523c 100644\n--- a/JASP-Desktop/html/js/image.js\n+++ b/JASP-Desktop/html/js/image.js\n@@ -1,113 +1,225 @@\n-$.widget("jasp.image", {\n+JASPWidgets.image = JASPWidgets.Resizeable.extend({\n \n-\toptions: {\n+\tdefaults: {\n \t\ttitle: "",\n \t\twidth: 480,\n \t\theight: 320,\n-        data: null,\n-        status : "waiting",\n-        resize : [ ],\n-        custom : null,\n-        customchanged : [ ],\n-        itemoptionschanged : [ ],\n-        error : null\n+\t\tdata: null,\n+\t\tstatus: "waiting",\n+\t\tcustom: null,\n+\t\terror: null\n+\t}\n+});\n+\n+JASPWidgets.imageView = JASPWidgets.View.extend({\n+\n+\tinitialize: function () {\n+\n+\t\tthis.toolbar = new JASPWidgets.Toolbar({ className: "jasp-toolbar" })\n+\t\tthis.toolbar.setParent(this);\n+\n+\t\tthis.resizer = new JASPWidgets.ResizeableView({ model: this.model, className: "jasp-resize" });\n+\n+\t\tthis.listenTo(this.resizer, "ResizeableView:resized", this.onResized)\n+\t\tthis.listenTo(this.resizer, "ResizeableView:resizeStart", this.onResizeStart)\n+\t\tthis.listenTo(this.resizer, "ResizeableView:resizeStop", this.onResizeStop)\n+\t\tvar self = this;\n+\t\tthis.resizer.resizeTargetElement = function () {\n+\t\t\tvar t = self.$(".jasp-image-holder");\n+\t\t\treturn self.$(".jasp-image-holder");\n+\t\t};\n+\t\tthis.resizer.resizeDisabled = function () {\n+\t\t\tvar custom = self.model.get("custom");\n+\t\t\treturn custom === null;\n+\t\t};\n \t},\n-\t_create: function () {\n-\t\tthis.element.addClass("jasp-image")\n-\t\tthis.imageElement = null\n-\t\tthis.refresh()\n+\n+\tonResized: function (w, h) {\n+\t\tvar options = {};\n+\t\tvar custom = this.model.get("custom");\n+\t\tif (custom !== null) {\n+\t\t\tif (_.has(custom, "width"))\n+\t\t\t\toptions[custom.width] = w;\n+\t\t\tif (_.has(custom, "height"))\n+\t\t\t\toptions[custom.height] = h;\n+\t\t}\n+\n+\t\tthis.model.trigger("CustomOptions:changed", options);\n+\t},\n+\n+\tonResizeStart: function (w, h) {\n+\t\tthis.$el.addClass("jasp-image-resizable");\n \t},\n-\t_setOptions: function (options) {\n \n-\t\tthis._super(options)\t\t\n-\t\tthis.refresh()\n+\tonResizeStop: function (w, h) {\n+\t\tthis.$el.removeClass("jasp-image-resizable");\n \t},\n-\t_startResize : function(event, ui) {\n-\t\n-\t\tthis.imageElement.addClass("jasp-image-resizable")\n+\n+\tevents: {\n+\t\t\t\'mouseenter\': \'_hoveringStart\',\n+\t\t\t\'mouseleave\': \'_hoveringEnd\',\n \t},\n-\t_resize : function(event, ui) {\n-\t\n-\t\tthis._trigger("resize", event, ui)\n+\n+\t_hoveringStart: function (e) {\n+\t\tthis.toolbar.setVisibility(true);\n+\t\tthis.resizer.setVisibility(true);\n \t},\n-\t_stopResize : function(event, ui) {\n-\n-\t\tthis.imageElement.removeClass("jasp-image-resizable")\n-\n-\t\tvar custom = this.options.custom\n-\n-\t\tvar options = { }\n-\t\t\n-\t\tif (_.has(custom, "width"))\n-\t\t\toptions[custom.width] = ui.size.width\n-\t\tif (_.has(custom, "height"))\n-\t\t\toptions[custom.height] = ui.size.height\n-\t\t\t\n-\t\tthis._trigger("customchanged", null, options)\n-\t\tthis._trigger("itemoptionschanged", null, options)\n-\t\t\n+\n+\t_hoveringEnd: function (e) {\n+\t\tthis.toolbar.setVisibility(false);\n+\t\tthis.resizer.setVisibility(false);\n \t},\n-\trefresh: function () {\n-\t\t\n+\n+\trender: function () {\n \t\tvar html = \'\'\n-\t\t\n-\t\tif (this.options.title) {\n-\t\t\n-\t\t\thtml += \'<h2>\' + this.options.title + \'</h2>\'\n+\t\tvar title = this.model.get("title");\n+\t\tvar status = this.model.get("status");\n+\t\tvar error = this.model.get("error");\n+\t\tvar data = this.model.get("data");\n+\t\tvar custom = this.model.get("custom");\n+\n+\t\tif (title) {\n+\t\t\tthis.toolbar.title = title;\n+\t\t\tthis.toolbar.titleTag = "h2";\n+\t\t\t//this.$el.append(\'<h2>\' + title + \'</h2>\');\n \t\t}\n-\t\t\n+\n+\t\tthis.toolbar.render();\n+\t\tthis.$el.append(this.toolbar.$el);\n+\n \t\tvar classes = ""\n-\t\tif (this.options.status)\n-\t\t\tclasses += this.options.status\n-\t\t\t\n-\t\tif (this.options.error)\n+\t\tif (status)\n+\t\t\tclasses += status\n+\n+\t\tif (error)\n \t\t\tclasses += " error-state"\n-\t\t\n-\t\thtml += \'<div class="jasp-image-holder \' + classes + \'" style="width : \' + this.options.width + \'px ; height : \' + this.options.height + \'px ; ">\'\n+\n+\t\thtml += \'<div class="jasp-image-holder \' + classes + \'>\'\n \n \t\thtml += \'<div class="jasp-image-image" style="\'\n \n-\t\tif (this.options.data) {\n+\t\tif (data) {\n+\t\t\thtml += \'background-image : url(\\\'\' + data + \'?x=\' + Math.random() + \'\\\') ;\'\n \n-\t\t\thtml += \'background-image : url(\\\'\' + this.options.data + \'?x=\' + Math.random() + \'\\\') ;\'\n \t\t\thtml += \'background-size : 100% 100% ;\'\n \t\t}\n-\t\t\n+\n \t\thtml += \'"></div>\'\n \n-\t\tif (this.options.error && this.options.error.errorMessage) {\n+\t\tif (error && error.errorMessage) {\n \n \t\t\thtml += \'<div  class="error-message-positioner">\'\n \t\t\thtml += \'<div  class="error-message-box ui-state-error">\'\n \t\t\thtml += \'<span class="error-message-symbol ui-icon ui-icon-alert"></span>\'\n-\t\t\thtml += \'<div  class="error-message-message">\' + this.options.error.errorMessage + \'</div>\'\n+\t\t\thtml += \'<div  class="error-message-message">\' + error.errorMessage + \'</div>\'\n \t\t\thtml += \'</div>\'\n \t\t\thtml += \'</div>\'\n \t\t}\n-\t\t\n+\n \t\thtml += \'<div class="jasp-image-loader"></div>\'\n-\t\t\n+\n \t\thtml += \'</div>\'\n \n-\t\tthis.element.html(html)\n-\t\t\n+\t\tthis.$el.append(html)\n+\n \t\tvar self = this\n \n-\t\tif (this.options.custom) {\n-\t\t\n-\t\t\tthis.imageElement = this.element.find(".jasp-image-holder")\n-\n-\t\t\tthis.imageElement.resizable( {\n-\t\t\t\tminWidth : 160,\n-\t\t\t\tminHeight: 160,\n-\t\t\t\tstart  : function(event, ui) { self._startResize(event, ui) },\n-\t\t\t\tstop   : function(event, ui) { self._stopResize(event, ui) },\n-\t\t\t\tresize : function(event, ui) { self._resize(event, ui) }\n-\t\t\t} )\n+\t\tthis.resizer.render();\n+\n+\t\treturn this;\n+\t},\n+\n+\texportBegin: function (exportParams) {\n+\n+\t\tif (exportParams == undefined)\n+\t\t\texportParams = JASPWidgets.Exporter.params();\n+\t\telse if (exportParams.error)\n+\t\t\treturn false;\n+\n+\t\tif (exportParams.format === JASPWidgets.ExportProperties.format.raw && exportParams.imageFormat === JASPWidgets.ExportProperties.imageFormat.resource) {\n+\t\t\texportParams.error = true;\n+\t\t}\n+\t\telse {\n+\n+\t\t\tvar data = this.model.get("data");\n+\n+\t\t\tif (exportParams.format === JASPWidgets.ExportProperties.format.html && exportParams.imageFormat === JASPWidgets.ExportProperties.imageFormat.resource) {\n+\t\t\t\tvar width = this.model.get("width");\n+\t\t\t\tvar height = this.model.get("height");\n+\t\t\t\tvar html = this._addHTMLWrapper(\'<div  style="background-image : url(\\\'\' + data + \'\\\'); width:\' + width + \'px; height:\' + height + \'px;"></div>\');\n+\n+\t\t\t\tthis.exportComplete(exportParams, html);\n+\t\t\t}\n+\t\t\telse {\n+\n+\t\t\t\tJASPWidgets.Encodings.base64Request(data, function (base64) {\n+\n+\t\t\t\t\tif (exportParams.format === JASPWidgets.ExportProperties.format.html) {\n+\n+\t\t\t\t\t\tvar width = this.model.get("width");\n+\t\t\t\t\t\tvar height = this.model.get("height");\n+\n+\t\t\t\t\t\tif (exportParams.imageFormat === JASPWidgets.ExportProperties.imageFormat.temporary) {\n+\t\t\t\t\t\t\tsaveImageBegin(data, base64, function (fullpath) {\n+\n+\t\t\t\t\t\t\t\tvar html = this._addHTMLWrapper(\'<img src="file:///\' + fullpath + \'" style="width:\' + width + \'px; height:\' + height + \'px;" />\\n\');\n+\t\t\t\t\t\t\t\tthis.exportComplete(exportParams, html);\n+\t\t\t\t\t\t\t}, this);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t\telse if (exportParams.imageFormat === JASPWidgets.ExportProperties.imageFormat.embedded) {\n+\t\t\t\t\t\t\tvar html = this._addHTMLWrapper(\'<div style="background-image : url(data:image/png;base64,\' + base64 + \'); width:\' + width + \'px; height:\' + height + \'px;"></div>\');\n+\t\t\t\t\t\t\tthis.exportComplete(exportParams, html);\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t\telse if (exportParams.format === JASPWidgets.ExportProperties.format.raw) {\n+\t\t\t\t\t\tthis.exportComplete(exportParams, base64);\n+\t\t\t\t\t}\n+\t\t\t\t\telse\n+\t\t\t\t\t\texportParams.error = true;\n+\n+\t\t\t\t}, this);\n+\t\t\t}\n \t\t}\n-\t\t\n+\n+\t\treturn true;\n+\t},\n+\n+\t_addHTMLWrapper: function(innerHTML)\n+\t{\n+\t\tvar title = this.model.get("title");\n+\t\tvar style = this.getStyleAttr();\n+\t\tvar text = \'<div \' + style + \'>\\n\';\n+\n+\t\tvar headerStyle = JASPWidgets.Exporter.getHeaderStyles(this.toolbar.$title());\n+\t\ttext += \'<\' + this.toolbar.titleTag + \' \' + headerStyle + \'>\' + title + \'</\' + this.toolbar.titleTag + \'>\\n\'\n+\t\ttext += innerHTML;\n+\t\ttext += \'</div>\\n\';\n+\n+\t\treturn text;\n+\t},\n+\n+\texportComplete: function (exportParams, data) {\n+\t\tif (!exportParams.error && exportParams.process == JASPWidgets.ExportProperties.process.copy) {\n+\t\t\tif (exportParams.format == JASPWidgets.ExportProperties.format.html)\n+\t\t\t\tpushHTMLToClipboard(data);\n+\t\t\telse if (exportParams.format == JASPWidgets.ExportProperties.format.raw)\n+\t\t\t\tpushImageToClipboard(data);\n+\t\t}\n+\t},\n+\n+\tcopyMenuClicked: function () {\n+\t\tthis.exportBegin({\n+\t\t\tformat: JASPWidgets.ExportProperties.format.raw,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.copy,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.temporary\n+\t\t});\n+\t\treturn true;\n \t},\n-\t_destroy: function () {\n-\t\tthis.element.removeClass("jasp-image").text("")\n+\n+\tmenuName: "Plot",\n+\n+\tonClose: function () {\n+\t\tthis.toolbar.close();\n+\t\tthis.resizer.close();\n \t}\n-})\n+});\ndiff --git a/JASP-Desktop/html/js/images.js b/JASP-Desktop/html/js/images.js\nindex 16979c299..1ab745975 100644\n--- a/JASP-Desktop/html/js/images.js\n+++ b/JASP-Desktop/html/js/images.js\n@@ -1,64 +1,44 @@\n-$.widget("jasp.images", {\n-\n-    options: {\n-        items : [ ],\n-        itemoptionschanged : [ ],\n-        status : "waiting"\n-    },\n-    _create: function () {\n-\n-        this.element.addClass("jasp-images")\n-\n-        this.images = $(this.element)\n-\n-        this.refresh()\n-    },\n-    _setOptions: function (options) {\n-        this._super(options)\n-\n-        this.refresh()\n-    },\n-    refresh: function () {\n-\n-        this.images.empty()\n-\n-        if (this.options.items && $.isArray(this.options.items) && this.options.items.length > 0)\n-        {\n-            for (var i = 0; i < this.options.items.length; i++)\n-            {\n-            \tvar options = this.options.items[i]\n-            \tif ( ! options["status"])\n-            \t\toptions["status"] = this.options.status\n-            \n-                var image = $(\'<div class="jasp-images-image"></div>\')\n-                image.image(options)\n-                this.images.append(image)\n-                \n-                var self = this\n-                var allImages = this.element.find(".jasp-image-holder");\n-                \n-\t\t\t\timage.bind("imageresize", function(event, ui) {\n-                \n-\t\t\t\t\t$(allImages)\n-\t\t\t\t\t\t.css("width", ui.size.width)\n-\t\t\t\t\t\t.css("height", ui.size.height)\n-\t\t\t\t\n-\t\t\t\t})\n-                \n-\t\t\t\timage.bind("imagecustomchanged", function(event, data) {\n-                \n-\t\t\t\t\tself._trigger("itemoptionschanged", null, data)\n-\t\t\t\t})\n-\t\t\t\t\n-            }\n-\n-        }\n-        else {\n-\n-        }\n-\n-    },\n-    _destroy: function () {\n-        this.element.removeClass("jasp-images").text("")\n-    }\n-})\n+JASPWidgets.images = Backbone.Collection.extend({\n+\n+\tmodel: JASPWidgets.image,\n+});\n+\n+JASPWidgets.imagesView = JASPWidgets.CollectionView.extend({\n+\n+\tcreateItemView: function (item) {\n+\t\tvar imageView = new JASPWidgets.imageView({ className: "jasp-images-image jasp-image", model: item });\n+\t\tthis.listenTo(imageView.resizer, "ResizeableView:resizeStart", this.onResizingStart);\n+\t\treturn imageView;\n+\t},\n+\n+\tonResizingStart: function (w, h) {\n+\t\tfor (var i = 0; i < this.views.length; i++) {\n+\t\t\tvar imageView = this.views[i];\n+\t\t\tif (imageView.resizer.isMouseResizing()) {\n+\t\t\t\tthis.listenTo(imageView.resizer, "ResizeableView:resized", this.onResized);\n+\t\t\t\tthis.listenTo(imageView.resizer, "ResizeableView:viewResized", this.onViewResized);\n+\t\t\t}\n+\t\t\telse\n+\t\t\t\timageView.resizer.resizeStart(w, h, true);\n+\t\t}\n+\t},\n+\n+\tonViewResized: function (w, h) {\n+\t\tfor (var i = 0; i < this.views.length; i++) {\n+\t\t\tvar imageView = this.views[i];\n+\t\t\tif (!imageView.resizer.isMouseResizing())\n+\t\t\t\timageView.resizer.resizeView(w, h);\n+\t\t}\n+\t},\n+\n+\tonResized: function (w, h) {\n+\t\tfor (var i = 0; i < this.views.length; i++) {\n+\t\t\tvar imageView = this.views[i];\n+\t\t\tif (imageView.resizer.isMouseResizing()) {\n+\t\t\t\tthis.stopListening(imageView.resizer);\n+\t\t\t}\n+\t\t\telse\n+\t\t\t\timageView.resizer.resizeStop(w, h);\n+\t\t}\n+\t}\n+});\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/jaspwidgets.js b/JASP-Desktop/html/js/jaspwidgets.js\nnew file mode 100644\nindex 000000000..063675c22\n--- /dev/null\n+++ b/JASP-Desktop/html/js/jaspwidgets.js\n@@ -0,0 +1,680 @@\n+var JASPWidgets = {};\n+\n+JASPWidgets.Encodings = {\n+\tgetBase64: function (arrayBuffer) {\n+\t\tvar base64 = \'\'\n+\t\tvar encodings = \'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\'\n+\n+\t\tvar bytes = new Uint8Array(arrayBuffer)\n+\t\tvar byteLength = bytes.byteLength\n+\t\tvar byteRemainder = byteLength % 3\n+\t\tvar mainLength = byteLength - byteRemainder\n+\n+\t\tvar a, b, c, d\n+\t\tvar chunk\n+\n+\t\t// Main loop deals with bytes in chunks of 3\n+\t\tfor (var i = 0; i < mainLength; i = i + 3) {\n+\t\t\t// Combine the three bytes into a single integer\n+\t\t\tchunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]\n+\n+\t\t\t// Use bitmasks to extract 6-bit segments from the triplet\n+\t\t\ta = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18\n+\t\t\tb = (chunk & 258048) >> 12 // 258048   = (2^6 - 1) << 12\n+\t\t\tc = (chunk & 4032) >> 6 // 4032     = (2^6 - 1) << 6\n+\t\t\td = chunk & 63               // 63       = 2^6 - 1\n+\n+\t\t\t// Convert the raw binary segments to the appropriate ASCII encoding\n+\t\t\tbase64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]\n+\t\t}\n+\n+\t\t// Deal with the remaining bytes and padding\n+\t\tif (byteRemainder == 1) {\n+\t\t\tchunk = bytes[mainLength]\n+\n+\t\t\ta = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2\n+\n+\t\t\t// Set the 4 least significant bits to zero\n+\t\t\tb = (chunk & 3) << 4 // 3   = 2^2 - 1\n+\n+\t\t\tbase64 += encodings[a] + encodings[b] + \'==\'\n+\t\t} else if (byteRemainder == 2) {\n+\t\t\tchunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]\n+\n+\t\t\ta = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10\n+\t\t\tb = (chunk & 1008) >> 4 // 1008  = (2^6 - 1) << 4\n+\n+\t\t\t// Set the 2 least significant bits to zero\n+\t\t\tc = (chunk & 15) << 2 // 15    = 2^4 - 1\n+\n+\t\t\tbase64 += encodings[a] + encodings[b] + encodings[c] + \'=\'\n+\t\t}\n+\n+\t\treturn base64\n+\t},\n+\n+\tbase64Request: function (path, callback, context) {\n+\t\tvar xhr = new XMLHttpRequest();\n+\t\txhr.open(\'GET\', path + \'?x=\' + Math.random(), true); //eg \'/my/image/name.png\'\n+\t\txhr.responseType = \'arraybuffer\';\n+\n+\t\tvar self = context || this;\n+\t\txhr.onload = function (e) {\n+\t\t\tcallback.call(self, JASPWidgets.Encodings.getBase64(this.response));\n+\t\t};\n+\t\txhr.onerror = function (e) {\n+\t\t\talert(xhr.statusText);\n+\t\t};\n+\n+\t\txhr.send();\n+\t},\n+\n+\tbyteRequest: function (path, callback, context) {\n+\t\tvar xhr = new XMLHttpRequest();\n+\t\txhr.open(\'GET\', path + \'?x=\' + Math.random(), true); //eg \'/my/image/name.png\'\n+\t\txhr.responseType = \'arraybuffer\';\n+\n+\t\tvar self = context || this;\n+\t\txhr.onload = function (e) {\n+\t\t\tcallback.call(self, this.response);\n+\t\t};\n+\n+\t\txhr.send();\n+\t}\n+}\n+JASPWidgets.ExportProperties = {\n+\n+\tformat: {\n+\t\traw: 0,\n+\t\thtml: 1,\n+\t},\n+\n+\tprocess: {\n+\t\tcopy: 0,\n+\t\tsave: 1,\n+\t},\n+\n+\timageFormat: {\n+\t\ttemporary: 0,\n+\t\tresource: 1,\n+\t\tembedded: 2,\n+\t}\n+}\n+\n+JASPWidgets.Exporter = {\n+\n+\tparams: function () {\n+\t\treturn {\n+\t\t\tformat: JASPWidgets.ExportProperties.format.raw,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.copy,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.temporary,\n+\t\t}\n+\t},\n+\n+\tbegin: function (exportObj, exportParams, useNBSP) {\n+\n+\t\tif (useNBSP == undefined)\n+\t\t\tuseNBSP = false;\n+\n+\t\tif (exportParams == undefined)\n+\t\t\texportParams = JASPWidgets.Exporter.params();\n+\t\telse if (exportParams.error)\n+\t\t\treturn false;\n+\n+\t\t\n+\t\tif (exportObj.views) {\n+\t\t\tvar viewList = exportObj.views;\n+\n+\t\t\tif (viewList.length === 0) {\n+\t\t\t\texportObj.exportComplete(exportParams, "");\n+\t\t\t}\n+\t\t\telse {\n+\t\t\t\texportObj.buffer = [];\n+\t\t\t\texportObj.exportCounter = viewList.length;\n+\n+\t\t\t\tfor (var i = 0; i < viewList.length; i++) {\n+\t\t\t\t\tthis._exportView(exportParams, viewList[i], i, exportObj, useNBSP);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t\telse if (exportObj.exportBegin)\n+\t\t\texportObj.exportBegin(exportParams);\n+\t\telse\n+\t\t\treturn false;\n+\t\t\n+\n+\t\treturn true;\n+\t},\n+\n+\t_exportView: function (exportParams, view, i, parent, useNBSP) {\n+\t\tvar self = parent;\n+\t\tvar index = i;\n+\t\tvar originalExportComplete = view.exportComplete;\n+\t\tview.exportComplete = function (exParams, data) {\n+\t\t\tthis.exportComplete = originalExportComplete;\n+\t\t\tself.buffer[index] = data;\n+\t\t\tself.exportCounter -= 1;\n+\t\t\tif (self.exportCounter === 0) {\n+\t\t\t\tvar completeText = "";\n+\t\t\t\tif (!exportParams.error) {\n+\t\t\t\t\tcompleteText = "<div " + self.getStyleAttr() + "\'>\\n";\n+\t\t\t\t\tcompleteText += "<div style=\'display:inline-block\'>\\n";\n+\t\t\t\t\tif (self.toolbar !== undefined) {\n+\t\t\t\t\t\tvar headerStyle = JASPWidgets.Exporter.getHeaderStyles(self.toolbar.$title());\n+\t\t\t\t\t\tcompleteText += \'<\' + self.toolbar.titleTag + \' \' + headerStyle + \'>\' + self.toolbar.title + \'</\' + self.toolbar.titleTag + \'>\\n\'\n+\t\t\t\t\t}\n+\t\t\t\t\tfor (var j = 0; j < self.buffer.length; j++) {\n+\t\t\t\t\t\tif (self.buffer[j]) {\n+\t\t\t\t\t\t\tcompleteText += self.buffer[j];\n+\t\t\t\t\t\t\tif (useNBSP && j < self.buffer.length && (JASPWidgets.Exporter.isInlineStyle(self.views[j].$el) == false))\n+\t\t\t\t\t\t\t\tcompleteText += "&nbsp;";\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t\tcompleteText += "</div>";\n+\t\t\t\t\tcompleteText += "</div>";\t\t\n+\t\t\t\t}\n+\t\t\t\tself.exportComplete(exportParams, completeText);\n+\t\t\t\tself.buffer = [];\n+\t\t\t}\n+\t\t};\n+\t\tview.exportBegin(exportParams);\n+\t},\n+\n+\tgetStyles: function (element, styleItems) {\n+\n+\t\tvar css = element.css(styleItems);\n+\t\tif (css === undefined)\n+\t\t\treturn "";\n+\n+\t\tvar style = "";\n+\t\tfor (var i = 0; i < styleItems.length; i++)\n+\t\t{\n+\t\t\tvar styleItem = styleItems[i];\n+\t\t\tif (css[styleItem])\n+\t\t\t\tstyle += styleItem + ": " + css[styleItem] + "; "\n+\t\t}\n+\n+\t\tif (style)\n+\t\t\tstyle = "style=\'" + style + "\'";\n+\n+\t\treturn style;\n+\t},\n+\n+\tgetHeaderStyles: function (element) {\n+\t\treturn JASPWidgets.Exporter.getStyles(element, ["padding", "text-align", "margin-bottom", "margin-top", "margin-left", "margin-right", "display", "float", "vertical-align", "font-size"]);\n+\t},\n+\n+\tgetTableStyles: function (element) {\n+\t\treturn JASPWidgets.Exporter.getStyles(element, ["border-collapse", "border-top-width", "border-bottom-width", "border-left-width", "border-right-width", "border-color", "border-style", "padding", "text-align", "margin-bottom", "margin-top", "display", "float"]);\n+\t},\n+\n+\tgetTableContentStyles: function (element) {\n+\t\treturn JASPWidgets.Exporter.getStyles(element, ["border-collapse", "border-top-width", "border-bottom-width", "border-left-width", "border-right-width", "border-color", "border-style", "padding", "text-align", "margin-bottom", "margin-top", "display", "float", "font-size", "font-weight"]);\n+\t},\n+\n+\tisInlineStyle: function (element) {\n+\t\tvar css = element.css("display");\n+\n+\t\treturn css === "inline" || css === "inline-block";\n+\t}\n+\n+};\n+\n+JASPWidgets.View = Backbone.View.extend({\n+\n+\tgetStyleAttr: function (styleItems) {\n+\n+\t\treturn JASPWidgets.Exporter.getStyles(this.$el, ["padding", "text-align", "margin-bottom", "margin-top", "margin-left", "margin-right", "display", "float"]);\n+\n+\t},\n+\n+\t_isClosed: false,\n+\t/** Removes DOM tree elements and any listeners from or too the view object */\n+\tclose: function () {\n+\t\tif (!this._isClosed) {\n+\t\t\tthis.remove();\n+\t\t\tthis.off();\n+\t\t\tthis._isClosed = true;\n+\t\t\tif (this.onClose)\n+\t\t\t\tthis.onClose();\n+\t\t}\n+\t},\n+})\n+\n+JASPWidgets.Toolbar = JASPWidgets.View.extend({\n+\tinitialize: function () {\n+\t\t$(document).mousedown(this, this._mouseDownGeneral);\n+\t\tthis.fixed = false;\n+\t\tthis.visible = false;\n+\t\tthis.selected = false;\n+\t},\n+\t\n+\ttitleHTML: function () {\n+\t\tvar html = this.title;\n+\t\tif (this.titleTag !== undefined)\n+\t\t\thtml = \'<\' + this.titleTag + \'>\' + this.title + \'</\' + this.titleTag + \'>\';\n+\n+\t\treturn html;\n+\t},\n+\n+\t$title: function() {\n+\t\treturn this.$el.find(this.titleTag);\n+\t},\n+\n+\trender: function () {\n+\t\tthis.$el.empty();\n+\n+\t\tif (this.title !== undefined) {\n+\t\t\tif (this.titleTag !== undefined)\n+\t\t\t\tthis.$el.append(\'<\' + this.titleTag + \'>\' + this.title + \'</\' + this.titleTag + \'>\');\n+\t\t\telse\n+\t\t\t\tthis.$el.append(this.title);\n+\t\t}\n+\n+\t\tif (this.hasMenu)\n+\t\t{\n+\t\t\tthis.$el.append(\'<div class="toolbar-button jasp-menu jasp-hide"/>\')\n+\n+\t\t\t//var $menuBtn = this.$el.find(".jasp-menu")\n+\n+\t\t\tvar $self = this.$el.find(">:first-child");\n+\t\t\t$self.tooltip({\n+\t\t\t\tcontent: "Damo is awesome",\n+\t\t\t\titems: "*",\n+\t\t\t\tdisabled: true,\n+\t\t\t\tshow: {\n+\t\t\t\t\tduration: 100\n+\t\t\t\t},\n+\t\t\t\tclose: function () {\n+\t\t\t\t\twindow.setTimeout(function () {\n+\t\t\t\t\t\t$self.tooltip("option", "disabled", true)\n+\t\t\t\t\t}, 500)\n+\t\t\t\t},\n+\t\t\t\tposition: {\n+\t\t\t\t\tmy: "left top",\n+\t\t\t\t\tat: "left bottom+5",\n+\t\t\t\t\tcollision: "flipfit"\n+\t\t\t\t}\n+\t\t\t})\n+\t\t}\n+\t\treturn this;\n+\t},\n+\n+\tevents: {\n+\t\t\'mousedown \': \'_mouseDown\',\n+\t},\n+\n+\t_mouseDownGeneral: function (e) {\n+\t\tif (!e || !e.data) return;\n+\t\tvar self = e.data;\n+\t\tself.decreaseFixedness();\n+\t},\n+\n+\tsetVisibility: function (value) {\n+\t\tthis.visible = value;\n+\n+\t\tif (this.fixed)\n+\t\t\treturn;\n+\n+\t\tif (value) {\n+\t\t\tthis.$(".toolbar-button").removeClass(\'jasp-hide\');\n+\t\t}\n+\t\telse {\n+\t\t\tthis.$(".toolbar-button").addClass(\'jasp-hide\');\n+\t\t}\n+\t},\n+\n+\t_mouseDown: function (e) {\n+\t\tthis.setFixedness(2);\n+\n+\t\tvar $titleLabel = this.$el.find(\'>:first-child\');\n+\n+\t\tvar offset = $titleLabel.offset();\n+\n+\t\tvar posY = offset.top + $titleLabel.height() - $(window).scrollTop() + 3;\n+\t\tvar posX = offset.left - $(window).scrollLeft();\n+\n+\t\tthis.options.rX = posX;\n+\t\tthis.options.rY = posY;\n+\n+\t\tthis.options.x = e.screenX;\n+\t\tthis.options.y = e.screenY;\n+\n+\t\tthis.parent.trigger(\'toolbar:showMenu\', this.parent, this.options);\n+\n+\t\treturn true;\n+\t},\n+\n+\tdecreaseFixedness: function() {\n+\t\tthis.setFixedness(this.fixed - 1);\n+\t},\n+\n+\tsetFixedness: function (value) {\n+\t\tthis.fixed = Math.max(0, value);\n+\t\tthis.setVisibility(this.visible);\n+\t\tthis.setSelected(this.fixed !== 0);\n+\t},\n+\n+\tsetParent: function (parent) {\n+\t\tthis.parent = parent;\n+\t\tthis.options = {\n+\n+\t\t\thasCopy: (parent.hasCopy === undefined || parent.hasCopy()) && parent.copyMenuClicked !== undefined,\n+\t\t\thasCite: (parent.hasCitation === undefined || parent.hasCitation()) && parent.citeMenuClicked !== undefined,\n+\t\t\thasAnnotate: (parent.hasAnnotate === undefined || parent.hasAnnotate()) && parent.annotateMenuClicked !== undefined,\n+\t\t\tobjectName: parent.menuName,\n+\t\t};\n+\n+\t\tthis.hasMenu = this.options.hasCopy || this.options.hasCite || this.options.hasAnnotate;\n+\t},\n+\n+\tselectionElement: function() {\n+\t\treturn this.parent.$el;\n+\t},\n+\n+\tsetSelected: function (value) {\n+\t\tthis.selected = value;\n+\n+\t\tvar $selectionElement = this.selectionElement();\n+\t\tif (value) {\n+\t\t\t$selectionElement.addClass("jasp-menu-selected")\n+\t\t}\n+\t\telse {\n+\t\t\t$selectionElement.removeClass("jasp-menu-selected")\n+\t\t}\n+\t},\n+\n+\tcompleteEvent: function (msg) {\n+\t\tthis.setFixedness(0);\n+\n+\t\tvar $self = this.$el.find(">:first-child");\n+\t\t$self.tooltip("option", "content", msg)\n+\t\t$self.tooltip("option", "disabled", false)\n+\t\t$self.tooltip("open")\n+\n+\t\twindow.setTimeout(function () {\n+\t\t\t$self.tooltip("close")\n+\t\t}, 800)\n+\t},\n+\n+\tcancelEvent: function () {\n+\t\tthis.setFixedness(0);\n+\t}\n+})\n+\n+JASPWidgets.CollectionView = JASPWidgets.View.extend({\n+\n+\t/** Initialises the base class property and creates the views from the model collection. */\n+\tinitialize: function () {\n+\t\tthis._collectionViewBase = JASPWidgets.View.prototype;\n+\t\tthis._collectionViewBase.initialize.call(this);\n+\n+\t\tthis.views = [];\n+\n+\t\tthis.collection.each(function (item) {\n+\t\t\tvar itemView = this.createItemView(item);\n+\t\t\tthis.listenTo(itemView, "all", this.eventEcho)\n+\t\t\tthis.views.push(itemView);\n+\t\t}, this);\n+\t},\n+\n+\teventEcho: function (eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7) {\n+\t\tthis.trigger(eventName, arg1, arg2, arg3, arg4, arg5, arg6, arg7);\n+\t},\n+\n+\t/**\n+\t* Returns a view object from the passed model object.\n+\t* @param {Backbone.Model} item - The model used to create the view object.\n+\t* @return {JASPWidgets.View} The newly created view.\n+\t*/\n+\tcreateItemView: function(item) {\n+\t\tthrow "The function \'createItemView\' must be overridden.";\n+\t},\n+\n+\t/**\n+\t* Renders an item view.\n+\t* @param {Backbone.View} itemView - The view to be rendered.\n+\t*/\n+\tonItemRender: function (itemView)\n+\t{\n+\t\titemView.render();\n+\t},\n+\n+\t/** Renders the collection view. */\n+\trender: function () {\n+\t\tfor (var i = 0; i < this.views.length; i++) {\n+\t\t\tvar itemView = this.views[i];\n+\t\t\tthis.onItemRender(itemView);\n+\t\t\tthis.$el.append(itemView.$el);\n+\t\t}\n+\t},\n+\n+\t/** Cleans up views when collection is closed. */\n+\tonClose: function () {\n+\t\tfor (var i = 0; i < this.views.length; i++) {\n+\t\t\tthis.views[i].close();\n+\t\t}\n+\t\tthis.views = [];\n+\t},\n+\n+\texportBegin: function (exportParams) {\n+\t\tif (exportParams == undefined)\n+\t\t\texportParams = JASPWidgets.Exporter.params();\n+\t\telse if (exportParams.error)\n+\t\t\treturn false;\n+\n+\t\tif (this.views.length > 0)\n+\t\t\tJASPWidgets.Exporter.begin(this, exportParams);\n+\t\telse\n+\t\t\tthis.exportComplete(exportParams, "");\n+\n+\t\treturn true;\n+\t},\n+\n+\texportComplete: function (exportParams, html) {\n+\t\tif (!exportParams.error)\n+\t\t\tpushHTMLToClipboard(html);\n+\t}\n+});\n+\n+JASPWidgets.Resizeable = Backbone.Model.extend({\n+\n+\tdefaults: { width: 150, height: 150 },\n+\n+\tsetDim: function (w, h) {\n+\t\tthis.set({ width: w, height: h });\n+\t}\n+});\n+\n+JASPWidgets.ResizeableView = JASPWidgets.View.extend({\n+\n+\t/** Property used to silence events from firing when needed. This is not to be set directly. Set using \'resizeStart\' method.*/\n+\tsilent: false,\n+\n+\t/** Initialises the class by assigning listeners to the model for external size changes and for global mouse events. */\n+\tinitialize: function () {\n+\t\tthis._resizeableViewBase = JASPWidgets.View.prototype;\n+\n+\t\t$(document).mousemove(this, this._mousemove).mouseup(this, this._mouseup);\n+\t\tthis.listenTo(this.model, \'change:width\', this.onModelChange);\n+\t\tthis.listenTo(this.model, \'change:height\', this.onModelChange);\n+\t},\n+\n+\t/** \n+\t* Returns the DOM tree element whos size attributes with be affected during resizing.\n+\tThis allows for the inheriting class to choose another element when required. See image.js\n+\t* @return {Object} DOM Tree element\n+\t*/\n+\tresizeTargetElement: function() {\n+\t\treturn null;\n+\t},\n+\n+\t/** \n+\t* A conditional check for the disabling of the resizing functionality.\n+\t* @return {Bollean} Disable condition result.\n+\t*/\n+\tresizeDisabled: function () {\n+\t\treturn true;\n+\t},\n+\n+\t/** \n+\t* Renders the mouse resize button to the targeted DOM tree element. \n+\t* @return {Object} \'this\' for chaining.\n+\t*/\n+\trender: function () {\n+\t\tthis.resizeTargetElement().append(this.$el)\n+\t\tthis._refreshView();\n+\t\treturn this;\n+\t},\n+\n+\t/** Called when an external event changes the views model obejct. */\n+\tonModelChange: function () {\n+\t\tvar size = this._refreshView();\n+\t\tthis.onResized(size.width, size.height);\n+\t},\n+\n+\t/**\n+\t* Called when the model has been resized.\n+\t* @param {Number} w - The width the model was resied to.\n+\t* @param {Number} h - The height the model was resied to.\n+\t*/\n+\tonResized: function (w, h) {\n+\t\tthis.trigger("ResizeableView:resized", w, h)\n+\t},\n+\n+\t/** \n+\t* Refreshes the size of the view using the model information.\n+\t* @return {Object} The size of the view. The object has a width and height property.\n+\t*/\n+\t_refreshView: function(){\n+\t\tvar w = this.model.get(\'width\');\n+\t\tvar h = this.model.get(\'height\');\n+\t\tthis.resizeView(w, h);\n+\t\treturn {\n+\t\t\twidth: w,\n+\t\t\theight: h\n+\t\t}\n+\t},\n+\n+\t/** \n+\t* Defines the maximum and minimum sizes the view can be resied to.\n+\t* @return {Object} The object has a minWidth, minHeight, maxWidth, maxHeight properties.\n+\t*/\n+\tsizeLimit: function () {\n+\t\treturn {\n+\t\t\tminWidth: 160,\n+\t\t\tminHeight: 160,\n+\t\t\tmaxWidth: 2000,\n+\t\t\tmaxHeight: 2000\n+\t\t};\n+\t},\n+\n+\t/** \n+\t* Defines the maximum and minimum sizes the view can be resied to.\n+\t* @return {Object} The object has a minWidth, minHeight, maxWidth, maxHeight properties.\n+\t*/\n+\t_mouseResizingStart: function (e) {\n+\t\tif (this.resizeDisabled()) return;\n+\n+\t\tthis.iX = e.pageX;\n+\t\tthis.iY = e.pageY;\n+\t\tvar limited = this._normaliseSize(this.model.get("width"), this.model.get("height"));\n+\t\tthis.iW = limited.width;\n+\t\tthis.iH = limited.height;\n+\t\tthis.mouseResizing = true;\n+\t\tthis.resizeStart(this.iW, this.iH, false);\n+\t\treturn false;\n+\t},\n+\n+\t_normaliseSize: function(w, h) {\n+\t\tvar normalised = {};\n+\t\tvar limit = this.sizeLimit();\n+\t\tnormalised.width = Math.min(Math.max(w, limit.minWidth), limit.maxWidth);\n+\t\tnormalised.height = Math.min(Math.max(h, limit.minHeight), limit.maxHeight);\n+\t\treturn normalised;\n+\t},\n+\n+\tresizeStart: function (w, h, silent) {\n+\t\tif (this.resizeDisabled()) return;\n+\t\tthis.silent = silent;\n+\t\tthis.resizing = true;\n+\t\tthis.onResizeStart(w, h);\n+\t},\n+\n+\tonResizeStart: function(w, h)\n+\t{\n+\t\tif (this.silent === false)\n+\t\t\tthis.trigger("ResizeableView:resizeStart", w, h)\n+\t},\n+\n+\tresizeView: function (w, h) {\n+\t\tthis.resizeTargetElement().css({\n+\t\t\twidth: w,\n+\t\t\theight: h\n+\t\t});\n+\t\tthis.onResizeView(w, h);\n+\t},\n+\n+\tonResizeView: function (w, h) {\n+\t\tif (this.silent === false)\n+\t\t\tthis.trigger("ResizeableView:viewResized", w, h);\n+\t},\n+\n+\tresizeStop: function (w, h) {\n+\t\tif (!this.resizeDisabled()) {\n+\t\t\tthis.model.setDim(w, h);\t\t\n+\t\t}\n+\t\tthis.resizing = false;\n+\t\tthis.mouseResizing = false;\n+\t\tthis.silent = false;\n+\t\tthis.onResizeStop(w, h);\n+\t},\n+\n+\tonResizeStop: function (w, h) {\n+\t\tif (this.silent === false)\n+\t\t\tthis.trigger("ResizeableView:resizeStop", w, h)\n+\t},\n+\n+\tisResizing: function () {\n+\t\treturn this.resizing;\n+\t},\n+\n+\tisMouseResizing: function () {\n+\t\treturn this.mouseResizing;\n+\t},\n+\n+\tevents: {\n+\t\t\t\'mousedown\': \'_mouseResizingStart\'\n+\t},\n+\n+\tsetVisibility: function (value) {\n+\t\tif (value && ! this.resizeDisabled())\n+\t\t\tthis.$el.removeClass(\'jasp-hide\');\n+\t\telse\n+\t\t\tthis.$el.addClass(\'jasp-hide\');\n+\t},\n+\n+\t_mouseup: function (e) {\n+\t\tif (!e || !e.data) return;\n+\t\tvar self = e.data;\n+\t\tif (!self.resizeDisabled() && self.mouseResizing) {\n+\t\t\tvar limited = self._normaliseSize(self.iW + e.pageX - self.iX, self.iH + e.pageY - self.iY);\n+\t\t\tself.iW = limited.width;\n+\t\t\tself.iH = limited.height;\n+\t\t\tself.resizeStop.call(self, self.iW, self.iH);\n+\t\t}\n+\t},\n+\n+\t_mousemove: function (e) {\n+\t\tif (!e || !e.data) return;\n+\t\tvar self = e.data;\n+\t\tif (!self.resizeDisabled() && self.mouseResizing) {\n+\t\t\tvar limited = self._normaliseSize(self.iW + e.pageX - self.iX, self.iH + e.pageY - self.iY);\n+\t\t\tself.iW = limited.width;\n+\t\t\tself.iH = limited.height;\n+\t\t\tself.resizeView(self.iW, self.iH);\n+\t\t\tself.iX = e.pageX;\n+\t\t\tself.iY = e.pageY;\n+\t\t}\n+\t},\n+});\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/main.js b/JASP-Desktop/html/js/main.js\nindex aa8e747d6..fd7fc2e40 100644\n--- a/JASP-Desktop/html/js/main.js\n+++ b/JASP-Desktop/html/js/main.js\n@@ -1,137 +1,196 @@\n \n-"use strict"\n+\'use strict\'\n \n-window.getPPI = function() {\n+window.getPPI = function () {\n \n \treturn 96 * window.devicePixelRatio\n }\n \n-$(document).ready(function() {\n \n+\n+$(document).ready(function () {\n \tvar ua = navigator.userAgent.toLowerCase();\n-\t\n+\n \tif (ua.indexOf("windows") !== -1)\n \t\t$("body").addClass("windows")\n \n-\tvar analyses = []\n+\tvar analysesViews = []\n+\tvar menuObject = null;\n \tvar selectedAnalysisId = -1;\n \tvar selectedAnalysis = null\n \n-    var $intro = $("#intro")\n-    var introVisible = true\n-    var introHiding  = false\n-    var introHidingResultsWaiting = [ ]\n-    \n+\tvar $intro = $("#intro")\n+\tvar introVisible = true\n+\tvar introHiding = false\n+\tvar introHidingResultsWaiting = []\n+\n \tvar $instructions = $("#instructions")\n-    var showInstructions = false;\n+\tvar showInstructions = false;\n \n-\twindow.select = function(id) {\n+\twindow.select = function (id) {\n \n \t\tif (selectedAnalysis != null)\n-\t\t\t$(selectedAnalysis).removeClass("selected")\n+\t\t\tselectedAnalysis.unselect()\n \n-\t\tvar analysis = $(\'#id-\' + id)\n-\t\tselectedAnalysisId = id\n+\t\tselectedAnalysisId = id;\n \n-\t\tif (analysis.length === 0)\n-\t\t\treturn\n+\t\tvar jaspWidget = _.find(analysesViews, function (cv) { return cv.model.get("id") === id; });\n+\t\tif (jaspWidget !== undefined) {\n+\t\t\tselectedAnalysis = jaspWidget;\n+\t\t\tselectedAnalysis.select();\n+\t\t\t$("body").addClass("selected")\n+\t\t}\n+\t}\n \n-\t\tselectedAnalysis = analysis[0]\n+\twindow.showInstructions = function () {\n \n-\t\tanalysis.addClass("selected").removeClass("unselected")\n-\t\t$("body").addClass("selected")\n-\t}\n-\t\n-\twindow.showInstructions = function() {\n-\t\n \t\tshowInstructions = true\n \t}\n-\t\n-\twindow.hideInstructions = function() {\n-\t\n+\n+\twindow.hideInstructions = function () {\n+\n \t\tshowInstructions = false\n-\t\t\n-\t\t$instructions.animate({opacity: 0}, 400, "easeOutCubic", function() {\n+\n+\t\t$instructions.animate({ opacity: 0 }, 400, "easeOutCubic", function () {\n \t\t\t$instructions.slideUp()\n \t\t})\n \t}\n-\t\n-\twindow.scrollIntoView = function(item) {\n-\t\n+\n+\twindow.copyMenuClicked = function () {\n+\t\tif (this.menuObject.copyMenuClicked | this.menuObject.copyMenuClicked()) {\n+\t\t\tthis.menuObject.toolbar.completeEvent("Copied to clipboard");\n+\t\t\tthis.menuObject = null;\n+\t\t}\n+\t}\n+\n+\twindow.citeMenuClicked = function () {\n+\t\tif (this.menuObject.citeMenuClicked | this.menuObject.citeMenuClicked()) {\n+\t\t\tthis.menuObject.toolbar.completeEvent("Citation copied to clipboard");\n+\t\t\tthis.menuObject = null;\n+\t\t}\n+\t}\n+\n+\twindow.annotateMenuClicked = function () {\n+\t\tif (this.menuObject.annotateMenuClicked | this.menuObject.annotateMenuClicked()) {\n+\t\t\tthis.menuObject.toolbar.completeEvent("Annotate...");\n+\t\t\tthis.menuObject = null;\n+\t\t}\n+\t}\n+\n+\twindow.menuCancelled = function () {\n+\t\tif (this.menuObject !== undefined && this.menuObject !== null) {\n+\t\t\tthis.menuObject.toolbar.cancelEvent();\n+\t\t\tthis.menuObject = null;\n+\t\t}\n+\t}\n+\n+\twindow.exportHTML = function (filename) {\n+\t\tvar exportObject = {\n+\t\t\tviews: analysesViews,\n+\t\t\texportComplete: function (exportParams, html) {\n+\t\t\t\tif (exportParams.error) {\n+\n+\t\t\t\t}\n+\n+\t\t\t\tif (exportParams.process === JASPWidgets.ExportProperties.process.save)\n+\t\t\t\t\tjasp.saveTextToFile(filename, wrapHTML(html));\n+\t\t\t},\n+\t\t\tgetStyleAttr: function () {\n+\t\t\t\treturn "style=\'display: block;\'";\n+\t\t\t}\n+\t\t};\n+\n+\t\tvar exportParams = {\n+\t\t\tformat: JASPWidgets.ExportProperties.format.html,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.save,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.embedded\n+\t\t};\n+\t\tif (filename === "%PREVIEW%") {\n+\t\t\texportParams.imageFormat = JASPWidgets.ExportProperties.imageFormat.resource;\n+\t\t}\n+\n+\t\tJASPWidgets.Exporter.begin(exportObject, exportParams, true)\n+\t}\n+\n+\twindow.scrollIntoView = function (item) {\n+\n \t\tvar itemTop = item.offset().top\n \t\tvar itemBottom = itemTop + item.height() + parseInt(item.css(\'marginBottom\')) + parseInt(item.css(\'marginTop\'))\n \t\tvar windowTop = document.body.scrollTop\n \t\tvar windowBottom = windowTop + window.innerHeight\n \n-\t\t//console.log(itemTop, itemBottom, windowTop, windowBottom)\n-\n \t\tif (item.height() < window.innerHeight) {\n \n \t\t\tif (itemTop < windowTop)\n-\t\t\t\t$("html, body").animate({ scrollTop: item.offset().top }, { duration: \'slow\', easing: \'swing\'});\n+\t\t\t\t$("html, body").animate({ scrollTop: item.offset().top }, { duration: \'slow\', easing: \'swing\' });\n \t\t\telse if (itemBottom > windowBottom)\n-\t\t\t\t$("html, body").animate({ scrollTop: itemBottom - window.innerHeight + 10 }, { duration: \'slow\', easing: \'swing\'});\n+\t\t\t\t$("html, body").animate({ scrollTop: itemBottom - window.innerHeight + 10 }, { duration: \'slow\', easing: \'swing\' });\n \t\t}\n-\t\telse\n-\t\t{\n+\t\telse {\n \t\t\tif (itemTop > windowTop)\n-\t\t\t\t$("html, body").animate({ scrollTop: item.offset().top }, { duration: \'slow\', easing: \'swing\'});\n+\t\t\t\t$("html, body").animate({ scrollTop: item.offset().top }, { duration: \'slow\', easing: \'swing\' });\n \t\t\telse if (itemBottom < windowBottom)\n-\t\t\t\t$("html, body").animate({ scrollTop: itemBottom - window.innerHeight + 10 }, { duration: \'slow\', easing: \'swing\'});\n+\t\t\t\t$("html, body").animate({ scrollTop: itemBottom - window.innerHeight + 10 }, { duration: \'slow\', easing: \'swing\' });\n \t\t}\n-\t\n+\n \t}\n \n-\twindow.unselect = function() {\n+\twindow.unselect = function () {\n+\n+\t\t_.invoke(analysesViews, "unselect");\n \n-\t\t$(analyses).removeClass("unselected")\n-\t\t$(selectedAnalysis).removeClass("selected")\n \t\t$("body").removeClass("selected")\n \n \t\tselectedAnalysisId = -1\n \t\tselectedAnalysis = null\n-\t\t\n+\n \t\tif (showInstructions)\n \t\t\thideInstructions()\n \t}\n \n-\twindow.remove = function(id) {\n+\twindow.remove = function (id) {\n \n \t\twindow.unselect()\n \n \t\tvar analysis = $(\'#id-\' + id)\n-\t\t\n-\t\tanalysis.animate({opacity: 0}, 400, "easeOutCubic", function() {\n-\t\t\n+\n+\t\tanalysis.animate({ opacity: 0 }, 400, "easeOutCubic", function () {\n+\n \t\t\tanalysis.slideUp(400)\n \t\t})\n-\t\t\n+\n+\t\tvar jaspWidget = _.find(analysesViews, function (cv) { return cv.model.get("id") === id; });\n+\t\tif (jaspWidget !== undefined) {\n+\t\t\tjaspWidget.close();\n+\t\t\tanalysesViews = _.without(analysesViews, jaspWidget);\n+\t\t}\n+\n \t\tif (showInstructions)\n \t\t\thideInstructions()\n \t}\n \n-\twindow.unselectByClickingBody = function(event) {\n+\twindow.unselectByClickingBody = function (event) {\n \n \t\tif (selectedAnalysisId !== -1 && $(event.target).is(".jasp-analysis *") == false) {\n-\t\n+\n \t\t\twindow.unselect()\n \t\t\tjasp.analysisUnselected()\n-\t\t\t\n+\n \t\t\tif (showInstructions)\n \t\t\t\thideInstructions()\n \t\t}\n \t}\n \n-\tvar selectedHandler = function(event) {\n+\tvar selectedHandler = function (event) {\n \n \t\tif ($(event.target).is(".toolbar") || $(event.target).is(".toolbar > *"))\n \t\t\treturn\n \n \t\tvar id = $(event.currentTarget).attr("id")\n \t\tvar idAsInt = parseInt(id.substring(3))\n-\t\t\n+\n \t\tif (selectedAnalysisId == idAsInt) {\n-\t\t\n+\n \t\t\twindow.unselect()\n \t\t\tjasp.analysisUnselected()\n \t\t}\n@@ -142,109 +201,134 @@ $(document).ready(function() {\n \t\t}\n \n \t}\n-\t\n-\tvar analysisChangedDownstreamHandler = function(event, data) {\n-\t\n-\t\tjasp.analysisChangedDownstream(data.id, JSON.stringify(data.options))\n-\t\n+\n+\tvar analysisChangedDownstreamHandler = function (event, data) {\n+\n+\t\tjasp.analysisChangedDownstream(data.id, JSON.stringify(data.model))\n+\n \t}\n \n-\twindow.analysisChanged = function(analysis) {\n+\n+\n+\twindow.analysisChanged = function (analysis) {\n \n \t\tif (introVisible) {\n-\t\t\t\n+\n \t\t\tintroHidingResultsWaiting.push(analysis)\n-\t\t\t\n+\n \t\t\tif (introHiding == false) {\n-\t\t\t\n+\n \t\t\t\tintroHiding = true\n-\t\t\t\n-\t\t\t\t$intro.hide("slide", { direction : "up", easing : "easeOutCubic" }, function() {\n-\t\t\t\t\n+\n+\t\t\t\t$intro.hide("slide", { direction: "up", easing: "easeOutCubic" }, function () {\n+\n \t\t\t\t\tintroHiding = false\n \t\t\t\t\tintroVisible = false\n-\t\t\t\t\t\n+\n \t\t\t\t\tintroHidingResultsWaiting.reverse()\n \n \t\t\t\t\twhile (introHidingResultsWaiting.length > 0)\n \t\t\t\t\t\twindow.analysisChanged(introHidingResultsWaiting.pop())\n \t\t\t\t})\n \t\t\t}\n-\t\t\t\n+\n \t\t\treturn\n \t\t}\n \n \t\tif (showInstructions)\n \t\t\t$instructions.fadeIn(400, "easeOutCubic")\n \n-        var id = "id-" + analysis.id\n-\t\tvar results = analysis.results\n-\t\tvar status = analysis.status\n+\t\tvar id = "id-" + analysis.id\n \n-\t\tvar item = $("#" + id)\n+\t\tvar jaspWidget = _.find(analysesViews, function (cv) { return cv.model.get("id") === analysis.id; });\n+\t\tif (jaspWidget == undefined) {\n+\t\t\tjaspWidget = new JASPWidgets.AnalysisView({ id: id, className: "jasp-analysis", model: new JASPWidgets.Analysis(analysis) });\n \n-\t\tvar newItem = $(\'<div id="\' + id + \'" class="jasp-analysis"></div>\')\n-\t\tnewItem.click(selectedHandler)\n-\t\t\n-\t\tif (analysis.id == selectedAnalysisId)\n-\t\t{\n-\t\t\tif (selectedAnalysis != null)\n-\t\t\t\t$(selectedAnalysis).removeClass("selected")\n+\t\t\tvar newItem = jaspWidget.$el;\n \n-\t\t\t$(analyses).addClass("unselected")\n-\t\t\t$("body").addClass("selected")\n+\t\t\tnewItem.click(selectedHandler)\n \n-\t\t\tselectedAnalysis = newItem[0]\n-\t\t\t$(selectedAnalysis).addClass("selected")\n-\t\t}\n-\t\t\n-\t\tanalyses.push(newItem[0])\n+\t\t\tif (selectedAnalysisId === analysis.id) {\n+\t\t\t\tif (selectedAnalysis != null)\n+\t\t\t\t\tselectedAnalysis.unselect()\n+\n+\t\t\t\t$("body").addClass("selected")\n+\n+\t\t\t\tselectedAnalysis = jaspWidget;\n+\t\t\t\tjaspWidget.select();\n+\t\t\t}\n+\n+\t\t\tanalysesViews.push(jaspWidget)\n \n-\t\tif (item.length !== 0)\n-\t\t{\n-\t\t\tanalyses.filter(function(a) { return a !== item[0] })\n-\t\t\titem.replaceWith(newItem);\n-\t\t}\n-\t\telse\n-\t\t{\n \t\t\tvar spacer = $("#spacer")\n \t\t\tnewItem.css("opacity", 0)\n \t\t\tspacer.before(newItem)\n-\t\t\tnewItem.animate({"opacity" : 1}, 400, "easeOutCubic")\n+\t\t\tnewItem.animate({ "opacity": 1 }, 400, "easeOutCubic")\n+\n+\t\t\tjaspWidget.on("optionschanged", function (id, options) {\n+\n+\t\t\t\tjasp.analysisChangedDownstream(id, JSON.stringify(options))\n+\n+\t\t\t});\n+\n+\t\t\tvar self = this;\n+\t\t\tjaspWidget.on("toolbar:showMenu", function (obj, options) {\n+\n+\t\t\t\tjasp.showAnalysesMenu(JSON.stringify(options));\n+\t\t\t\tself.menuObject = obj;\n+\t\t\t});\n \t\t}\n+\t\telse\n+\t\t\tjaspWidget.model.set(analysis);\n+\n+\t\tjaspWidget.render();\n \n-\t\titem = newItem\n-\t\t\n-\t\titem.analysis( { id : analysis.id, results : results, status : status } )\n-\t\titem.bind("analysisoptionschanged", analysisChangedDownstreamHandler)\n-\t\t\n-\t\tif (selectedAnalysisId == analysis.id)\n-\t\t\twindow.scrollIntoView(item);\n+\t\tif (selectedAnalysisId === analysis.id)\n+\t\t\twindow.scrollIntoView(jaspWidget.$el);\n \t}\n \n \t$("body").click(window.unselectByClickingBody)\n \n+\n })\n \n-var stringify = function(element, tabs) {\n+\n+var wrapHTML = function (html) {\n+\tvar completehtml = "<!DOCTYPE HTML>\\n"\n+\tcompletehtml += "<html>\\n"\n+\tcompletehtml += "\t<head>\\n"\n+\tcompletehtml += "\t\t<meta http-equiv=\'Content-Type\' content=\'text/html; charset=utf-8\' />\\n"\n+\tcompletehtml += "\t\t<title>JASP</title>"\n+\tcompletehtml += "\t</head>\\n"\n+\n+\tvar styles = JASPWidgets.Exporter.getStyles($("body"), ["font-family", "display", "font-size", ]);\n+\n+\tcompletehtml += "\t<body " + styles + ">\\n";\n+\tcompletehtml += html;\n+\tcompletehtml += "\t</body>\\n"\n+\tcompletehtml += "</html>";\n+\treturn completehtml;\n+};\n+\n+var stringify = function (element, tabs) {\n \n \ttabs = tabs || ""\n \n \tvar text = ""\n \tvar $el = $(element)\n-\t\n+\n \tif ($el.hasClass("do-not-copy"))\n \t\treturn text\n \n \tvar tag = $el.prop("tagName").toLowerCase()\n-\t\n+\n \tvar attrs = ""\n \tvar style = ""\n-\t\n-\tvar css = $el.css( [ "border-collapse", "border-top-width", "border-bottom-width", "border-left-width", "border-right-width", "border-color", "border-style", "padding", "text-align" ] )\n+\n+\tvar css = $el.css(["border-collapse", "border-top-width", "border-bottom-width", "border-left-width", "border-right-width", "border-color", "border-style", "padding", "text-align"])\n \n \tif (tag === "td" || tag === "th") {\n-\t\n+\n \t\tif (css["border-top-width"])\n \t\t\tstyle += "border-width : " + css["border-top-width"] + " " + css["border-right-width"] + " " + css["border-bottom-width"] + " " + css["border-left-width"] + "; "\n \t\tif (css["border-color"])\n@@ -263,22 +347,22 @@ var stringify = function(element, tabs) {\n \n \tif (tag === "table" && css[\'border-collapse\'])\n \t\tstyle += "border-collapse : " + css[\'border-collapse\'] + "; "\n-\t\n+\n \tif (style)\n \t\ttext = tabs + \'<\' + tag + \' style="\' + style + \'" \' + attrs + \'>\'\n \telse\n \t\ttext = tabs + \'<\' + tag + \' \' + attrs + \'>\'\n-\t\n+\n \tvar contents = $el.contents()\n \n \tif (contents.length > 0) {\n-\t\t\n+\n \t\tfor (var i = 0; i < contents.length; i++) {\n \t\t\tvar node = contents[i]\n \t\t\tif (node.nodeType === 3) {\n \t\t\t\tvar value = $(node).text()\n \t\t\t\tif (value) {\n-\t\t\t\t\n+\n \t\t\t\t\tvalue = value\n \t\t\t\t\t\t.replace(/&/g, \'&amp;\')\n \t\t\t\t\t\t.replace(/"/g, \'&quot;\')\n@@ -286,7 +370,7 @@ var stringify = function(element, tabs) {\n \t\t\t\t\t\t.replace(/</g, \'&lt;\')\n \t\t\t\t\t\t.replace(/>/g, \'&gt;\')\n \t\t\t\t\t\t.replace(/\\u2212/g, \'-\')\n-\t\t\t\t\n+\n \t\t\t\t\ttext += "\\n" + tabs + value + "\\n"\n \t\t\t\t}\n \t\t\t}\n@@ -294,27 +378,56 @@ var stringify = function(element, tabs) {\n \t\t\t\ttext += "\\n" + stringify(contents[i], tabs + "\\t")\n \t\t\t}\n \t\t}\n-\t\t\n+\n \t\ttext += tabs + \'</\' + tag + \'>\\n\'\n \t}\n \telse {\n-\t\n-\t\ttext += \'</\' + tag + \'>\\n\'\t\n+\n+\t\ttext += \'</\' + tag + \'>\\n\'\n \t}\n-\t\n+\n \treturn text\n }\n \n-var pushToClipboard = function(element) {\n+var pushToClipboard = function (element) {\n \n \tvar $el = $(element)\n-\t\n+\n \tjasp.pushToClipboard("text/html", stringify($el, "\\t\\t"))\n \n }\n \n-var pushTextToClipboard = function(str) {\n+var pushHTMLToClipboard = function (html) {\n+\tjasp.pushToClipboard("text/html", html)\n+\n+}\n+\n+var pushTextToClipboard = function (str) {\n \n \tjasp.pushToClipboard("text/plain", str)\n }\n \n+var pushImageToClipboard = function (base64) {\n+\tjasp.pushImageToClipboard(base64)\n+\n+}\n+\n+\n+var savingId = 0;\n+var savingImages = {};\n+\n+var saveImageBegin = function (path, base64, callback, context) {\n+\tvar index = savingId;\n+\tsavingId += 1;\n+\tsavingImages[index] = {\n+\t\tcallback: callback,\n+\t\tcontext: context\n+\t}\n+\tjasp.saveTempImage(index, path, base64);\n+}\n+\n+window.imageSaved = function (args) {\n+\tvar callbackData = savingImages[args.id];\n+\tcallbackData.callback.call(callbackData.context, args.fullPath);\n+\tdelete savingImages.savingId;\n+}\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/table.js b/JASP-Desktop/html/js/table.js\nindex c08cee712..f6c38b621 100644\n--- a/JASP-Desktop/html/js/table.js\n+++ b/JASP-Desktop/html/js/table.js\n@@ -1,54 +1,64 @@\n-$.widget("jasp.table", {\n+JASPWidgets.table = Backbone.Model.extend({\n \n-\toptions: {\n+\tdefaults: {\n \t\ttitle: "",\n \t\tsubtitle: null,\n-\t\tvariables: [ ],\n-\t\tdata: [ ],\n-\t\tcasesAcrossColumns : false,\n-\t\tformats : null,\n-\t\tstatus : "waiting",\n-\t\tfootnotes : [ ],\n-\t\tcitation : null\n-\t},\n-\t_create: function () {\n-\t\tthis.element.addClass("jasp-table")\n-\t\tthis.refresh()\n-\t},\n-\t_setOptions: function (options) {\n-\t\tthis._super(options)\n-\t\t\n-\t\tthis.refresh()\n+\t\tvariables: [],\n+\t\tdata: [],\n+\t\tcasesAcrossColumns: false,\n+\t\tformats: null,\n+\t\tstatus: "waiting",\n+\t\tfootnotes: [],\n+\t\tcitation: null,\n+\t\terror: null\n+\t}\n+});\n+\n+JASPWidgets.tableView = JASPWidgets.View.extend({\n+\n+\tinitialize: function () {\n+\t\tthis.toolbar = new JASPWidgets.Toolbar({ className: "jasp-toolbar" })\n+\t\tthis.toolbar.setParent(this);\n+\t\tthis.toolbar.title = this.model.get("title");\n+\t\tthis.toolbar.titleTag = "span";\n+\t\tvar self = this;\n+\t\tthis.toolbar.selectionElement = function () {\n+\t\t\treturn self.$el.find(\'th, td:not(.squash-left)\');\n+\t\t};\n \t},\n-\t_fsd : function(value) { // first significant digit position\n-\t\n+\n+\t_fsd: function (value) { // first significant digit position\n+\n \t\tif (value > 0)\n \t\t\treturn Math.floor(Math.log(+value) / Math.log(10)) + 1\n \t\telse if (value < 0)\n \t\t\treturn Math.floor(Math.log(-value) / Math.log(10)) + 1\n \t\telse\n \t\t\treturn 1\n-\t\n+\n \t},\n-\t_fsdoe : function(value) { // first significant digit position in the exponent in scientific notation\n-\t\n+\n+\t_fsdoe: function (value) { // first significant digit position in the exponent in scientific notation\n+\n \t\tif (value == 0)\n \t\t\treturn 1\n \n \t\tvar exponent = Math.floor(Math.log(Math.abs(value)) / Math.log(10))\n \n \t\treturn this._fsd(exponent)\n-\t\n+\n \t},\n-\t_symbol : function(index) {\n-\t\n+\n+\t_symbol: function (index) {\n+\n \t\t// no c in webkit?!\n \n \t\treturn ("\\u1D43\\u1D47" /* \\u1D9C */ + "\\u1D48\\u1D49\\u1DA0\\u1D4D\\u02B0\\u2071\\u02B2\\u1D4F\\u02E1\\u1D50\\u207F\\u1D52\\u1D56\\u02B3\\u02E2\\u1D57\\u1D58\\u1D5B\\u02B7\\u02E3\\u02B8\\u1DBB").charAt(index)\n-\t\n+\n \t},\n-\t_swapRowsAndColumns : function(columnHeaders, columns) {\n-\t\n+\n+\t_swapRowsAndColumns: function (columnHeaders, columns) {\n+\n \t\tvar newRowCount = columns.length - 1\n \t\tvar newColumnCount = columns[0].length + 1\n \n@@ -58,70 +68,71 @@ $.widget("jasp.table", {\n \t\tvar cornerCell = columnHeaders.shift()\n \t\tnewColumnHeaders.unshift(cornerCell)\n \t\tnewColumns[0] = columnHeaders\n-\t\t\n+\n \t\tfor (var colNo = 1; colNo < newColumnCount; colNo++) {\n-\t\t\n+\n \t\t\tnewColumns[colNo] = Array(newRowCount)\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < newRowCount; rowNo++) {\n-\t\t\t\n-\t\t\t\tnewColumns[colNo][rowNo] = columns[rowNo+1][colNo-1]\n+\n+\t\t\t\tnewColumns[colNo][rowNo] = columns[rowNo + 1][colNo - 1]\n \t\t\t}\n-\t\t\n+\n \t\t}\n-\t\t\n-\t\treturn { columnHeaders : newColumnHeaders, columns : newColumns, rowCount : newRowCount, columnCount : newColumnCount }\n-\t\n+\n+\t\treturn { columnHeaders: newColumnHeaders, columns: newColumns, rowCount: newRowCount, columnCount: newColumnCount }\n+\n \t},\n-\t_formatColumn : function(column, type, format, alignNumbers, combine) {\n+\n+\t_formatColumn: function (column, type, format, alignNumbers, combine) {\n \n \t\tvar columnCells = Array(column.length)\n \n \t\tif (type == "string" || typeof format == "undefined") {\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n \n \t\t\t\tvar clazz = (type == "string") ? "text" : "number"\n-\t\t\t\t\n+\n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n \t\t\t\tvar formatted\n \t\t\t\tvar combined = false\n-\t\t\t\t\n+\n \t\t\t\tif (typeof content == "undefined") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "." }\n+\n+\t\t\t\t\tformatted = { content: "." }\n \t\t\t\t}\n-\t\t\t\telse if (combine && rowNo > 0 && column[rowNo-1].content == content) {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;", class : clazz }\n+\t\t\t\telse if (combine && rowNo > 0 && column[rowNo - 1].content == content) {\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;", class: clazz }\n \t\t\t\t\tcombined = true\n \t\t\t\t}\n \t\t\t\telse {\n-\t\t\t\t\n+\n \t\t\t\t\tif (typeof content === "string")\n \t\t\t\t\t\tcontent = content.replace(/\\u273B/g, "<small>\\u273B</small>")\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : content, "class" : clazz }\n+\n+\t\t\t\t\tformatted = { content: content, "class": clazz }\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (combined == false && cell.isStartOfGroup)\n \t\t\t\t\tformatted.isStartOfGroup = true\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfSubGroup)\n \t\t\t\t\tformatted.isStartOfSubGroup = true\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isEndOfGroup)\n \t\t\t\t\tformatted.isEndOfGroup = true\n-\t\t\t\t\t\n+\n \t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\tformatted.footnotes = this._getFootnotes(cell.footnotes)\n-\t\t\t\t\t\n \n-\t\t\t\t\t\n+\n+\n \t\t\t\tcolumnCells[rowNo] = formatted\n \t\t\t}\n-\t\t\t\n+\n \t\t\treturn columnCells\n \t\t}\n \n@@ -133,88 +144,88 @@ $.widget("jasp.table", {\n \t\tvar approx = false\n \n \t\tfor (var i = 0; i < formats.length; i++) {\n-\t\t\n+\n \t\t\tvar f = formats[i]\n-\t\t\t\n+\n \t\t\tif (f.indexOf("p:") != -1)\n \t\t\t\tp = f.substring(2)\n \n \t\t\tif (f.indexOf("dp:") != -1)\n \t\t\t\tdp = f.substring(3)\n-\t\t\t\n+\n \t\t\tif (f.indexOf("sf:") != -1)\n \t\t\t\tsf = f.substring(3)\n-\t\t\t\t\n+\n \t\t\tif (f.indexOf("pc") != -1)\n \t\t\t\tpc = true\n-\t\t\t\t\n+\n \t\t\tif (f.indexOf("~") != -1)\n \t\t\t\tapprox = true;\n \t\t}\n-\t\t\n+\n \t\tif (isFinite(sf)) {\n \n \t\t\tvar upperLimit = 1e6\n \t\t\tvar minLSD = Infinity\t// right most position of the least significant digit\n \t\t\tvar maxFSDOE = -Infinity  // left most position of the least significant digit of the exponent in scientific notation\n-\t\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n \n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n-\t\t\t\t\n+\n \t\t\t\tif (isNaN(parseFloat(content)))  // isn\'t a number\n \t\t\t\t\tcontinue\n \n-\t\t\t\tvar fsd   = this._fsd(content)  // position of first significant digit\n-\t\t\t\tvar lsd   = fsd - sf\n-\t\t\t\t\n+\t\t\t\tvar fsd = this._fsd(content)  // position of first significant digit\n+\t\t\t\tvar lsd = fsd - sf\n+\n \t\t\t\tif (Math.abs(content) >= upperLimit || Math.abs(content) <= Math.pow(10, -dp)) {\n-\t\t\t\t\n+\n \t\t\t\t\tvar fsdoe = this._fsdoe(content)\n \t\t\t\t\tif (fsdoe > maxFSDOE)\n \t\t\t\t\t\tmaxFSDOE = fsdoe\n \t\t\t\t}\n \t\t\t\telse if (lsd < minLSD) {\n-\t\t\t\t\n+\n \t\t\t\t\tminLSD = lsd\n \t\t\t\t}\n \t\t\t}\n-\t\t\t\n+\n \t\t\tif (minLSD < -dp)\n \t\t\t\tminLSD = -dp\n \t\t\tif (minLSD > 0)\n \t\t\t\tminLSD = 0\n \t\t\telse if (minLSD < -20)\n \t\t\t\tminLSD = -20\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n \n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n \t\t\t\tvar formatted\n \t\t\t\tvar isNumber = false\n-\t\t\t\t\n+\n \t\t\t\tif (typeof content == "undefined") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "." }\n+\n+\t\t\t\t\tformatted = { content: "." }\n \t\t\t\t}\n \t\t\t\telse if (typeof content === "") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;", "class" : "number" }\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;", "class": "number" }\n \t\t\t\t}\n-\t\t\t\telse if (combine && rowNo > 0 && column[rowNo-1].content == content) {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;", "class" : "number" }\n+\t\t\t\telse if (combine && rowNo > 0 && column[rowNo - 1].content == content) {\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;", "class": "number" }\n \t\t\t\t}\n \t\t\t\telse if (isNaN(parseFloat(content))) {  // isn\'t a number\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : content, "class" : "number" }\n+\n+\t\t\t\t\tformatted = { content: content, "class": "number" }\n \n \t\t\t\t}\n \t\t\t\telse if (content < p) {\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : "<&nbsp" + p, "class" : "p-value" }\n+\n+\t\t\t\t\tformatted = { content: "<&nbsp" + p, "class": "p-value" }\n \n \t\t\t\t}\n \t\t\t\telse if (content == 0) {\n@@ -222,234 +233,236 @@ $.widget("jasp.table", {\n \t\t\t\t\tvar zero = 0\n \n \t\t\t\t\tif (isFinite(dp))\n-\t\t\t\t\t\tformatted = { content : zero.toFixed(dp), "class" : "number" }\n+\t\t\t\t\t\tformatted = { content: zero.toFixed(dp), "class": "number" }\n \t\t\t\t\telse\n-\t\t\t\t\t\tformatted = { content : zero.toPrecision(sf), "class" : "number" }\n-\t\t\t\t\t\t\n+\t\t\t\t\t\tformatted = { content: zero.toPrecision(sf), "class": "number" }\n+\n \t\t\t\t\tisNumber = true\n-\t\t\t\t\t\n+\n \t\t\t\t}\n \t\t\t\telse if (Math.abs(content) >= upperLimit || Math.abs(content) <= Math.pow(10, -dp)) {\n-\t\t\t\t\n-\t\t\t\t\tvar exponentiated = content.toExponential(sf-1).replace(/-/g, "&minus;")\n+\n+\t\t\t\t\tvar exponentiated = content.toExponential(sf - 1).replace(/-/g, "&minus;")\n \t\t\t\t\tvar paddingNeeded = Math.max(maxFSDOE - this._fsdoe(content), 0)\n-\t\t\t\t\t\n+\n \t\t\t\t\tvar split = exponentiated.split("e")\n \t\t\t\t\tvar mantissa = split[0]\n \t\t\t\t\tvar exponent = split[1]\n \t\t\t\t\tvar exponentSign = exponent.substr(0, 1)\n-\t\t\t\t\tvar exponentNum  = exponent.substr(1)\n+\t\t\t\t\tvar exponentNum = exponent.substr(1)\n \n \t\t\t\t\tvar padding\n-\t\t\t\t\t\n+\n \t\t\t\t\tif (paddingNeeded)\n \t\t\t\t\t\tpadding = \'<span class="do-not-copy" style="visibility: hidden;">\' + Array(paddingNeeded + 1).join("0") + \'</span>\'\n \t\t\t\t\telse\n \t\t\t\t\t\tpadding = \'\'\n-\t\t\t\t\t\n+\n \t\t\t\t\tvar reassembled = mantissa + "e&thinsp;" + padding + exponentSign + exponentNum\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : reassembled, "class" : "number" }\n-\t\t\t\t\t\n+\n+\t\t\t\t\tformatted = { content: reassembled, "class": "number" }\n+\n \t\t\t\t\tisNumber = true\n \t\t\t\t}\n \t\t\t\telse {\n-\t\t\t\t\t\n+\n \t\t\t\t\tif (alignNumbers) {\n-\t\t\t\t\t\n-\t\t\t\t\t\tformatted = { content : content.toFixed(-minLSD).replace(/-/g, "&minus;"), "class" : "number" }\n+\n+\t\t\t\t\t\tformatted = { content: content.toFixed(-minLSD).replace(/-/g, "&minus;"), "class": "number" }\n \t\t\t\t\t}\n \t\t\t\t\telse {\n-\t\t\t\t\t\n-\t\t\t\t\t\tformatted = { content : content.toPrecision(sf).replace(/-/g, "&minus;"), "class" : "number" }\n+\n+\t\t\t\t\t\tformatted = { content: content.toPrecision(sf).replace(/-/g, "&minus;"), "class": "number" }\n \t\t\t\t\t}\n-\t\t\t\t\t\n+\n \t\t\t\t\tisNumber = true\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\tformatted.footnotes = this._getFootnotes(cell.footnotes)\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfGroup)\n \t\t\t\t\tformatted["class"] += " new-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfSubGroup)\n \t\t\t\t\tformatted["class"] += " new-sub-group-row"\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isEndOfGroup)\n \t\t\t\t\tformatted["class"] += " last-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (isNumber && approx)\n \t\t\t\t\tformatted.content = "~&thinsp;" + formatted.content\n-\t\t\t\t\t\t\t\t\t\n+\n \t\t\t\tcolumnCells[rowNo] = formatted\n \t\t\t}\n \t\t}\n \t\telse if (isFinite(dp)) {\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n \n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n \t\t\t\tvar formatted\n-\t\t\t\t\n+\n \t\t\t\tvar isNumber = false\n-\t\t\t\t\n+\n \t\t\t\tif (typeof content == "undefined") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "." }\n+\n+\t\t\t\t\tformatted = { content: "." }\n \t\t\t\t}\n \t\t\t\telse if (content === "") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;" }\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;" }\n \t\t\t\t}\n-\t\t\t\telse if (combine && rowNo > 0 && column[rowNo-1].content == content) {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;", "class" : "number" }\n+\t\t\t\telse if (combine && rowNo > 0 && column[rowNo - 1].content == content) {\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;", "class": "number" }\n \t\t\t\t}\n \t\t\t\telse if (isNaN(parseFloat(content))) {  // isn\'t a number\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : content, "class" : "number" }\n+\n+\t\t\t\t\tformatted = { content: content, "class": "number" }\n \t\t\t\t}\n \t\t\t\telse if (content < p) {\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : "<&nbsp" + p, "class" : "p-value" }\n+\n+\t\t\t\t\tformatted = { content: "<&nbsp" + p, "class": "p-value" }\n \n \t\t\t\t}\n \t\t\t\telse if (pc) {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "" + (100 * content).toFixed(dp) + "&thinsp;%", "class" : "percentage" }\n+\n+\t\t\t\t\tformatted = { content: "" + (100 * content).toFixed(dp) + "&thinsp;%", "class": "percentage" }\n \t\t\t\t\tisNumber = true\n \t\t\t\t}\n \t\t\t\telse {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : content.toFixed(dp).replace(/-/g, "&minus;"), "class" : "number" }\n+\n+\t\t\t\t\tformatted = { content: content.toFixed(dp).replace(/-/g, "&minus;"), "class": "number" }\n \t\t\t\t\tisNumber = true\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\tformatted.footnotes = this._getFootnotes(cell.footnotes)\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfGroup)\n \t\t\t\t\tformatted["class"] += " new-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfSubGroup)\n \t\t\t\t\tformatted["class"] += " new-sub-group-row"\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isEndOfGroup)\n \t\t\t\t\tformatted["class"] += " last-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (isNumber && approx)\n \t\t\t\t\tformatted.content = "~&thinsp;" + formatted.content\n-\t\t\t\t\n+\n \t\t\t\tcolumnCells[rowNo] = formatted\n \t\t\t}\n \t\t}\n \t\telse if (pc) {\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n \n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n \t\t\t\tvar formatted\n-\t\t\t\t\n+\n \t\t\t\tvar isNumber = false\n-\t\t\t\t\n+\n \t\t\t\tif (typeof content == "undefined") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "." }\n+\n+\t\t\t\t\tformatted = { content: "." }\n \t\t\t\t}\n \t\t\t\telse if (content === "") {\n \n-\t\t\t\t\tformatted = { content : "&nbsp;" }\t\t\t\t\n+\t\t\t\t\tformatted = { content: "&nbsp;" }\n \t\t\t\t}\n \t\t\t\telse if (isNaN(parseFloat(content))) {  // isn\'t a number\n-\t\t\t\t\t\n-\t\t\t\t\tformatted = { content : content, "class" : "percentage" }\n+\n+\t\t\t\t\tformatted = { content: content, "class": "percentage" }\n \t\t\t\t}\n \t\t\t\telse {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "" + (100 * content.toFixed(0)) + "&thinsp;%", "class" : "percentage" }\n+\n+\t\t\t\t\tformatted = { content: "" + (100 * content.toFixed(0)) + "&thinsp;%", "class": "percentage" }\n \t\t\t\t\tisNumber = true\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\tformatted.footnotes = this._getFootnotes(cell.footnotes)\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfGroup)\n \t\t\t\t\tformatted["class"] += " new-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfSubGroup)\n \t\t\t\t\tformatted["class"] += " new-sub-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isEndOfGroup)\n \t\t\t\t\tformatted["class"] += " last-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (isNumber && approx)\n \t\t\t\t\tformatted.content = "~&thinsp;" + formatted.content\n-\t\t\t\t\n+\n \t\t\t\tcolumnCells[rowNo] = formatted\n-\t\t\t}\t\t\t\n+\t\t\t}\n \t\t}\n \t\telse {\n-\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < column.length; rowNo++) {\n-\t\t\t\n+\n \t\t\t\tvar cell = column[rowNo]\n \t\t\t\tvar content = cell.content\n \t\t\t\tvar formatted\n-\t\t\t\t\n+\n \t\t\t\tif (typeof content == "undefined") {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "." }\n+\n+\t\t\t\t\tformatted = { content: "." }\n \t\t\t\t}\n \t\t\t\telse if (content === "") {\n \n-\t\t\t\t\tformatted = { content : "&nbsp;" }\t\t\t\t\n+\t\t\t\t\tformatted = { content: "&nbsp;" }\n \t\t\t\t}\n-\t\t\t\telse if (combine && rowNo > 0 && column[rowNo-1].content == content) {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : "&nbsp;" }\n+\t\t\t\telse if (combine && rowNo > 0 && column[rowNo - 1].content == content) {\n+\n+\t\t\t\t\tformatted = { content: "&nbsp;" }\n \t\t\t\t}\n \t\t\t\telse {\n-\t\t\t\t\n-\t\t\t\t\tformatted = { content : content }\n+\n+\t\t\t\t\tformatted = { content: content }\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\tformatted.footnotes = this._getFootnotes(cell.footnotes)\n-\t\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfGroup)\n \t\t\t\t\tformatted["class"] += " new-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isStartOfSubGroup)\n \t\t\t\t\tformatted["class"] += " new-sub-group-row"\n-\t\t\t\t\n+\n \t\t\t\tif (cell.isEndOfGroup)\n \t\t\t\t\tformatted["class"] += " last-group-row"\n-\t\t\t\t\t\n+\n \t\t\t\tcolumnCells[rowNo] = formatted\n \t\t\t}\n \t\t}\n-\t\t\n+\n \t\treturn columnCells\n-\t\n+\n \t},\n-\t_getFootnotes : function(indices) {\n-\t\n+\n+\t_getFootnotes: function (indices) {\n+\n \t\tvar footnotes = Array(indices.length)\n+\t\tvar optFootnotes = this.model.get("footnotes");\n \n \t\tfor (var i = 0; i < indices.length; i++) {\n-\t\t\n+\n \t\t\tvar index = indices[i]\n-\t\t\t\n+\n \t\t\tif (_.isString(index)) {\n-\t\t\t\n+\n \t\t\t\tfootnotes[i] = index\n-\t\t\n-\t\t\t} else if (index < this.options.footnotes.length) {\n-\t\t\n-\t\t\t\tvar footnote = this.options.footnotes[index]\n+\n+\t\t\t} else if (index < optFootnotes.length) {\n+\n+\t\t\t\tvar footnote = optFootnotes[index]\n \t\t\t\tif (typeof footnote.symbol == "undefined")\n \t\t\t\t\tfootnotes[i] = this._symbol(index)\n \t\t\t\telse if (_.isNumber(footnote.symbol))\n@@ -458,313 +471,323 @@ $.widget("jasp.table", {\n \t\t\t\t\tfootnotes[i] = footnote.symbol\n \t\t\t}\n \t\t\telse {\n-\t\t\t\n-\t\t\t\tfootnotes[i] = this._symbol(index)\t\t\t\n+\n+\t\t\t\tfootnotes[i] = this._symbol(index)\n \t\t\t}\n-\t\t\t\n+\n \t\t}\n-\t\t\t\n+\n \t\treturn footnotes\n-\t\t\n+\n \t},\n-\trefresh: function () {\n \n-\t\tvar columnDefs = this.options.schema.fields\n+\trender: function () {\n+\t\tvar optSchema = this.model.get("schema");\n+\t\tvar optData = this.model.get("data");\n+\t\tvar optTitle = this.model.get("title");\n+\t\tvar optSubtitle = this.model.get("subtitle");\n+\t\tvar optCasesAcrossColumns = this.model.get("casesAcrossColumns");\n+\t\tvar optFootnotes = this.model.get("footnotes");\n+\t\tvar optCitation = this.model.get("citation");\n+\t\tvar optStatus = this.model.get("status");\n+\t\tvar optError = this.model.get("error");\n+\n+\t\tvar columnDefs = optSchema.fields\n \t\tvar columnCount = columnDefs.length\n-\t\t\n-\t\tvar rowData = this.options.data\n+\n+\t\tvar rowData = optData;\n \t\tvar rowCount = rowData ? rowData.length : 0\n \n \t\tvar columnHeaders = Array(columnCount)\n \t\tvar columns = Array(columnCount)\n-\t\t\n-\t\t\n+\n+\n \t\tfor (var colNo = 0; colNo < columnCount; colNo++) {\n-\t\t\n+\n \t\t\t// populate column headers\n-\t\t\t\t\n+\n \t\t\tvar columnDef = columnDefs[colNo]\n \t\t\tvar columnName = columnDef.name\n \n \t\t\tvar title = columnDef.title\n \t\t\tvar overTitle = columnDef.overTitle\n-\t\t\t\n-\t\t\tif (typeof title == "undefined") \n+\n+\t\t\tif (typeof title == "undefined")\n \t\t\t\ttitle = columnName\n-\t\t\t\t\n+\n \t\t\tif (title == "")\n \t\t\t\ttitle = "&nbsp;"\n-\t\t\t\n+\n \t\t\tvar columnType = columnDef.type\n-\t\t\t\n-\t\t\tvar columnHeader = { content : title, header : true, type : columnType }\n-\t\t\t\n+\n+\t\t\tvar columnHeader = { content: title, header: true, type: columnType }\n+\n \t\t\tif (overTitle)\n \t\t\t\tcolumnHeader.overTitle = overTitle\n-\t\t\t\n+\n \t\t\tif (typeof columnDef[".footnotes"] != "undefined")\n \t\t\t\tcolumnHeader.footnotes = this._getFootnotes(columnDef[".footnotes"])\n \n-\t\t\tcolumnHeaders[colNo] = columnHeader\t\t\t\n-\t\t\t\n+\t\t\tcolumnHeaders[colNo] = columnHeader\n+\n \t\t\t// populate cells column-wise\n-\t\t\t\n+\n \t\t\tvar column = Array(rowCount)\n \t\t\tvar isGrouped = false\n-\t\t\t\n+\n \t\t\tfor (var rowNo = 0; rowNo < rowCount; rowNo++) {\n-\t\t\t\n+\n \t\t\t\tvar row = rowData[rowNo]\n \t\t\t\tvar content = row[columnName]\n-\t\t\t\tvar cell = { content : content }\n-\t\t\t\t\n+\t\t\t\tvar cell = { content: content }\n+\n \t\t\t\tif (row[\'.footnotes\'] && row[\'.footnotes\'][columnName])\n \t\t\t\t\tcell.footnotes = row[\'.footnotes\'][columnName]\n-\t\t\t\t\t\n+\n \t\t\t\tif (colNo == 0 && columnDef.type == "string" && row[".rowLevel"])\n-\t\t\t\t\tcell.content = Array(row[".rowLevel"]+1).join("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;") + cell.content\n-\t\t\t\t\t\n+\t\t\t\t\tcell.content = Array(row[".rowLevel"] + 1).join("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;") + cell.content\n+\n \t\t\t\tif (row[".isNewGroup"]) {\n-\t\t\t\t\n+\n \t\t\t\t\tcell.isStartOfGroup = true\n \t\t\t\t\tisGrouped = true\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (row[".isNewSubGroup"]) {\n-\t\t\t\t\n+\n \t\t\t\t\tcell.isStartOfSubGroup = true\n \t\t\t\t\tisGrouped = true\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (isGrouped) {\n-\t\t\t\t\n+\n \t\t\t\t\tif (rowNo + 1 < rowCount) {\n-\t\t\t\t\t\n+\n \t\t\t\t\t\tif (rowData[rowNo + 1][".isNewGroup"])\n \t\t\t\t\t\t\tcell.isEndOfGroup = true\n \t\t\t\t\t}\n \t\t\t\t\telse {\n-\t\t\t\t\t\n-\t\t\t\t\t\tcell.isEndOfGroup = true\t\t\t\t\t\n+\n+\t\t\t\t\t\tcell.isEndOfGroup = true\n \t\t\t\t\t}\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tcolumn[rowNo] = cell\n \t\t\t}\n-\t\t\t\n+\n \t\t\tcolumns[colNo] = column\n \t\t}\n \n \t\tvar cells = Array(columnCount)\n-\t\t\n+\n \t\tfor (var colNo = 0; colNo < columnCount; colNo++) {\n-\t\t\n+\n \t\t\tvar column = columns[colNo]\n-\t\t\tvar name   = columnDefs[colNo].name\n-\t\t\tvar type   = columnDefs[colNo].type\n+\t\t\tvar name = columnDefs[colNo].name\n+\t\t\tvar type = columnDefs[colNo].type\n \t\t\tvar format = columnDefs[colNo].format\n-\t\t\tvar alignNumbers = ! this.options.casesAcrossColumns  // numbers can\'t be aligned across rows\n+\t\t\tvar alignNumbers = !optCasesAcrossColumns  // numbers can\'t be aligned across rows\n \t\t\tvar combine = columnDefs[colNo].combine\n-\t\t\t\n+\n \t\t\tcells[colNo] = this._formatColumn(column, type, format, alignNumbers, combine)\n \t\t}\n-\t\t\n-\t\tvar columnsInColumn = { }  // dictionary of counts\n-\t\tvar columnsInsertedInColumn = { }\n+\n+\t\tvar columnsInColumn = {}  // dictionary of counts\n+\t\tvar columnsInsertedInColumn = {}\n \t\tvar maxColumnsInColumn = 0\n-\t\tvar columnNames = [ ]\n-\t\t\n+\t\tvar columnNames = []\n+\n \t\tfor (var colNo = 0; colNo < columnCount; colNo++) {\n-\t\t\n-\t\t\tvar columnName = this.options.schema.fields[colNo].name\n+\n+\t\t\tvar columnName = optSchema.fields[colNo].name\n \t\t\tvar subRowPos = columnName.indexOf("[")\n-\t\t\t\n+\n \t\t\tif (subRowPos != -1)\n \t\t\t\tcolumnName = columnName.substr(0, subRowPos)\n-\t\t\t\n+\n \t\t\tcolumnNames[colNo] = columnName\n-\t\t\t\n+\n \t\t\tvar cic = columnsInColumn[columnName]\n-\t\t\t\n+\n \t\t\tif (typeof cic == "undefined")\n \t\t\t\tcic = 1\n \t\t\telse\n \t\t\t\tcic++\n-\t\t\t\t\n+\n \t\t\tif (maxColumnsInColumn < cic)\n \t\t\t\tmaxColumnsInColumn = cic\n-\t\t\t\n+\n \t\t\tcolumnsInColumn[columnName] = cic\n \t\t}\n-\t\t\n+\n \t\tif (maxColumnsInColumn > 1) {  // do columns need to be folded\n-\t\t\n+\n \t\t\tvar foldedColumnNames = _.uniq(columnNames)\n \t\t\tvar foldedCells = Array(foldedColumnNames.length)\n \t\t\tvar foldedColumnHeaders = Array(foldedColumnNames.length)\n-\t\t\t\n+\n \t\t\t// fold the headers\n \n \t\t\tfor (var colNo = 0; colNo < foldedColumnNames.length; colNo++) {\n-\t\t\t\n+\n \t\t\t\tvar headerIndex = columnNames.indexOf(foldedColumnNames[colNo])\n \t\t\t\tfoldedColumnHeaders[colNo] = columnHeaders[headerIndex]\n \t\t\t}\n-\t\t\t\n-\t\t\t\n+\n+\n \t\t\t// fold the columns\n-\t\t\t\n+\n \t\t\tfor (var colNo = 0; colNo < columnNames.length; colNo++) {\n-\t\t\t\n+\n \t\t\t\tvar columnCells = cells[colNo]\n-\t\t\t\tvar columnName  = columnNames[colNo]\n+\t\t\t\tvar columnName = columnNames[colNo]\n \t\t\t\tvar targetIndex = foldedColumnNames.indexOf(columnName)\n \t\t\t\tvar column = foldedCells[targetIndex]\n \t\t\t\tvar cic = columnsInColumn[columnName]\n-\t\t\t\t\n+\n \t\t\t\tif (typeof column == "undefined")\n \t\t\t\t\tcolumn = Array(columnCells.length * cic)\n-\t\t\t\t\t\n+\n \t\t\t\tvar offset = columnsInsertedInColumn[columnName]\n \t\t\t\tif (typeof offset == "undefined")\n \t\t\t\t\toffset = 0\n \n \t\t\t\tfor (var rowNo = 0; rowNo < columnCells.length; rowNo++) {\n-\t\t\t\t\n+\n \t\t\t\t\tvar cell = columnCells[rowNo]\n-\t\t\t\t\t\n+\n \t\t\t\t\tif (offset == 0)\n \t\t\t\t\t\tcell.isStartOfGroup = true\n \t\t\t\t\tif (offset == cic - 1)\n \t\t\t\t\t\tcell.isEndOfGroup = true\n-\t\t\t\t\t\n+\n \t\t\t\t\tcell.span = maxColumnsInColumn / cic\n \t\t\t\t\tcolumn[rowNo * cic + offset] = cell\n-\t\t\t\t\n+\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tcolumnsInsertedInColumn[columnName] = offset + 1\n \t\t\t\tfoldedCells[targetIndex] = column\n \t\t\t}\n-\t\t\t\n+\n \t\t\tcells = foldedCells\n \t\t\tcolumnHeaders = foldedColumnHeaders\n \t\t\tcolumnCount = foldedColumnHeaders.length\n \t\t\trowCount *= maxColumnsInColumn\n \t\t}\n-\t\t\n-\t\tif (this.options.casesAcrossColumns) {\n-\t\t\n+\n+\t\tif (optCasesAcrossColumns) {\n+\n \t\t\tvar swapped = this._swapRowsAndColumns(columnHeaders, cells)\n \t\t\tcells = swapped.columns\n \t\t\tcolumnHeaders = swapped.columnHeaders;\n \t\t\trowCount = swapped.rowCount\n \t\t\tcolumnCount = swapped.columnCount\n \t\t}\n-\t\t\n-\t\tvar chunks = [ ]\t\t\n \n-\t\tif (this.options.error) {\n-\t\t\n+\t\tvar chunks = []\n+\n+\t\tif (optError) {\n+\n \t\t\tchunks.push(\'<table class="error-state">\')\n \t\t}\n \t\telse {\n-\t\t\n+\n \t\t\tchunks.push(\'<table>\')\n \t\t}\n-\t\t\n-\t\n \n-\t\t\tchunks.push(\'<thead>\')\n-\t\t\t\tchunks.push(\'<tr>\')\n-\t\t\t\t\tchunks.push(\'<th colspan="\' + 2 * columnCount + \'">\' + this.options.title + \'<div class="toolbar do-not-copy"><div class="status"></div><div class="copy toolbar-button" style="visibility: hidden ;"></div><div class="cite toolbar-button" style="visibility: hidden ;"></div></div>\')\n-\t\t\t\t\t\n-\t\t\t\t\tif (this.options.error && this.options.error.errorMessage) {\n-\t\t\n-\t\t\t\t\t\tchunks.push(\'<div  class="error-message-positioner">\')\n-\t\t\t\t\t\tchunks.push(\'<div  class="error-message-box ui-state-error">\')\n-\t\t\t\t\t\tchunks.push(\'<span class="error-message-symbol ui-icon ui-icon-alert"></span>\')\n-\t\t\t\t\t\tchunks.push(\'<div  class="error-message-message">\' + this.options.error.errorMessage + \'</div>\')\n-\t\t\t\t\t\tchunks.push(\'</div>\')\n-\t\t\t\t\t\tchunks.push(\'</div>\')\n-\t\t\t\t\t}\n-\t\t\t\t\t\n-\t\t\t\t\tchunks.push(\'</th>\')\n-\t\t\t\tchunks.push(\'</tr>\')\n \n-\t\tif (this.options.subtitle) {\n-\t\t\t\tchunks.push(\'<tr>\')\n-\t\t\t\t\tchunks.push(\'<th colspan="\' + 2 * columnCount + \'"></th>\')\n-\t\t\t\tchunks.push(\'</tr>\')\n+\n+\t\tchunks.push(\'<thead>\')\n+\t\tchunks.push(\'<tr>\')\n+\t\tchunks.push(\'<th colspan="\' + 2 * columnCount + \'"><div class="toolbar"></div></div>\')\n+\n+\t\tif (optError && optError.errorMessage) {\n+\n+\t\t\tchunks.push(\'<div  class="error-message-positioner">\')\n+\t\t\tchunks.push(\'<div  class="error-message-box ui-state-error">\')\n+\t\t\tchunks.push(\'<span class="error-message-symbol ui-icon ui-icon-alert"></span>\')\n+\t\t\tchunks.push(\'<div  class="error-message-message">\' + optError.errorMessage + \'</div>\')\n+\t\t\tchunks.push(\'</div>\')\n+\t\t\tchunks.push(\'</div>\')\n \t\t}\n-\t\t\n+\n+\t\tchunks.push(\'</th>\')\n+\t\tchunks.push(\'</tr>\')\n+\n+\t\tif (optSubtitle) {\n+\t\t\tchunks.push(\'<tr>\')\n+\t\t\tchunks.push(\'<th colspan="\' + 2 * columnCount + \'"></th>\')\n+\t\t\tchunks.push(\'</tr>\')\n+\t\t}\n+\n \t\tif (columnHeaders.length > 0) {\n-\t\t\n+\n \t\t\tvar overTitles = false\n \n \t\t\tfor (var i = 0; i < columnHeaders.length; i++) {\n \n \t\t\t\tif (columnHeaders[i].overTitle) {\n-\t\t\t\t\t\n+\n \t\t\t\t\toverTitles = true\n \t\t\t\t\tbreak;\n-\t\t\t\t}\t\t\t\n+\t\t\t\t}\n \t\t\t}\n-\t\t\t\n+\n \t\t\tif (overTitles) {\n-\t\t\t\n+\n \t\t\t\tchunks.push(\'<tr class="over-title">\')\n-\t\t\t\t\n+\n \t\t\t\tvar span = 1;\n \t\t\t\tvar oldTitle = columnHeaders[0].overTitle\n \t\t\t\tvar newTitle = ""\n-\t\t\t\t\n-\t\t\t\tif ( ! oldTitle)\n+\n+\t\t\t\tif (!oldTitle)\n \t\t\t\t\toldTitle = ""\n \n \t\t\t\tfor (var colNo = 1; colNo < columnHeaders.length; colNo++) {\n \n \t\t\t\t\tnewTitle = columnHeaders[colNo].overTitle\n-\t\t\t\t\tif ( ! newTitle)\n+\t\t\t\t\tif (!newTitle)\n \t\t\t\t\t\tnewTitle = ""\n-\t\t\t\t\t\n+\n \t\t\t\t\tif (newTitle == oldTitle) {\n-\t\t\t\t\t\t\n+\n \t\t\t\t\t\tspan++\n \t\t\t\t\t}\n \t\t\t\t\telse {\n-\t\t\t\t\t\n+\n \t\t\t\t\t\tchunks.push(\'<th colspan="\' + (2 * span) + \'">\' + oldTitle + \'</th>\')\n \t\t\t\t\t\toldTitle = newTitle\n \t\t\t\t\t\tspan = 1\n \t\t\t\t\t}\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (newTitle == oldTitle)\n \t\t\t\t\tchunks.push(\'<th colspan="\' + (2 * span) + \'">\' + newTitle + \'</th>\')\n-\t\t\t\n+\n \t\t\t\tchunks.push(\'</tr>\')\n \t\t\t}\n \t\t}\n \n-\t\t\t\tchunks.push(\'<tr>\')\n+\t\tchunks.push(\'<tr>\')\n+\n \n-\t\t\t\t\n \t\tfor (var colNo = 0; colNo < columnHeaders.length; colNo++) {\n \n \t\t\tvar cell = columnHeaders[colNo]\n-\t\t\t\n+\n \t\t\tif (cell.content == "\\u03B7\\u00B2\\u209A")\n \t\t\t\tcell.content = \'&eta;&sup2;<small><sub style="position: relative; left: -1ex;">p</sub></small>\'\n-\t\t\t\n+\n \t\t\tvar span = cell.span\n \t\t\tif (typeof span == "undefined")\n \t\t\t\tspan = 1\n-\t\t\t\t\n+\n \t\t\tif (span) {\n \n \t\t\t\tspan *= 2  // times 2, because of footnote markers\n-\t\t\t\n+\n \t\t\t\tchunks.push(\'<th colspan="\' + span + \'" class="\' + cell.type + \'">\' + cell.content)\n \t\t\t\tif (cell.footnotes)\n \t\t\t\t\tchunks.push(cell.footnotes.join(\' \'))\n@@ -772,23 +795,23 @@ $.widget("jasp.table", {\n \t\t\t}\n \n \t\t}\n-\t\t\t\t\n-\t\t\t\tchunks.push(\'</tr>\')\n-\t\t\tchunks.push(\'</thead>\')\n-\t\t\tchunks.push(\'<tbody>\')\n-\t\t\t\n+\n+\t\tchunks.push(\'</tr>\')\n+\t\tchunks.push(\'</thead>\')\n+\t\tchunks.push(\'<tbody>\')\n+\n \t\tvar tableProgress = Array(columnCount)\n \t\tfor (var i = 0; i < columnCount; i++) {\n-\t\t\n-\t\t\ttableProgress[i] = { from : 0, to : 0 }\n+\n+\t\t\ttableProgress[i] = { from: 0, to: 0 }\n \t\t}\n-\t\t\n+\n \t\tfor (var rowNo = 0; rowNo < rowCount; rowNo++) {\n-\t\t\t\n+\n \t\t\tchunks.push(\'<tr>\')\n \n \t\t\tfor (var colNo = 0; colNo < columnCount; colNo++) {\n-\t\t\t\n+\n \t\t\t\tif (tableProgress[colNo].to == rowNo) {\n \n \t\t\t\t\tvar fromIndex = tableProgress[colNo].from\n@@ -802,134 +825,239 @@ $.widget("jasp.table", {\n \n \t\t\t\t\tcellHtml += (cell.header ? \'<th\' : \'<td\')\n \t\t\t\t\tcellHtml += \' class="value \' + cellClass + \'"\'\n-\t\t\t\t\tcellHtml += (cell.span   ? \' rowspan="\' + cell.span + \'"\' : \'\')\n+\t\t\t\t\tcellHtml += (cell.span ? \' rowspan="\' + cell.span + \'"\' : \'\')\n \t\t\t\t\tcellHtml += \'>\'\n-\t\t\t\t\tcellHtml += (typeof cell.content   != "undefined" ? cell.content : \'\')\n+\t\t\t\t\tcellHtml += (typeof cell.content != "undefined" ? cell.content : \'\')\n \t\t\t\t\tcellHtml += (cell.header ? \'</th>\' : \'</td>\')\n-\t\t\t\t\t\n+\n \t\t\t\t\tcellHtml += (cell.header ? \'<th\' : \'<td\')\n \t\t\t\t\tcellHtml += \' class="symbol \' + cellClass + \'"\'\n-\t\t\t\t\tcellHtml += (cell.span   ? \' rowspan="\' + cell.span + \'"\' : \'\')\n+\t\t\t\t\tcellHtml += (cell.span ? \' rowspan="\' + cell.span + \'"\' : \'\')\n \t\t\t\t\tcellHtml += \'>\'\n \t\t\t\t\tif (typeof cell.footnotes != "undefined")\n \t\t\t\t\t\tcellHtml += cell.footnotes.join(\' \')\n \t\t\t\t\tcellHtml += (cell.header ? \'</th>\' : \'</td>\')\n-\t\t\t\t\t\n+\n \t\t\t\t\ttableProgress[colNo].from += 1\n-\t\t\t\t\t\n+\n \t\t\t\t\tif (cell.span) {\n-\t\t\t\t\t\n+\n \t\t\t\t\t\ttableProgress[colNo].to += cell.span\n \n \t\t\t\t\t}\n \t\t\t\t\telse {\n-\t\t\t\t\t\n+\n \t\t\t\t\t\ttableProgress[colNo].to += 1\n \t\t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\t\tchunks.push(cellHtml)\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t}\n-\t\t\t\n+\n \t\t\tif (rowNo == 0) // squashes the table to the left\n \t\t\t\tchunks.push(\'<td class="squash-left" rowspan="\' + rowCount + \'"></td>\')\n \n \t\t\tchunks.push(\'</tr>\')\n \t\t}\n-\t\t\n-\t\t\tchunks.push(\'<tr><td colspan="\' + 2 * columnCount + \'"></td></tr>\')\n-\t\n-\t\t\tchunks.push(\'</tbody>\')\n-\t\t\t\n-\t\tif (this.options.footnotes) {\n+\n+\t\tchunks.push(\'<tr><td colspan="\' + 2 * columnCount + \'"></td></tr>\')\n+\n+\t\tchunks.push(\'</tbody>\')\n+\n+\t\tif (optFootnotes) {\n \n \t\t\tchunks.push(\'<tfoot>\')\n \n-\t\t\tfor (var i = 0; i < this.options.footnotes.length; i++) {\n+\t\t\tfor (var i = 0; i < optFootnotes.length; i++) {\n \n \t\t\t\tchunks.push(\'<tr><td colspan="\' + 2 * columnCount + \'">\')\n-\t\t\t\t\n-\t\t\t\tvar footnote = this.options.footnotes[i]\n-\t\t\t\t\n+\n+\t\t\t\tvar footnote = optFootnotes[i]\n+\n \t\t\t\tif (_.isString(footnote)) {\n \n \t\t\t\t\tchunks.push(this._symbol(i) + \'&nbsp;\')\n \t\t\t\t\tchunks.push(footnote)\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tif (_.has(footnote, "symbol")) {\n-\t\t\t\t\n+\n \t\t\t\t\tif (_.isNumber(footnote.symbol))\n \t\t\t\t\t\tchunks.push(this._symbol(footnote.symbol) + \'&nbsp;\')\n \t\t\t\t\telse\n \t\t\t\t\t\tchunks.push(footnote.symbol + \'&nbsp;\')\n-\t\t\t\t\t\t\n+\n \t\t\t\t\tchunks.push(footnote.text)\n \t\t\t\t}\n-\t\t\t\t\n+\n \t\t\t\tchunks.push(\'</td></tr>\')\n \t\t\t}\n \n-\t\t\tchunks.push(\'</tfoot>\')\n+\t\t\tchunks.push(\'</tfoot>\');\n \t\t}\n \n-\t\tchunks.push(\'</table>\')\n-\t\t\n-\t\n-\t\tvar html = chunks.join("")\n+\t\tchunks.push(\'</table>\');\n+\n+\n+\t\tvar html = chunks.join("");\n+\n+\n+\t\tthis.$el.append(html);\n \n-\t\tthis.element.html(html)\n-\t\t\n-\t\tvar $table = this.element.children("table")\n-\t\tvar $toolbar = this.element.find("div.toolbar")\n-\t\t\n-\t\tvar $toolbarButtons = $toolbar.find("div.toolbar-button")\n-\t\tvar $copy = $toolbar.find("div.copy")\n-\t\tvar $cite = $toolbar.find("div.cite")\n-\t\t\n-\t\tvar $status = $toolbar.find("div.status")\n-\t\t\n-\t\t$table.mouseenter(function(event){\n-\t\t\t$toolbarButtons.css("visibility", "visible")\n-\t\t})\n-\t\t$table.mouseleave(function(event) {\n-\t\t\t$toolbarButtons.css("visibility", "hidden")\n-\t\t})\n-\t\t\n-\t\t$copy.tooltip({ content: "Copied to clipboard", items: "*", disabled: true, show: { duration : 100 }, close : function() { window.setTimeout(function(){ $copy.tooltip("option", "disabled", true) }, 500) }, position: { my: "center+10 top+15", at: "center bottom", collision: "flipfit" }})\n-\t\t$cite.tooltip({ content: "Citation copied to clipboard", items: "*", disabled: true, show: { duration : 100 }, close : function() { window.setTimeout(function(){ $cite.tooltip("option", "disabled", true) }, 500) }, position: { my: "center+10 top+15", at: "center bottom", collision: "flipfit" }})\n-\t\t\n-\t\t$copy.click(function(event) {\n-\t\t\tpushToClipboard($table)\n-\t\t\tevent.preventDefault()\n-\t\t\t$copy.tooltip("option", "disabled", false)\n-\t\t\t$copy.tooltip("open")\n-\t\t\t\n-\t\t\twindow.setTimeout(function() {\n-\t\t\t\t$copy.tooltip("close")\n-\t\t\t}, 800)\n-\t\t})\n-\t\t\n-\t\tvar citation = this.options.citation\n-\t\tif (citation == null)\n-\t\t\t$cite.hide()\n-\n-\t\t$cite.click(function(event) {\n-\t\t\tpushTextToClipboard(citation.join("\\n\\n"))\n-\t\t\tevent.preventDefault()\n-\t\t\t$cite.tooltip("option", "disabled", false)\n-\t\t\t$cite.tooltip("open")\n-\t\t\t\n-\t\t\twindow.setTimeout(function() {\n-\t\t\t\t$cite.tooltip("close")\n-\t\t\t}, 800)\n-\t\t})\n-\t\t\n-\t\t$status.addClass(this.options.status)\n \t\t\n+\t\tthis.toolbar.render();\n+\t\tthis.toolbar.$el.append(\'<div class="status">\');\n+\n+\t\tvar $container = this.$el.find("div.toolbar");\n+\t\t$container.append(this.toolbar.$el);\n+\n+\t\tvar $toolbar = this.$el.find(".jasp-toolbar");\n+\t\tvar $status = $toolbar.find("div.status");\n+\t\t$status.addClass(optStatus);\n+\t},\n+\n+\tgetExportAttributes: function (element)\n+\t{\n+\t\tvar attrs = ""\n+\t\tvar style = ""\n+\n+\t\tvar $elObj = $(element)\n+\t\tvar tag = $elObj.prop("tagName").toLowerCase()\n+\n+\t\tif (tag === "td" || tag === "th") {\n+\n+\t\t\tstyle = JASPWidgets.Exporter.getTableContentStyles($elObj);\n+\n+\t\t\tif ($elObj.prop("rowspan") && $elObj.prop("rowspan") != 1)\n+\t\t\t\tattrs += \'rowspan="\' + $elObj.prop("rowspan") + \'" \'\n+\t\t\tif ($elObj.prop("colspan") && $elObj.prop("colspan") != 1)\n+\t\t\t\tattrs += \'colspan="\' + $elObj.prop("colspan") + \'" \'\n+\t\t}\n+\t\telse if (tag === "table") {\n+\t\t\tstyle = JASPWidgets.Exporter.getTableStyles($elObj);\n+\t\t}\n+\t\telse if (tag === "span" || tag === "h1" || tag === "h2" || tag === "h3") {\n+\t\t\tstyle = JASPWidgets.Exporter.getHeaderStyles($elObj);\n+\t\t}\n+\n+\n+\t\tif (style)\n+\t\t\tattrs = style + \' \' + attrs\n+\n+\t\treturn attrs;\n \t},\n-\t_destroy: function () {\n-\t\tthis.element.removeClass("jasp-table").text("")\n+\n+\texportHTML: function (element, tabs) {\n+\t\tif (element == null)\n+\t\t\telement = this.el;\n+\n+\t\ttabs = tabs || ""\n+\n+\t\tvar text = ""\n+\t\tvar $elObj = $(element)\n+\n+\t\tif ($elObj.hasClass("do-not-copy"))\n+\t\t\treturn text\n+\n+\t\tvar tag = $elObj.prop("tagName").toLowerCase()\n+\n+\t\tvar attrs = this.getExportAttributes(element);\n+\n+\t\tif (attrs)\n+\t\t\ttext = tabs + \'<\' + tag + \' \' + attrs + \'>\'\n+\t\telse\n+\t\t\ttext = tabs + \'<\' + tag + \'>\'\n+\n+\t\tvar contents = $elObj.contents()\n+\n+\t\tif (contents.length > 0) {\n+\n+\t\t\tfor (var i = 0; i < contents.length; i++) {\n+\t\t\t\tvar node = contents[i]\n+\t\t\t\tif (node.nodeType === 3) { //is text node\n+\t\t\t\t\tvar value = $(node).text()\n+\t\t\t\t\tif (value) {\n+\n+\t\t\t\t\t\tvalue = value\n+\t\t\t\t\t\t\t.replace(/&/g, \'&amp;\')\n+\t\t\t\t\t\t\t.replace(/"/g, \'&quot;\')\n+\t\t\t\t\t\t\t.replace(/\'/g, \'&#39;\')\n+\t\t\t\t\t\t\t.replace(/</g, \'&lt;\')\n+\t\t\t\t\t\t\t.replace(/>/g, \'&gt;\')\n+\t\t\t\t\t\t\t.replace(/\\u2212/g, \'-\')\n+\n+\t\t\t\t\t\ttext += "\\n" + tabs + value + "\\n"\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\telse {\n+\t\t\t\t\ttext += "\\n" + this.exportHTML(contents[i], tabs + "\\t")\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\ttext += tabs + \'</\' + tag + \'>\\n\'\n+\t\t}\n+\t\telse {\n+\n+\t\t\ttext += \'</\' + tag + \'>\\n\'\n+\t\t}\n+\n+\t\treturn text;\n+\t},\n+\n+\texportBegin: function (exportParams) {\n+\n+\t\tif (exportParams == undefined)\n+\t\t\texportParams = JASPWidgets.Exporter.params();\n+\t\telse if (exportParams.error)\n+\t\t\treturn false;\n+\n+\t\tthis.exportComplete(exportParams, this.exportHTML());\n+\n+\t\treturn true;\n+\t},\n+\n+\texportComplete: function (exportParams, html) {\n+\t\tif (!exportParams.error)\n+\t\t\tpushHTMLToClipboard(html);\n+\t},\n+\n+\tcopyMenuClicked: function () {\n+\t\tthis.exportBegin({\n+\t\t\tformat: JASPWidgets.ExportProperties.format.html,\n+\t\t\tprocess: JASPWidgets.ExportProperties.process.copy,\n+\t\t\timageFormat: JASPWidgets.ExportProperties.imageFormat.temporary,\n+\t\t});\n+\t\treturn true;\n+\t},\n+\n+\thasCitation: function () {\n+\t\tvar optCitation = this.model.get("citation");\n+\t\treturn optCitation !== null\n+\t},\n+\n+\tciteMenuClicked: function () {\n+\t\tvar optCitation = this.model.get("citation");\n+\t\tpushTextToClipboard(optCitation.join("\\n\\n"));\n+\t\treturn true;\n+\t},\n+\n+\tmenuName: "Table",\n+\n+\tevents: {\n+\t\t\'mouseenter\': \'_hoveringStart\',\n+\t\t\'mouseleave\': \'_hoveringEnd\',\n+\t},\n+\n+\t_hoveringStart: function (e) {\n+\t\tthis.toolbar.setVisibility(true);\n+\t},\n+\n+\t_hoveringEnd: function (e) {\n+\t\tthis.toolbar.setVisibility(false);\n+\t},\n+\n+\tonClose: function () {\n+\t\tthis.toolbar.close();\n \t}\n-})\n+});\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/html/js/tables.js b/JASP-Desktop/html/js/tables.js\nindex 241cc6005..0e0a89a97 100644\n--- a/JASP-Desktop/html/js/tables.js\n+++ b/JASP-Desktop/html/js/tables.js\n@@ -1,46 +1,11 @@\n-$.widget("jasp.tables", {\n+JASPWidgets.tables = Backbone.Collection.extend({\n \n-    options: {\n-        items : [ ],\n-        status : "waiting"\n-    },\n-    _create: function () {\n+\tmodel: JASPWidgets.table,\n+});\n \n-        this.element.addClass("jasp-tables")\n+JASPWidgets.tablesView = JASPWidgets.CollectionView.extend({\n \n-        this.tables = $(this.element)\n-\n-        this.refresh()\n-    },\n-    _setOptions: function (options) {\n-        this._super(options)\n-\n-        this.refresh()\n-    },\n-    refresh: function () {\n-\n-        this.tables.empty()\n-\n-        if (this.options.items && $.isArray(this.options.items) && this.options.items.length > 0)\n-        {\n-            for (var i = 0; i < this.options.items.length; i++)\n-            {\n-            \tvar options = this.options.items[i]\n-            \tif ( ! options["status"])\n-            \t\toptions["status"] = this.options.status\n-            \n-                var table = $(\'<div class="jasp-tables-table"></div>\')\n-                table.table(options)\n-                this.tables.append(table)\n-            }\n-\n-        }\n-        else {\n-\n-        }\n-\n-    },\n-    _destroy: function () {\n-        this.element.removeClass("jasp-tables").text("")\n-    }\n-})\n+\tcreateItemView: function (item) {\n+\t\treturn new JASPWidgets.tableView({ className: "jasp-tables-table jasp-table", model: item });\n+\t},\n+});\n\\ No newline at end of file\ndiff --git a/JASP-Desktop/mainwindow.cpp b/JASP-Desktop/mainwindow.cpp\nindex 6e445581a..71678dc59 100644\n--- a/JASP-Desktop/mainwindow.cpp\n+++ b/JASP-Desktop/mainwindow.cpp\n@@ -116,6 +116,7 @@ MainWindow::MainWindow(QWidget *parent) :\n \tui->webViewResults->page()->setNetworkAccessManager(new LRNAM(tq(tempfiles_sessionDirName()), this));\n \tui->webViewResults->setUrl(QUrl(QString("qrc:///core/index.html")));\n \tconnect(ui->webViewResults, SIGNAL(loadFinished(bool)), this, SLOT(resultsPageLoaded(bool)));\n+\tconnect(ui->webViewResults, SIGNAL(scrollValueChanged()), this, SLOT(scrollValueChangedHandle()));\n \n \t_tableModel = new DataSetTableModel();\n \tui->tableView->setModel(_tableModel);\n@@ -152,8 +153,12 @@ MainWindow::MainWindow(QWidget *parent) :\n \n \tconnect(this, SIGNAL(analysisSelected(int)), this, SLOT(analysisSelectedHandler(int)));\n \tconnect(this, SIGNAL(analysisUnselected()), this, SLOT(analysisUnselectedHandler()));\n+\tconnect(this, SIGNAL(saveTempImage(int, QString, QByteArray)), this, SLOT(saveTempImageHandler(int, QString, QByteArray)));\n \tconnect(this, SIGNAL(pushToClipboard(QString, QString)), this, SLOT(pushToClipboardHandler(QString, QString)));\n+\tconnect(this, SIGNAL(pushImageToClipboard(QByteArray)), this, SLOT(pushImageToClipboardHandler(QByteArray)));\n+\tconnect(this, SIGNAL(saveTextToFile(QString, QString)), this, SLOT(saveTextToFileHandler(QString, QString)));\n \tconnect(this, SIGNAL(analysisChangedDownstream(int, QString)), this, SLOT(analysisChangedDownstreamHandler(int, QString)));\n+\tconnect(this, SIGNAL(showAnalysesMenu(QString)), this, SLOT(showAnalysesMenuHandler(QString)));\n \n \t_buttonPanel = new QWidget(ui->pageOptions);\n \t_buttonPanelLayout = new QVBoxLayout(_buttonPanel);\n@@ -183,6 +188,8 @@ MainWindow::MainWindow(QWidget *parent) :\n \n \tconnect(ui->splitter, SIGNAL(splitterMoved(int,int)), this, SLOT(splitterMovedHandler(int,int)));\n \n+\t_analysisMenu = new QMenu(this);\n+\n \tupdateUIFromOptions();\n \n \tui->panelMid->hide();\n@@ -883,9 +890,8 @@ void MainWindow::saveSelected(const QString &filename)\n {\n \tif (_analyses->count() > 0)\n \t{\n-\t\tQWebElement element =  ui->webViewResults->page()->mainFrame()->documentElement();\n-\t\tQString qHTML = element.toOuterXml();\n-\t\t_package->analysesHTML = fq(qHTML);\n+\t\t_package->analysesHTML = "";\n+\t\tui->webViewResults->page()->mainFrame()->evaluateJavaScript("window.exportHTML(\'%PREVIEW%\');");\n \n \t\tJson::Value analysesData = Json::arrayValue;\n \t\tfor (Analyses::iterator itr = _analyses->begin(); itr != _analyses->end(); itr++)\n@@ -900,6 +906,8 @@ void MainWindow::saveSelected(const QString &filename)\n \t\t}\n \n \t\t_package->analysesData = analysesData;\n+\n+\n \t\t_package->hasAnalyses = true;\n \t}\n \n@@ -907,10 +915,33 @@ void MainWindow::saveSelected(const QString &filename)\n \t_alert->show();\n }\n \n+void MainWindow::saveTextToFileHandler(const QString &filename, const QString &data)\n+{\n+\tif (filename == "%PREVIEW%")\n+\t{\n+\t\t_package->analysesHTML = fq(data);\n+\t}\n+\telse\n+\t{\n+\t\tQFile file(filename);\n+\t\tfile.open(QIODevice::WriteOnly | QIODevice::Truncate);\n+\t\tQTextStream stream(&file);\n+\t\tstream.setCodec("UTF-8");\n+\n+\t\tstream << data;\n+\t\tstream.flush();\n+\t\tfile.close();\n+\t}\n+}\n \n void MainWindow::exportSelected(const QString &filename)\n {\n-\tQWebElement element = ui->webViewResults->page()->mainFrame()->documentElement().clone();\n+\tui->webViewResults->page()->mainFrame()->evaluateJavaScript("window.exportHTML(\'" + filename + "\');");\n+\n+\n+\n+\n+\t/*QWebElement element = ui->webViewResults->page()->mainFrame()->documentElement().clone();\n \n \tQWebElementCollection nodes;\n \n@@ -943,7 +974,7 @@ void MainWindow::exportSelected(const QString &filename)\n \tstream.flush();\n \tfile.close();\n \n-\tui->tabBar->setCurrentIndex(1);\n+\tui->tabBar->setCurrentIndex(1);*/\n }\n \n void MainWindow::adjustOptionsPanelWidth()\n@@ -1143,16 +1174,17 @@ void MainWindow::pushToClipboardHandler(const QString &mimeType, const QString &\n \t\t\t\t\t   "\t\t<meta http-equiv=\'Content-Type\' content=\'text/html; charset=utf-8\' />\\n"\n \t\t\t\t\t   "\t\t<title>JASP</title>"\n \t\t\t\t\t   "\t</head>\\n"\n-\t\t\t\t\t   "\t<body>\\n";\n+\t\t\t\t\t   "\t<body style=\'font-family: sans-serif; display:block;\'>\\n";\n \t\ttoClipboard += data;\n \t\ttoClipboard += "\t</body>\\n"\n \t\t\t\t\t   "</html>";\n \n-\t\tmimeData->setData("text/html", toClipboard.toUtf8());\n+\t\tmimeData->setHtml(toClipboard);\n+\t\t//mimeData->setData("text/html", toClipboard.toUtf8());\n \t}\n \telse\n \t{\n-\t\tmimeData->setData(mimeType, data.toUtf8());\n+\t\tmimeData->setText(data);\n \t}\n \n \tQClipboard *clipboard = QApplication::clipboard();\n@@ -1161,6 +1193,109 @@ void MainWindow::pushToClipboardHandler(const QString &mimeType, const QString &\n \t//qDebug() << clipboard->mimeData(QClipboard::Clipboard)->data("text/html");\n }\n \n+void MainWindow::pushImageToClipboardHandler(const QByteArray &base64)\n+{\n+\tif (_log != NULL)\n+\t\t_log->log("Copy");\n+\n+\tQMimeData *mimeData = new QMimeData();\n+\n+\tQByteArray byteArray = QByteArray::fromBase64(base64);\n+\n+\tQImage pm;\n+\tif(pm.loadFromData(byteArray))\n+\t{\n+#ifdef __WIN32__ //needed because jpegs/clipboard doesn\'t support transparency in windows\n+\t\tQImage image2(pm.size(), QImage::Format_ARGB32);\n+\t\timage2.fill(Qt::white);\n+\t\tQPainter painter(&image2);\n+\t\tpainter.drawImage(0, 0, pm);\n+\n+\t\tmimeData->setImageData(image2);\n+#else\n+\t\tmimeData->setImageData(pm);\n+#endif\n+\n+\t\tQClipboard *clipboard = QApplication::clipboard();\n+\t\tclipboard->setMimeData(mimeData, QClipboard::Clipboard);\n+\t}\n+\n+\n+\t//qDebug() << clipboard->mimeData(QClipboard::Clipboard)->data("text/html");\n+}\n+\n+void MainWindow::saveTempImageHandler(int id, QString path, QByteArray data)\n+{\n+\tQByteArray byteArray = QByteArray::fromBase64(data);\n+\n+\tQString fullpath = tq(tempfiles_createSpecific_clipboard(fq(path)));\n+\n+\tQFile file(fullpath);\n+\tfile.open(QIODevice::WriteOnly);\n+\tfile.write(byteArray);\n+\tfile.close();\n+\n+\tQString eval = QString("window.imageSaved({ id: %1, fullPath: \'%2\'});").arg(id).arg(fullpath);\n+\tui->webViewResults->page()->mainFrame()->evaluateJavaScript(eval);\n+}\n+\n+void MainWindow::showAnalysesMenuHandler(QString options)\n+{\n+\tJson::Value menuOptions;\n+\n+\tJson::Reader parser;\n+\tparser.parse(fq(options), menuOptions);\n+\n+\tQIcon _copyIcon = QIcon(":/icons/copy.png");\n+\tQIcon _citeIcon = QIcon(":/icons/cite.png");\n+\n+\t_analysisMenu->clear();\n+\n+\tQString objName = tq(menuOptions["objectName"].asString());\n+\n+\tif (menuOptions["hasCopy"].asBool())\n+\t\t_analysisMenu->addAction(_copyIcon, "Copy " + objName, this, SLOT(copySelected()));\n+\n+\tif (menuOptions["hasCite"].asBool())\n+\t{\n+\t\t_analysisMenu->addSeparator();\n+\t\t_analysisMenu->addAction(_citeIcon, "Cite", this, SLOT(citeSelected()));\n+\t}\n+\n+\tQPoint point = ui->webViewResults->mapToGlobal(QPoint(menuOptions["rX"].asInt(), menuOptions["rY"].asInt()));\n+\n+\t_analysisMenu->move(point);\n+\t_analysisMenu->show();\n+\n+\t//ui->webViewResults->page()->mainFrame()->s\n+\n+}\n+\n+void MainWindow::copySelected()\n+{\n+\n+\ttempfiles_purgeClipboard();\n+\tui->webViewResults->page()->mainFrame()->evaluateJavaScript("window.copyMenuClicked();");\n+\n+}\n+\n+void MainWindow::citeSelected()\n+{\n+\n+\ttempfiles_purgeClipboard();\n+\tui->webViewResults->page()->mainFrame()->evaluateJavaScript("window.citeMenuClicked();");\n+\n+}\n+\n+void MainWindow::scrollValueChangedHandle()\n+{\n+\tif ( ! _analysisMenu->isHidden())\n+\t{\n+\t\t_analysisMenu->hide();\n+\t\tui->webViewResults->page()->mainFrame()->evaluateJavaScript("window.menuCancelled();");\n+\t}\n+}\n+\n void MainWindow::analysisChangedDownstreamHandler(int id, QString options)\n {\n \tAnalysis *analysis = _analyses->get(id);\ndiff --git a/JASP-Desktop/mainwindow.h b/JASP-Desktop/mainwindow.h\nindex 478e832d6..0034ca7d6 100644\n--- a/JASP-Desktop/mainwindow.h\n+++ b/JASP-Desktop/mainwindow.h\n@@ -38,6 +38,7 @@ private:\n \tUi::MainWindow *ui;\n \n \tAnalysisForm *_currentOptionsWidget;\n+\tQMenu* _analysisMenu;\n \tDataSetPackage *_package;\n \tDataSetTableModel *_tableModel;\n \tAnalysis *_currentAnalysis;\n@@ -83,16 +84,27 @@ signals:\n \tvoid analysisSelected(int id);\n \tvoid analysisUnselected();\n \tvoid analysisChangedDownstream(int id, QString options);\n+\tvoid saveTextToFile(QString filename, QString text);\n \tvoid pushToClipboard(QString mimeType, QString data);\n+\tvoid pushImageToClipboard(QByteArray base64);\n+\tvoid saveTempImage(int id, QString path, QByteArray data);\n+\tvoid showAnalysesMenu(QString options);\n \n private slots:\n \n \tvoid analysisResultsChangedHandler(Analysis* analysis);\n \tvoid analysisSelectedHandler(int id);\n \tvoid analysisUnselectedHandler();\n+\tvoid pushImageToClipboardHandler(const QByteArray &base64);\n+\tvoid saveTextToFileHandler(const QString &filename, const QString &data);\n \tvoid pushToClipboardHandler(const QString &mimeType, const QString &data);\n+\tvoid saveTempImageHandler(int id, QString path, QByteArray data);\n \tvoid analysisChangedDownstreamHandler(int id, QString options);\n \n+\tvoid showAnalysesMenuHandler(QString options);\n+\tvoid copySelected();\n+\tvoid citeSelected();\n+\n \tvoid tabChanged(int index);\n \tvoid helpToggled(bool on);\n \tvoid dataSetSelected(const QString &filename);\n@@ -121,6 +133,7 @@ private slots:\n \tvoid updateUIFromOptions();\n \n \tvoid resultsPageLoaded(bool success);\n+\tvoid scrollValueChangedHandle();\n \n \tvoid saveKeysSelected();\n \tvoid openKeysSelected();\ndiff --git a/JASP-Desktop/mainwindow.ui b/JASP-Desktop/mainwindow.ui\nindex 350d90d0a..fce1269c4 100644\n--- a/JASP-Desktop/mainwindow.ui\n+++ b/JASP-Desktop/mainwindow.ui\n@@ -244,8 +244,8 @@ QTableView::item::last\n                       <rect>\n                        <x>0</x>\n                        <y>0</y>\n-                       <width>562</width>\n-                       <height>542</height>\n+                       <width>552</width>\n+                       <height>537</height>\n                       </rect>\n                      </property>\n                      <layout class="QGridLayout" name="optionsContentAreaLayout">\n@@ -333,21 +333,7 @@ QTableView::item::last\n                 <number>0</number>\n                </property>\n                <item row="0" column="0">\n-                <widget class="QWebView" name="webViewResults">\n-                 <property name="acceptDrops">\n-                  <bool>false</bool>\n-                 </property>\n-                 <property name="styleSheet">\n-                  <string notr="true">QWidget {\n-\tbackground-color: rgb(250, 250, 250);\n-}</string>\n-                 </property>\n-                 <property name="url">\n-                  <url>\n-                   <string>about:blank</string>\n-                  </url>\n-                 </property>\n-                </widget>\n+                <widget class="WebView" name="webViewResults" native="true"/>\n                </item>\n               </layout>\n              </widget>\n@@ -498,6 +484,12 @@ QPushButton#first:checked {\n    <header>widgets/tabbar.h</header>\n    <container>1</container>\n   </customwidget>\n+  <customwidget>\n+   <class>WebView</class>\n+   <extends>QWidget</extends>\n+   <header>widgets/webview.h</header>\n+   <container>1</container>\n+  </customwidget>\n  </customwidgets>\n  <resources/>\n  <connections/>\ndiff --git a/JASP-Desktop/resources/icons.qrc b/JASP-Desktop/resources/icons.qrc\nindex 94dfcab8b..e6b7d0396 100644\n--- a/JASP-Desktop/resources/icons.qrc\n+++ b/JASP-Desktop/resources/icons.qrc\n@@ -61,5 +61,7 @@\n         <file>icons/variable-nominal-inactive.svg</file>\n         <file>icons/variable-nominal-text.svg</file>\n         <file>icons/analysis-classical-sem.svg</file>\n+        <file>icons/copy.png</file>\n+        <file>icons/cite.png</file>\n     </qresource>\n </RCC>\ndiff --git a/JASP-Desktop/resources/icons/cite.png b/JASP-Desktop/resources/icons/cite.png\nnew file mode 100644\nindex 000000000..b0b3ba9d1\nBinary files /dev/null and b/JASP-Desktop/resources/icons/cite.png differ\ndiff --git a/JASP-Desktop/resources/icons/cite.svg b/JASP-Desktop/resources/icons/cite.svg\nnew file mode 100644\nindex 000000000..a8303a62d\n--- /dev/null\n+++ b/JASP-Desktop/resources/icons/cite.svg\n@@ -0,0 +1,66 @@\n+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n+<!-- Created with Inkscape (http://www.inkscape.org/) -->\n+\n+<svg\n+   xmlns:dc="http://purl.org/dc/elements/1.1/"\n+   xmlns:cc="http://creativecommons.org/ns#"\n+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n+   xmlns:svg="http://www.w3.org/2000/svg"\n+   xmlns="http://www.w3.org/2000/svg"\n+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\n+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\n+   version="1.1"\n+   width="16"\n+   height="16"\n+   id="svg2"\n+   inkscape:version="0.48.2 r9819"\n+   sodipodi:docname="cite.svg">\n+  <sodipodi:namedview\n+     pagecolor="#ffffff"\n+     bordercolor="#666666"\n+     borderopacity="1"\n+     objecttolerance="10"\n+     gridtolerance="10"\n+     guidetolerance="10"\n+     inkscape:pageopacity="0"\n+     inkscape:pageshadow="2"\n+     inkscape:window-width="640"\n+     inkscape:window-height="480"\n+     id="namedview3069"\n+     showgrid="false"\n+     inkscape:zoom="10.429825"\n+     inkscape:cx="18.938861"\n+     inkscape:cy="7.9722601"\n+     inkscape:window-x="429"\n+     inkscape:window-y="139"\n+     inkscape:window-maximized="0"\n+     inkscape:current-layer="svg2" />\n+  <defs\n+     id="defs4" />\n+  <metadata\n+     id="metadata7">\n+    <rdf:RDF>\n+      <cc:Work\n+         rdf:about="">\n+        <dc:format>image/svg+xml</dc:format>\n+        <dc:type\n+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\n+        <dc:title />\n+      </cc:Work>\n+    </rdf:RDF>\n+  </metadata>\n+  <g\n+     transform="matrix(0.58793765,0,0,0.58793765,-1.5260109,-601.11175)"\n+     id="layer1">\n+    <text\n+       x="2.7129822"\n+       y="1041.097"\n+       id="text2991"\n+       xml:space="preserve"\n+       style="font-size:13.575634px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"\n+       sodipodi:linespacing="125%"><tspan\n+         x="2.7129822"\n+         y="1041.097"\n+         id="tspan2993">Cite</tspan></text>\n+  </g>\n+</svg>\ndiff --git a/JASP-Desktop/resources/icons/copy.png b/JASP-Desktop/resources/icons/copy.png\nnew file mode 100644\nindex 000000000..d810b9e31\nBinary files /dev/null and b/JASP-Desktop/resources/icons/copy.png differ\ndiff --git a/JASP-Desktop/widgets/webview.cpp b/JASP-Desktop/widgets/webview.cpp\nnew file mode 100644\nindex 000000000..4772c539e\n--- /dev/null\n+++ b/JASP-Desktop/widgets/webview.cpp\n@@ -0,0 +1,27 @@\n+#include "webview.h"\n+#include <QWebFrame>\n+\n+WebView::WebView(QWidget *parent):\n+\tQWebView(parent)\n+{\n+\n+}\n+\n+void WebView::paintEvent(QPaintEvent * event)\n+{\n+\tQWebView::paintEvent(event);\n+\n+\tQWebFrame *frameff = page()->mainFrame();\n+\n+\tint hValue = frameff->scrollBarValue(Qt::Horizontal);\n+\tint vValue = frameff->scrollBarValue(Qt::Vertical);\n+\n+\tif (hValue != _lastHorizontalValue || vValue != _lastVerticalValue)\n+\t{\n+\t\t_lastHorizontalValue = hValue;\n+\t\t_lastVerticalValue = vValue;\n+\n+\t\temit scrollValueChanged();\n+\t}\n+\n+}\ndiff --git a/JASP-Desktop/widgets/webview.h b/JASP-Desktop/widgets/webview.h\nnew file mode 100644\nindex 000000000..beed79f85\n--- /dev/null\n+++ b/JASP-Desktop/widgets/webview.h\n@@ -0,0 +1,27 @@\n+#ifndef WEBVIEW_H\n+#define WEBVIEW_H\n+\n+#include <QWebView>\n+\n+#include "common.h"\n+\n+class WebView : public QWebView\n+{\n+\n+\tQ_OBJECT\n+\n+public:\n+\texplicit WebView(QWidget *parent = 0);\n+\n+signals:\n+\tvoid scrollValueChanged();\n+\n+protected:\n+\tvirtual void paintEvent(QPaintEvent *event) OVERRIDE;\n+\n+private:\n+\tint _lastVerticalValue;\n+\tint _lastHorizontalValue;\n+};\n+\n+#endif // WEBVIEW_H\n', 'commit_subject': "Merge branch 'dropmann-temp_development' into pimped", 'repo_name': 'jasp-stats/jasp-desktop', 'committer': {'name': 'Jonathon Love', 'email': '44f878afe53efc66b76772bd845eb65944ed8232@thon.cc', 'time_sec': '1436221546', 'tz_offset': '-420', 'date': {'seconds': '1436221546'}}, 'commit_tree': '21c427980ef113ec69396abc238c34f95adfecff', 'commit_message': "Merge branch 'dropmann-temp_development' into pimped\n", 'language_name': 'JavaScript', 'license': 'agpl-3.0', 'hunks': '@@ -0,0 +1,27 @@\n+#ifndef WEBVIEW_H\n+#define WEBVIEW_H\n+\n+#include <QWebView>\n+\n+#include "common.h"\n+\n+class WebView : public QWebView\n+{\n+\n+\tQ_OBJECT\n+\n+public:\n+\texplicit WebView(QWidget *parent = 0);\n+\n+signals:\n+\tvoid scrollValueChanged();\n+\n+protected:\n+\tvirtual void paintEvent(QPaintEvent *event) OVERRIDE;\n+\n+private:\n+\tint _lastVerticalValue;\n+\tint _lastHorizontalValue;\n+};\n+\n+#endif // WEBVIEW_H\n', 'addition_count': 27, 'deletion_count': 0, 'src_file': '/dev/null', 'tgt_file': 'b/JASP-Desktop/widgets/webview.h', 'file_extension': '.h', 'before_file': 'ADDFILE', 'message': "Merge branch 'dropmann-temp_development' into pimped\n", 'author_name': 'Jonathon Love', 'lang': 'h'}