luci-base: luci.js: add dynamic class loader
authorJo-Philipp Wich <jo@mein.io>
Mon, 7 Jan 2019 13:48:19 +0000 (14:48 +0100)
committerJo-Philipp Wich <jo@mein.io>
Sun, 7 Jul 2019 13:25:49 +0000 (15:25 +0200)
Introduce L.require() to fetch additional JavaScript classes.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/htdocs/luci-static/resources/luci.js

index 7cddc4e10d06e98f948228c26426add6157ca82a..610cbcb62a24aa3b9f5d229fe0d6b6573e3653e6 100644 (file)
            tooltipTimeout = null,
            dummyElem = null,
            domParser = null,
-           originalCBIInit = null;
+           originalCBIInit = null,
+           classes = {};
 
        LuCI = Class.extend({
                __name__: 'LuCI',
                        window.cbi_init = function() {};
                },
 
+               /* Class require */
+               require: function(name, from) {
+                       var L = this, url = null, from = from || [];
+
+                       /* Class already loaded */
+                       if (classes[name] != null) {
+                               /* Circular dependency */
+                               if (from.indexOf(name) != -1)
+                                       throw new Error('Circular dependency: class "%s" depends on "%s"'
+                                               .format(name, from.join('" which depends on "')));
+
+                               return classes[name];
+                       }
+
+                       document.querySelectorAll('script[src$="/luci.js"]').forEach(function(s) {
+                               url = '%s/%s.js'.format(
+                                       s.getAttribute('src').replace(/\/luci\.js$/, ''),
+                                       name.replace(/\./g, '/'));
+                       });
+
+                       if (url == null)
+                               throw new Error('Cannot find url of luci.js');
+
+                       from = [ name ].concat(from);
+
+                       var compileClass = function(res) {
+                               if (!res.ok)
+                                       throw new Error('HTTP error %d while loading class file "%s"'
+                                               .format(res.status, url));
+
+                               var source = res.text(),
+                                   reqmatch = /(?:^|\n)[ \t]*(?:["']require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?["']);/g,
+                                   depends = [],
+                                   args = '';
+
+                               /* find require statements in source */
+                               for (var m = reqmatch.exec(source); m; m = reqmatch.exec(source)) {
+                                       var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
+                                       depends.push(L.require(dep, from));
+                                       args += ', ' + as;
+                               }
+
+                               /* load dependencies and instantiate class */
+                               return Promise.all(depends).then(function(instances) {
+                                       try {
+                                               _factory = eval(
+                                                       '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
+                                                               .format(args, source, res.url));
+                                       }
+                                       catch (error) {
+                                               throw new SyntaxError('%s\n  in %s:%s'
+                                                       .format(error.message, res.url, error.lineNumber || '?'));
+                                       }
+
+                                       _factory.displayName = toCamelCase(name + 'ClassFactory');
+                                       _class = _factory.apply(_factory, [window, document, L].concat(instances));
+
+                                       if (!Class.isSubclass(_class))
+                                               throw new TypeError('"%s" factory yields invalid constructor'
+                                                       .format(name));
+
+                                       if (_class.displayName == 'AnonymousClass')
+                                               _class.displayName = toCamelCase(name + 'Class');
+
+                                       var ptr = Object.getPrototypeOf(L),
+                                           parts = name.split(/\./),
+                                           instance = new _class();
+
+                                       for (var i = 0; ptr && i < parts.length - 1; i++)
+                                               ptr = ptr[parts[i]];
+
+                                       if (!ptr)
+                                               throw new Error('Parent "%s" for class "%s" is missing'
+                                                       .format(parts.slice(0, i).join('.'), name));
+
+                                       classes[name] = ptr[parts[i]] = instance;
+
+                                       return instance;
+                               });
+                       };
+
+                       /* Request class file */
+                       classes[name] = Request.get(url, { cache: true }).then(compileClass);
+
+                       return classes[name];
+               },
+
                /* DOM setup */
                setupDOM: function(ev) {
                        this.tabs.init();