Commit c7ead705 authored by Cédric Le Ninivin's avatar Cédric Le Ninivin

WIP: move to use IndexxedDB with jIO for service Worker

parent b8e84c68
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>CribJS Loader</title>
<script src="../lib/rsvp.js"></script>
<script src="../lib/renderjs.js"></script>
<script src="../lib/jszip.js" type="text/javascript"></script>
<script src="./gadget_landing_cribjs2.js"></script>
</head>
<body>
<div data-gadget-url="./crib-sw-gadget.html"
data-gadget-scope="crib_sw_gadget"
data-gadget-sandbox="public"></div>
<div data-gadget-url="./gadget_jio.html"
data-gadget-scope="jio_gadget"
data-gadget-sandbox="public"></div>
</body>
</html>
\ No newline at end of file
/*jslint browser: true, indent: 2, maxlen: 80*/
/*globals RSVP, rJS,
location, console, fetch, Promise*/
(function (window, document, RSVP, rJS, location, console, JSZip) {
"use strict";
function getExtension(url) {
var extension = url.split('.').pop();
if (extension.endsWith('/')) {
return ".html";
}
return "." + extension;
}
function getSetting(gadget, key, default_value) {
if (key === "site_editor_gadget_url") {
return window.location.protocol + "//" + window.location.host +
"/gadget_jio_crib.html";
}
return default_value;
}
function loadZipIntoCrib(crib_sw_gadget, zip, from_path, path_to_load) {
var promise_list = [], url_number = 0,
site_url = window.location.protocol + "//" + window.location.host;
zip.forEach(function (relativePath, zipEntry) {
var end_url;
url_number += 1;
if (zipEntry.dir) {
return;
}
if (!relativePath.startsWith(from_path)) {
return;
}
relativePath = relativePath.substring(from_path.length);
console.log(relativePath);
if (relativePath.startsWith("/")) {
end_url = relativePath.substring(1);
} else {
end_url = relativePath;
}
promise_list.push(
new RSVP.Queue()
.push(function () {
return zipEntry.async('blob');
})
.push(function (result) {
if (end_url.endsWith(".js")) {
// This is a ugly hack as mimetype needs to be correct for JS
result = result.slice(0, result.size, "application/javascript");
} else if (end_url.endsWith(".html")) {
// This is a ugly hack as mimetype needs to be correct for JS
result = result.slice(0, result.size, "text/html");
} else if (end_url.endsWith(".css")) {
// This is a ugly hack as mimetype needs to be correct for JS
result = result.slice(0, result.size, "text/css");
}
return crib_sw_gadget.put(end_url, {blob: result});
})
);
});
return RSVP.all(promise_list);
}
function loadContentFromZIPFile(gadget, options) {
var path_to_load = options.to_path,
from_path = options.from_path,
file_list = options.file_list;
if (file_list.length === 0) {
return "No File to Load";
}
return new RSVP.Queue()
.push(function () {
return RSVP.all([
gadget.getDeclaredGadget('crib_sw_gadget'),
JSZip.loadAsync(file_list[0])
]);
})
.push(function (result_list) {
return loadZipIntoCrib(
result_list[0], result_list[1], from_path, path_to_load
);
})
.push(console.log, console.log);
}
function loadContentFromZIPURL(gadget, options) {
var path_to_load = options.to_path, file_list, crib_sw_gadget,
from_path = options.from_path, zip_url = options.zip_url,
jio_gadget, url_list = [], url_number = 0;
return new RSVP.Queue()
.push(function () {
return gadget.getDeclaredGadget('crib_sw_gadget');
})
.push(function (returned_gadget) {
crib_sw_gadget = returned_gadget;
return fetch(zip_url)
.then(function (response) { // 2) filter on 200 OK
if (response.status === 200 || response.status === 0) {
return Promise.resolve(response.blob());
} else {
return Promise.reject(new Error(response.statusText));
}
});
})
.push(JSZip.loadAsync)
.push(function (zip) {
return loadZipIntoCrib(crib_sw_gadget, zip, from_path, path_to_load);
})
.push(console.log, console.log);
}
rJS(window)
.ready(function (g) {
g.props = {};
})
.declareMethod('loadFromZipUrl', function (options) {
return loadContentFromZIPURL(this, options);
})
.declareMethod('loadFromZipFile', function (options) {
return loadContentFromZIPFile(this, options);
})
.allowPublicAcquisition("getSetting", function (argument_list) {
return getSetting(this, argument_list[0], argument_list[1]);
});
}(window, document, RSVP, rJS, location, console, JSZip));
\ No newline at end of file
/*globals window, document, RSVP, rJS, navigator, jIO, URL*/
/*jslint indent: 2, maxlen: 80, nomen: true*/
var repair = false;
(function (window, document, RSVP, rJS, jIO, navigator, URL) {
"use strict";
function createStorage(gadget) {
return jIO.createJIO({
type: "replicate",
parallel_operation_attachment_amount: 10,
parallel_operation_amount: 1,
conflict_handling: 2,
signature_hash_key: 'hash',
check_remote_attachment_modification: true,
check_remote_attachment_creation: true,
check_remote_attachment_deletion: true,
check_remote_deletion: true,
check_local_creation: false,
check_local_deletion: false,
check_local_modification: false,
signature_sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: "officejs-hash"
}
},
local_sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: "ojs_source_code"
}
}
},
remote_sub_storage: {
type: "appcache",
manifest: gadget.props.cache_file,
version: gadget.props.version_url,
take_installer: true
}
});
}
function waitForServiceWorkerActive(registration) {
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
} else if (registration.waiting) {
serviceWorker = registration.waiting;
} else if (registration.active) {
serviceWorker = registration.active;
}
if (serviceWorker.state !== "activated") {
return RSVP.Promise(function (resolve, reject) {
serviceWorker.addEventListener('statechange', function (e) {
if (e.target.state === "activated") {
resolve();
}
});
RSVP.delay(500).then(function () {
reject(new Error("Timeout service worker install"));
});
});
}
}
rJS(window)
.setState({error_amount: 0})
.ready(function (gadget) {
var i,
element_list =
gadget.element.querySelectorAll('[data-install-configuration]');
gadget.props = {};
for (i = 0; i < element_list.length; i += 1) {
gadget.props[element_list[i].getAttribute(
'data-install-configuration'
)] = element_list[i].textContent;
}
gadget.props.redirect_url = new URL(window.location);
gadget.props.redirect_url.pathname += gadget.props.version_url;
if (gadget.props.redirect_url.hash) {
if (gadget.props.redirect_url.hash.startsWith('#access_token')) {
// This is a bad hack to support dropbox.
gadget.props.redirect_url.hash =
gadget.props.redirect_url.hash.replace(
'#access_token',
'#/?page=ojs_dropbox_configurator&access_token'
);
} else if (gadget.props.redirect_url.hash
.startsWith('#page=settings_configurator')) {
// Make monitoring app still compatible with old instances setup URLs
gadget.props.redirect_url.hash =
gadget.props.redirect_url.hash.replace(
'#page=settings_configurator',
'#/?page=settings_configurator'
);
}
}
})
.declareService(function () {
var gadget = this;
return RSVP.all([
new RSVP.Queue()
.push(function () {
return RSVP.delay(600);
})
.push(function () {
return gadget.changeState({
app_name: gadget.props.app_name,
redirect_url: gadget.props.redirect_url
});
}),
gadget.install()
.push(function () {
window.location.replace(gadget.props.redirect_url);
})
]);
})
.declareMethod('render', function (options) {
var gadget = this;
return new RSVP.Queue()
.push(function () {
var element = gadget.element.querySelector('.presentation');
if (element) {
return gadget.getDeclaredGadget('view');
}
element = document.createElement("div");
element.className = "presentation";
gadget.element.appendChild(element);
return gadget.declareGadget(
"gadget_officejs_bootloader_presentation.html",
{"scope": "view", "element": element}
);
})
.push(function (view_gadget) {
return view_gadget.render(options);
});
})
.onStateChange(function (modification_dict) {
return this.render(modification_dict);
})
.declareMethod("install", function () {
var gadget = this,
storage = createStorage(gadget);
if (navigator.serviceWorker !== undefined) {
return storage.repair()
.push(function () {
return navigator.serviceWorker.register(
"gadget_officejs_bootloader_serviceworker.js"
);
})
.push(function (registration) {
return waitForServiceWorkerActive(registration);
})
.push(undefined, function (error) {
return gadget.changeState({
error_amount: gadget.state.error_amount + 1,
error: error
})
.push(function () {
return RSVP.delay(1000);
})
.push(function () {
return gadget.install();
});
});
}
return;
});
}(window, document, RSVP, rJS, jIO, navigator, URL));
\ No newline at end of file
/*jslint indent: 2*/
/*global self, fetch, Request, Response */
var global = self, window = self;
(function (self, fetch, Request, Response) {
"use strict";
self.DOMParser = {};
self.sessionStorage = {};
self.localStorage = {};
self.openDatabase = {};
self.DOMError = {};
self.Node = {};
self.XMLSerializer = Object;
self.DOMParser = Object;
self.postMessage = function () {return; };
self.importScripts('./lib/rsvp.js', './lib/jio-latest.js');
self.storage = {};
self.cache_list = [];
function createStorage(database) {
return self.jIO.createJIO({
type: "indexeddb",
database: database
});
}
function getFromLocal(relative_url) {
if (self.storage.get === undefined) {
self.storage = createStorage("ojs_source_code");
}
return self.storage.getAttachment(self.registration.scope, relative_url)
.push(function (blob) {
return new Response(blob, {
'headers': {
'content-type': blob.type
}
});
});
}
self.addEventListener('install', function (event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
var relative_url = event.request.url.split("#")[0]
.replace(self.registration.scope, "")
.replace(self.version_url, "");
if (relative_url === './no-cache') {
event.respondWith(new Response(self.cache_list));
return;
}
else if (event.request !== undefined && event.request.referrer === self.registration.scope) {
event.respondWith(
new self.RSVP.Queue()
.push(function () {
return fetch(event.request);
})
.push(undefined, function (error) {
if (error.name === 'TypeError' &&
error.message === 'Failed to fetch') {
return {};
}
throw error;
})
.push(function (response) {
if (response.status === 200) {
return response;
}
return getFromLocal(relative_url);
})
.push(undefined, function (error) {
return new Response(error, {"statusText": error.message, "status": 500});
})
);
} else {
event.respondWith(
new self.RSVP.Queue()
.push(function () {
return getFromLocal(relative_url);
})
.push(undefined, function (error) {
if (error instanceof self.jIO.util.jIOError) {
if (relative_url.indexOf('http') === -1) {
if (self.cache_list.indexOf(relative_url) === -1) {
self.cache_list.push(relative_url);
}
}
return fetch(event.request);
}
return new Response(error, {"statusText": error.message, "status": 500});
})
);
}
});
}(self, fetch, Request, Response));
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Jio Gadget</title>
<link rel="http://www.renderjs.org/rel/interface" href="interface_jio.html">
<!-- renderjs -->
<script src="./lib/rsvp.js" type="text/javascript"></script>
<script src="./lib/renderjs.js" type="text/javascript"></script>
<script src="./lib/jio-latest.js" type="text/javascript"></script>
<!-- custom script -->
<script src="gadget_jio_crib.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>
\ No newline at end of file
/*global window, rJS, jIO, FormData */
/*jslint indent: 2, maxerr: 3 */
(function (window, rJS, jIO) {
"use strict";
function waitForServiceWorkerActive(registration) {
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
} else if (registration.waiting) {
serviceWorker = registration.waiting;
} else if (registration.active) {
serviceWorker = registration.active;
}
if (serviceWorker.state !== "activated") {
return RSVP.Promise(function (resolve, reject) {
serviceWorker.addEventListener('statechange', function (e) {
if (e.target.state === "activated") {
resolve();
}
});
RSVP.delay(500).then(function () {
reject(new Error("Timeout service worker install"));
});
});
}
}
rJS(window)
.ready(function (gadget) {
// Initialize the gadget local parameters
gadget.state_parameter_dict = {};
gadget.state_parameter_dict.default_document = window.location.protocol + "//" + window.location.host + window.location.pathname.substring(0, window.location.pathname.length - "gadget_jio_crib.html".length);
console.log(gadget.state_parameter_dict.default_document);
this.state_parameter_dict.jio_storage = jIO.createJIO({
"type": "indexeddb",
"database": "ojs_source_code"
});
return this.state_parameter_dict.jio_storage.put(gadget.state_parameter_dict.default_document, {})
})
.ready(function (gadget) {
if ('serviceWorker' in navigator) {
// XXX Hack to not add a new service worker when one is already declared
if (!navigator.serviceWorker.controller) {
return new RSVP.Queue()
.push(function () {
return navigator.serviceWorker.register(
'/gadget_cribjs_bootloader_serviceworker.js',
{scope: '/'}
);
})
.push(function (registration) {
return waitForServiceWorkerActive(registration);
})
}
} else {
throw "Service Worker are not available in your browser";
}
})
.declareMethod('get', function (url) {
var storage = this.state_parameter_dict.jio_storage;
return storage.getAttachment(this.state_parameter_dict.default_document, url)
.push(function (result) {
return jIO.util.readBlobAsDataURL(result);
})
.push(function (e) {
return e.target.result;
});
})
.declareMethod('put', function (url, data_uri) {
var storage = this.state_parameter_dict.jio_storage;
data_uri = jIO.util.dataURItoBlob(data_uri);
console.log(this.state_parameter_dict.default_document);
console.log(url);
return storage.putAttachment(
this.state_parameter_dict.default_document,
url,
data_uri
);
})
.declareMethod('allDocs', function () {
var storage = this.state_parameter_dict.jio_storage;
console.log(this.state_parameter_dict.default_document);
return storage.allAttachments(this.state_parameter_dict.default_document)
.push(function(result) {
console.log(result);
return result;
})
})
.declareMethod('remove', function (url) {
var storage = this.state_parameter_dict.jio_storage;
return storage.removeAttachment(
this.state_parameter_dict.default_document,
url
);
});
}(window, rJS, jIO));
\ No newline at end of file
/*jslint indent: 2*/
/*global self, fetch, Request, Response */
var global = self, window = self;
(function (self, fetch, Request, Response) {
"use strict";
self.DOMParser = {};
self.sessionStorage = {};
self.localStorage = {};
self.openDatabase = {};
self.DOMError = {};
self.Node = {};
self.XMLSerializer = Object;
self.DOMParser = Object;
self.postMessage = function () {return; };
self.importScripts('app/rsvp.js', 'app/jiodev.js');
self.storage = {};
self.cache_list = [];
function createStorage(database) {
return self.jIO.createJIO({
type: "indexeddb",
database: database
});
}
function getFromLocal(relative_url) {
if (self.storage.get === undefined) {
self.storage = createStorage("ojs_source_code");
}
return self.storage.getAttachment(self.registration.scope, relative_url)
.push(function (blob) {
return new Response(blob, {
'headers': {
'content-type': blob.type
}
});
});
}
self.addEventListener('install', function (event) {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
var relative_url = event.request.url.split("#")[0]
.replace(self.registration.scope, "")
.replace(self.version_url, "");
if (relative_url === './no-cache') {
event.respondWith(new Response(self.cache_list));
return;
}
else if (event.request !== undefined && event.request.referrer === self.registration.scope) {
event.respondWith(
new self.RSVP.Queue()
.push(function () {
return fetch(event.request);
})
.push(undefined, function (error) {
if (error.name === 'TypeError' &&
error.message === 'Failed to fetch') {
return {};
}
throw error;
})
.push(function (response) {
if (response.status === 200) {
return response;
}
return getFromLocal(relative_url);
})
.push(undefined, function (error) {
return new Response(error, {"statusText": error.message, "status": 500});
})
);
} else {
event.respondWith(
new self.RSVP.Queue()
.push(function () {
return getFromLocal(relative_url);
})
.push(undefined, function (error) {
if (error instanceof self.jIO.util.jIOError) {
if (relative_url.indexOf('http') === -1) {
if (self.cache_list.indexOf(relative_url) === -1) {
self.cache_list.push(relative_url);
}
}
return fetch(event.request);
}
return new Response(error, {"statusText": error.message, "status": 500});
})
);
}
});
}(self, fetch, Request, Response));
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>CribJS Loader</title>
<link rel="stylesheet" href="./lib/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="./landing.css">
<script src="./lib/rsvp.js"></script>
<script src="./lib/renderjs.js"></script>
<script src="./gadget/gadget_global.js"></script>
<script src="./landing2.js"></script>
</head>
<body>
<div data-gadget-url="./gadget/gadget_landing_cribjs2.html"
data-gadget-scope="jio_cribjs"
data-gadget-sandbox="public"></div>
<div class="nav_content container">
<h1>CribJS Loader</h1>
<p>Load your Crib from here</p>
</div>
<div class="nav_content container">
<h3>Start Editing this Site</h3>
<a class="edit-url btn btn-primary" type="text" size="60" href="">Start Editing This Site Now</a>
</div>
<div class="nav_content container">
<form class="crib-load-from-zip-url form-inline">
<h3>Fill This site from a zip URL</h3>
<div class="form-group">
<label>Zip Url:
<input name="load-zip-url" class="load-zip-url form-control" type="text" size="60"></label>
</div>
<div class="form-group">
<label> Path in Zip:
<input name="load-from-zip-path" class="load-from-zip-path form-control" type="text" size="30" value="/">
</label>
</div>
<div class="form-group">
<label> to path:
<input name="load-zip-to-path" class="load-zip-to-path form-control" type="text" size="30" value="/">
</label>
</div>
<div class="form-group">
<label> And redirect to:
<input name="redirect-url" class="redirect-url form-control" type="text" size="30" value="">
</label>
</div>
<button name="load-zip-contents" class="load-zip-contents btn btn-default" type="submit">Import from URL</button>
</form>
</div>
<div class="nav_content container">
<div><span class="info crib-save-to-zip-status"></span></div>
<form class="crib-load-from-zip-file form-inline">
<h3>Import from zip File</h3>
<div class="form-group">
<label>Zip File:
<input name="load-zip-file" class="load-zip-file form-control" type="file" size="30"></label>
</div>
<div class="form-group">
<label> Path in Zip:
<input name="load-zip-path" class="load-from-zip-path form-control" type="text" size="30" value="/">
</label>
</div>
<div class="form-group">
<label> to path:
<input name="load-zip-to-path" class="load-zip-to-path form-control" type="text" size="30" value="/">
</label>
</div>
<div class="form-group">
<label> And redirect to:
<input name="redirect-url" class="redirect-url form-control" type="text" size="30" value="">
</label>
</div>
<button name="load-zip-contents" class="load-zip-contents btn btn-default" type="submit">Import from File</button>
</form>
</div>
<div class="nav_content container">
<p>You can check where it started, and start a crib from the beginning: <a href="jungle.html">The Jungle :)</a>. This one is a simple editor with no import/export functions. First challenge is to use that to add an import/export function :).</p>
<p>A simple editor can be found <a href="base.html">here</a></p>
</div>
</body>
</html>
/*jslint nomen: true, indent: 2, maxerr: 3 */
/*global window, rJS */
(function (window, rJS, loopEventListener) {
"use strict";
function getParameterDict () {
var hash = window.location.hash.substring(1),
params = {};
hash.split('&').map(hk => {
let temp = hk.split('=');
params[temp[0]] = temp[1];
});
return params;
}
function makeid(length) {
var result = '';
var characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function loadCribJSFromZipFile (gadget, event) {
return RSVP.Queue()
.push(function() {
return gadget.getDeclaredGadget("jio_cribjs");
})
.push(function(jio_cribjs_gadget) {
return jio_cribjs_gadget.loadFromZipFile({
path: document.location.href,
file_list: gadget.props.element.querySelector("form.crib-load-from-zip-file .load-zip-file").files,
from_path: gadget.props.element.querySelector("form.crib-load-from-zip-file .load-from-zip-path").value,
to_path: gadget.props.element.querySelector("form.crib-load-from-zip-file .load-zip-to-path").value,
application_id: "cribjs"
})
})
.push(function (url_list) {
document.location = gadget.props.element.querySelector("form.crib-load-from-zip-file .redirect-url").value;
})
.push(console.log, console.log);
}
function loadCribJSFromZipUrl (gadget, event) {
return RSVP.Queue()
.push(function() {
return gadget.getDeclaredGadget("jio_cribjs");
})
.push(function(jio_cribjs_gadget) {
return jio_cribjs_gadget.loadFromZipUrl({
path: document.location.href,
zip_url: gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-url").value,
from_path: gadget.props.element.querySelector("form.crib-load-from-zip-url .load-from-zip-path").value,
to_path: gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-to-path").value,
application_id: "cribjs"
})
})
.push(function (url_list) {
document.location = gadget.props.element.querySelector("form.crib-load-from-zip-url .redirect-url").value;
})
.push(console.log, console.log);
}
rJS(window)
.declareMethod('render', function (options) {
var gadget = this,
getURL = window.location,
site = getURL.protocol + "//" + getURL.host,
params = getParameterDict(),
edit_url="https://" + makeid(10) + ".cribjs.nexedi.net/crib-editor4/#page=editor&url="
+ site + "&crib_enable_url=" + site + "/crib-enable.html";
gadget.props.element.querySelector("a.edit-url").href = edit_url ;
if ( params.hasOwnProperty("from_path") ) {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-from-zip-path").value = params.from_path;
} else {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-from-zip-path").value = "cribjs-editor-master/";
}
if ( params.hasOwnProperty("to_path") ) {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-to-path").value = params.to_path;
} else {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-to-path").value = site;
}
if ( params.hasOwnProperty("zip_url") ) {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-url").value = params.zip_url;
} else {
gadget.props.element.querySelector("form.crib-load-from-zip-url .load-zip-url").value = "https://lab.nexedi.com/cedric.leninivin/cribjs-editor/-/archive/master/cribjs-editor-master.zip";
}
if ( params.hasOwnProperty("redirect_url") ) {
gadget.props.element.querySelector("form.crib-load-from-zip-url .redirect-url").value = params.redirect_url;
} else {
gadget.props.element.querySelector("form.crib-load-from-zip-url .redirect-url").value = site + "/index.html";
}
return RSVP.Queue()
.push(function () {
var promise_list = []
promise_list.push(loopEventListener(
gadget.props.element.querySelector("form.crib-load-from-zip-url"),
'submit',
false,
function (event) {loadCribJSFromZipUrl(gadget, event)}
));
promise_list.push(loopEventListener(
gadget.props.element.querySelector("form.crib-load-from-zip-file"),
'submit',
false,
function (event) {loadCribJSFromZipFile(gadget, event)}
));
return RSVP.all(promise_list)
})
.fail(function(e){console.log(e)})
})
.ready(function (g) {
g.props = {};
return g.getElement()
.push(function (element) {
g.props.element = element;
}).push(function() {
g.render({})
})
;
});
}(window, rJS, loopEventListener));
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment