define([
'jquery',
'moduleVersions',
'internal/sitebuilder/common/log',
'spine',
'internal/sitebuilder/common/webs.modules',
'internal/sitebuilder/common/creativeCommons'
], function($, moduleVersions, log, Spine, websModules) {
var MODULE_LOAD_TIMEOUT = 30000;
var RESOLVED_PROMISE = $.Deferred().resolve().promise();
var ModuleClassLoader = Spine.Class.create({
init: function(options) {
this.MODULES_URL = options.MODULES_URL;
this.MODULES_VERSION = options.MODULES_VERSION;
/**
* Map of module slugs to classes
*/
this.classes = {};
/**
* Map from module slugs to a promises for when it loads
*/
this.modulePromises = {};
/**
* Map from module slugs to promises for when the JS file loads
*/
this.moduleJSPromises = {};
},
getClass: function(moduleSlug) {
return this.classes[moduleSlug];
},
/**
* Called from the module definition file. Each module type is only registered once.
*/
register: function(moduleSlug, include, extend) {
var superClass;
extend.slug = moduleSlug;
if(extend.iframe) {
superClass = websModules.IframeModule;
} else if(extend.isWidget) {
superClass = websModules.WidgetModule;
} else if(extend.submodules) {
superClass = websModules.CompositeModule;
} else {
superClass = websModules.CustomModule;
}
this.classes[moduleSlug] = superClass.create(include, extend);
if(!this.moduleJSPromises[moduleSlug]) {
this.moduleJSPromises[moduleSlug] = new $.Deferred();
}
this.moduleJSPromises[moduleSlug].resolve();
},
/**
* This create method is used when we KNOW that we have already loaded the module
*
* @param {ModuleClass} moduleClass
* @param {Object} options
* @param {$.Deferred} deferred
* @returns {*}
* @private
*/
_create: function(moduleClass, options, deferred) {
var
moduleSlug = moduleClass.slug,
proto = $.extend({}, moduleClass),
data = $.extend(true, {}, moduleClass.defaultData, options.data),
style = moduleClass.styles[data._style] || moduleClass.defaultStyle;
// TODO: HACK for buckets. jQuery deep extend also does arrays :(
if(data.bucketContents && options.data && options.data.bucketContents) {
data.bucketContents = options.data.bucketContents;
// Hack for SITEBUILDER-2017 and SITEBUILDER-2025
// Sometimes the image values were stored as NaN which caused image to come back as null
// So we need to default it
for(var i = 0, len = data.cols.length; i < len; i++) {
if(!data.cols[i].image) {
log.error('An image within buckets was null');
data.cols[i].image = moduleClass.defaultData.cols[0].image;
}
}
}
if(moduleSlug == 'contact_form' && options.data && options.data.fields) {
data.fields = options.data.fields;
}
var obj = this.getClassForStyle(moduleClass, style).init({
el: options.container || options.el,
data: data
});
deferred.resolve(obj);
this.trigger('created', obj);
return obj;
},
/**
* Essentially the same as create, but for appModules.
* Sidebar modules are different. We don't call 'load' on them.
*/
createAppModule: function(moduleSlug, options) {
var
moduleClass = this.getClass(moduleSlug),
deferred = $.Deferred();
if(!moduleClass) {
// If module is not registered, register a fake one and log an error
moduleClass = this.classes[moduleSlug] = websModules.AppModule.create({id: moduleSlug.substring(12)});
log.error('Unable to load appmodule', moduleSlug);
}
this._create(moduleClass, options, deferred);
return deferred.promise();
},
create: function(moduleSlug, options) {
if(moduleSlug.indexOf('app-sidebar-') === 0) {
return this.createAppModule(moduleSlug, options);
}
var
self = this,
moduleClass = this.getClass(moduleSlug),
deferred = $.Deferred();
self.load(moduleSlug).done(function() {
self._create(self.getClass(moduleSlug), options, deferred);
}).fail(deferred.reject);
return deferred.promise();
},
/**
* Given a module class and a style, return the proper class to init
* Styles have their own subclasses of the moduleClass, because they can add methods
* TODO: This should probably move to Module.getClassForStyle
*/
getClassForStyle: function(moduleClass, style) {
if(style) {
/* jshint ignore:start */
// Collect all styles (because styles can inherit from other styles)
var styles = [];
do {
styles.push(style);
} while(style = moduleClass.styles[style.inherit]);
/* jshint ignore:end */
// Iterate through the styles in reverse, so that the base style is first, and the specified style is last
var
superClass = moduleClass,
subClass,
s;
for(var i = styles.length - 1; i >= 0; i--) {
s = styles[i];
subClass = s.klass;
if(!subClass) {
delete s.defaultStyle;
if(s.global.js) {
// Extend the super class
var include = {}, extend = {};
s.global.js(include, extend);
s.klass = subClass = superClass.create(include, extend);
} else {
// This happens when a style doesn't change the JS, but does change the CSS
// No JS to add. Just use the super class
s.klass = subClass = superClass;
}
}
superClass = subClass;
}
return superClass;
} else {
// How!?!?
// TODO: In the future, not all modules will need a style
if(log) {
log.trigger('Modules', 'info', 'No style found for ' + moduleClass.slug + ' module!', moduleClass);
}
return moduleClass;
}
},
getModuleSlugFromUrl: function(url) {
var urlParts = url.split('/');
var moduleSlug;
if(urlParts[2] == 'modules') {
moduleSlug = urlParts[3];
} else {
moduleSlug = urlParts[3] + '_' + urlParts[urlParts.length - 1].replace('.less', '');
}
return moduleSlug;
},
getModuleVersion: function(slug) {
var version;
if(moduleVersions && slug in moduleVersions) {
version = 'v' + moduleVersions[slug];
} else {
log.warn('WARNING: Retrieving unversioned asset for module ' + slug);
version = this.MODULES_VERSION;
}
return version;
},
getModuleAssetURL: function(slug, path) {
return this.MODULES_URL + slug + '/' + this.getModuleVersion(slug) + '/' + path;
},
cssPath: function(slug) {
return this.getModuleAssetURL(slug, 'view.packaged.less');
},
/**
* Load a module class from the backend
*/
load: function(slug) {
if (this.modulePromises[slug]) {
return this.modulePromises[slug];
}
var
self = this,
cssLoadedPromise = this.loadCss(this.cssPath(slug)),
dependenciesLoadedPromise = $.Deferred();
this.modulePromises[slug] = $.Deferred();
if (!this.moduleJSPromises[slug]) {
this.moduleJSPromises[slug] = new $.Deferred();
}
this.loadJs(slug);
this.moduleJSPromises[slug].done(function() {
self.loadModuleDependencies(slug).done(dependenciesLoadedPromise.resolve).fail(dependenciesLoadedPromise.reject);
}).fail(dependenciesLoadedPromise.reject);
// Module is loaded when css and js are all loaded.
$.when(cssLoadedPromise, dependenciesLoadedPromise)
.done(self.modulePromises[slug].resolve)
.fail(self.modulePromises[slug].reject);
// Trigger error callbacks if not laoded quickly.
setTimeout(this.modulePromises[slug].reject, MODULE_LOAD_TIMEOUT);
return this.modulePromises[slug];
},
loadAll: function(types) {
var
deferredBulkLoad = $.Deferred(),
typePromises = [];
for(var i = 0, len = types.length; i < len; i++) {
typePromises.push(this.load(types[i]));
}
$.when.apply($, typePromises).done(deferredBulkLoad.resolve).fail(deferredBulkLoad.reject);
return deferredBulkLoad.promise();
},
loadCss: function(url) {
// In view mode, all the CSS is already loaded
return RESOLVED_PROMISE;
},
loadJs: function(moduleSlug) {
var deferred = $.Deferred();
require([this.getModuleAssetURL(moduleSlug, moduleSlug + '_view.js')], null);
return deferred;
},
/* Loads theme style css and returns a list of theme style js files */
themeStyleFileList: function(moduleSlug) {
// Per-Theme Module Styles
var
self = this,
moduleClass = this.getClass(moduleSlug),
theme = webs.theme,
files = [],
cssPromises = [];
if(theme.moduleStyles && theme.moduleStyles[moduleSlug]) {
$.each(theme.moduleStyles[moduleSlug], function(styleSlug, def) {
if(log) {
log.trigger('Modules', 'debug', 'Loading theme style ' + styleSlug + ' for ' + moduleSlug);
}
moduleClass.styles[styleSlug] = def;
def.slug = styleSlug;
if(def.global.js) {
files.push(theme.url + '/' + def.global.js);
}
if(def.global.css) {
cssPromises.push(self.loadCss(theme.url + '/' + def.global.css));
}
// Set this as the default style
//jscs:disable requireDotNotation
if(def['default']) {
moduleClass.defaultStyle = def;
}
//jscs:enable requireDotNotation
});
}
return [files, $.when.apply($,cssPromises)];
},
/* A list of module names the given module depends on */
moduleDependencyList: function(moduleSlug) {
var
submodules = [],
moduleClass = this.getClass(moduleSlug);
// Module js dependencies
if(moduleClass.submodules) {
$.each(moduleClass.submodules, function(slug, sm) {
submodules.push(slug);
});
}
return submodules;
},
shouldConsolidateAssets: function(moduleSlug) {
return moduleSlug.indexOf('zumba') === -1 && moduleSlug.indexOf('app-sidebar') === -1;
},
/* Loads both theme styles and module dependencies for the given module. */
loadModuleDependencies: function(moduleSlug) {
var
self = this,
dependencies = this.themeStyleFileList(moduleSlug),
jsFiles = dependencies[0],
cssPromise = dependencies[1],
modules = this.moduleDependencyList(moduleSlug),
total = jsFiles.length + modules.length,
loaded = 0;
if(total === 0) {
return cssPromise;
} else {
var
deferred = $.Deferred(),
allRequiredPromises = [cssPromise];
$.each(modules, function(index, moduleSlug) {
allRequiredPromises.push(self.load(moduleSlug));
});
$.each(jsFiles, function(index, file) {
var jsPromise = $.Deferred();
allRequiredPromises.push(jsPromise);
require([script(file)], jsPromise.resolve);
});
$.when.apply($, allRequiredPromises).done(deferred.resolve).fail(deferred.reject);
return deferred.promise();
}
}
});
ModuleClassLoader.include(Spine.Events);
var mcl = ModuleClassLoader.init({
MODULES_URL: webs.props.dynamicAssetServer + '/s/modules/',
MODULES_VERSION: webs.props.modulesVersion
});
var deferredModules = $.Deferred();
mcl.renderedModulesPromise = deferredModules.promise();
$(function() {
$.when.apply($, webs.renderedModulesPromises || []).done(deferredModules.resolve).fail(deferredModules.reject);
});
websModules.ModuleClassLoader = mcl;
return mcl;
});