Commit 8708c37a by Valera Rozuvan

Merge pull request #3126 from edx/valera/convert_xmodule_to_javascript

Converting XModule module to JS.
parents f3ff6cf3 269d7246
...@@ -6,3 +6,4 @@ ...@@ -6,3 +6,4 @@
# Tests for Time are written in pure JavaScript. # Tests for Time are written in pure JavaScript.
!time_spec.js !time_spec.js
!collapsible_spec.js !collapsible_spec.js
!xmodule_spec.js
(function () {
'use strict';
describe('XBlockToXModuleShim', function () {
describe('definition', function () {
it('XBlockToXModuleShim is defined, and is a function', function () {
expect($.isFunction(XBlockToXModuleShim)).toBe(true);
});
});
describe('implementation', function () {
var el,
videoModule = {
'module': 'video_module'
},
editCallback,
displayCallback,
removeNone,
removeVideo;
beforeEach(function () {
el = $('<div />');
if (window.None) {
spyOn(window, 'None');
removeNone = false;
} else {
window.None = jasmine.createSpy('None');
removeNone = true;
}
if (window.Video) {
spyOn(window, 'Video');
removeVideo = false;
} else {
window.Video = jasmine.createSpy('Video');
removeVideo = true;
}
window.Video.andReturn(videoModule);
editCallback = jasmine.createSpy('editCallback');
$(document).on('XModule.loaded.edit', editCallback);
spyOnEvent($(document), 'XModule.loaded.edit');
displayCallback = jasmine.createSpy('displayCallback');
$(document).on('XModule.loaded.display', displayCallback);
spyOnEvent($(document), 'XModule.loaded.display');
});
afterEach(function () {
el = null;
if (removeNone) {
window.None = undefined;
}
if (removeVideo) {
window.Video = undefined;
}
});
it('if element module is of type None, nothing happens', function () {
el.data('type', 'None');
expect(XBlockToXModuleShim(null, el)).toBeUndefined();
expect(window.None).not.toHaveBeenCalled();
});
it('if element module is of type Video, Video module constructor is called', function () {
el.data('type', 'Video');
expect(XBlockToXModuleShim(null, el)).toEqual(videoModule);
expect(window.Video).toHaveBeenCalled();
expect('XModule.loaded.edit').not.toHaveBeenTriggeredOn(document);
expect('XModule.loaded.display').not.toHaveBeenTriggeredOn(document);
});
it('if element has class "xmodule_edit"', function () {
el.data('type', 'Video')
.addClass('xmodule_edit');
XBlockToXModuleShim(null, el);
expect('XModule.loaded.edit').toHaveBeenTriggeredOn($(document));
expect(editCallback).toHaveBeenCalledWith(jasmine.any($.Event), el, videoModule);
expect('XModule.loaded.display').not.toHaveBeenTriggeredOn($(document));
});
it('if element has class "xmodule_display"', function () {
el.data('type', 'Video')
.addClass('xmodule_display');
XBlockToXModuleShim(null, el);
expect('XModule.loaded.edit').not.toHaveBeenTriggeredOn($(document));
expect('XModule.loaded.display').toHaveBeenTriggeredOn($(document));
expect(displayCallback).toHaveBeenCalledWith(jasmine.any($.Event), el, videoModule);
});
it('if element has classes "xmodule_edit", and "xmodule_display"', function () {
el.data('type', 'Video')
.addClass('xmodule_edit')
.addClass('xmodule_display');
XBlockToXModuleShim(null, el);
expect('XModule.loaded.edit').toHaveBeenTriggeredOn($(document));
expect('XModule.loaded.display').toHaveBeenTriggeredOn($(document));
});
it('element is of an unknown Module type, console.error() is called if it is defined', function () {
var oldConsole = window.console;
if (window.console && window.console.error) {
spyOn(window.console, 'error');
} else {
window.console = jasmine.createSpy('console.error');
}
el.data('type', 'UnknownModule');
expect(XBlockToXModuleShim(null, el)).toBeUndefined();
expect(console.error).toHaveBeenCalledWith(
'Unable to load UnknownModule: window[moduleType] is not a constructor'
);
window.console = oldConsole;
});
it('element is of an unknown Module type, JavaScript throws if console.error() is not defined', function () {
var oldConsole = window.console,
testFunction = function () {
return XBlockToXModuleShim(null, el);
};
if (window.console) {
window.console = undefined;
}
el.data('type', 'UnknownModule');
expect(testFunction).toThrow();
window.console = oldConsole;
});
});
});
describe('XModule.Descriptor', function () {
describe('definition', function () {
it('XModule is defined, and is a plain object', function () {
expect($.isPlainObject(XModule)).toBe(true);
});
it('XModule.Descriptor is defined, and is a function', function () {
expect($.isFunction(XModule.Descriptor)).toBe(true);
});
it('XModule.Descriptor has a complete prototype', function () {
expect($.isFunction(XModule.Descriptor.prototype.onUpdate)).toBe(true);
expect($.isFunction(XModule.Descriptor.prototype.update)).toBe(true);
expect($.isFunction(XModule.Descriptor.prototype.save)).toBe(true);
});
});
describe('implementation', function () {
var el, obj, callback, length;
// This is a dummy callback.
callback = function () {
var x = 1;
return x + 1;
};
beforeEach(function () {
el = 'dummy object';
obj = new XModule.Descriptor(el);
spyOn(obj, 'save').andCallThrough();
});
afterEach(function () {
el = null;
obj = null;
length = undefined;
});
it('Descriptor is a proper constructor function', function () {
expect(obj.hasOwnProperty('element')).toBe(true);
expect(obj.element).toBe(el);
expect(obj.hasOwnProperty('update')).toBe(true);
});
it('Descriptor.onUpdate called for the first time', function () {
expect(obj.hasOwnProperty('callbacks')).toBe(false);
obj.onUpdate(callback);
expect(obj.hasOwnProperty('callbacks')).toBe(true);
expect($.isArray(obj.callbacks)).toBe(true);
length = obj.callbacks.length;
expect(length).toBe(1);
expect(obj.callbacks[length - 1]).toBe(callback);
});
it('Descriptor.onUpdate called for Nth time', function () {
// In this test it doesn't matter what obj.callbacks
// consists of.
obj.callbacks = ['test1', 'test2', 'test3'];
obj.onUpdate(callback);
length = obj.callbacks.length;
expect(length).toBe(4);
expect(obj.callbacks[length - 1]).toBe(callback);
});
it('Descriptor.save returns a blank object', function () {
// NOTE: In the future the implementation of .save()
// method may change!
expect(obj.save()).toEqual({});
});
it('Descriptor.update triggers all callbacks with whatever .save() returns', function () {
var callback1 = jasmine.createSpy('callback1'),
callback2 = jasmine.createSpy('callback2'),
testValue = 'test 123';
obj.onUpdate(callback1);
obj.onUpdate(callback2);
obj.save.andReturn(testValue);
obj.update();
expect(callback1).toHaveBeenCalledWith(testValue);
expect(callback2).toHaveBeenCalledWith(testValue);
});
});
});
}).call(this);
...@@ -12,3 +12,4 @@ ...@@ -12,3 +12,4 @@
# Converted to JS from CoffeeScript. # Converted to JS from CoffeeScript.
!time.js !time.js
!collapsible.js !collapsible.js
!xmodule.js
@XModule = {}
@XBlockToXModuleShim = (runtime, element) ->
###
Load a single module (either an edit module or a display module)
from the supplied element, which should have a data-type attribute
specifying the class to load
###
moduleType = $(element).data('type')
if moduleType == 'None'
return
try
module = new window[moduleType](element)
if $(element).hasClass('xmodule_edit')
$(document).trigger('XModule.loaded.edit', [element, module])
if $(element).hasClass('xmodule_display')
$(document).trigger('XModule.loaded.display', [element, module])
return module
catch error
if window.console and console.log
console.error "Unable to load #{moduleType}: #{error.message}"
else
throw error
class @XModule.Descriptor
###
Register a callback method to be called when the state of this
descriptor is updated. The callback will be passed the results
of calling the save method on this descriptor.
###
onUpdate: (callback) ->
if ! @callbacks?
@callbacks = []
@callbacks.push(callback)
###
Notify registered callbacks that the state of this descriptor has changed
###
update: =>
data = @save()
callback(data) for callback in @callbacks
###
Bind the module to an element. This may be called multiple times,
if the element content has changed and so the module needs to be rebound
@method: constructor
@param {html element} the .xmodule_edit section containing all of the descriptor content
###
constructor: (@element) -> return
###
Return the current state of the descriptor (to be written to the module store)
@method: save
@returns {object} An object containing children and data attributes (both optional).
The contents of the attributes will be saved to the server
###
save: -> return {}
(function () {
'use strict';
var XModule = {};
XModule.Descriptor = (function () {
/*
* Bind the module to an element. This may be called multiple times,
* if the element content has changed and so the module needs to be rebound
*
* @method: constructor
* @param {html element} the .xmodule_edit section containing all of the descriptor content
*/
var Descriptor = function (element) {
this.element = element;
this.update = _.bind(this.update, this);
};
/*
* Register a callback method to be called when the state of this
* descriptor is updated. The callback will be passed the results
* of calling the save method on this descriptor.
*/
Descriptor.prototype.onUpdate = function (callback) {
if (!this.callbacks) {
this.callbacks = [];
}
this.callbacks.push(callback);
};
/*
* Notify registered callbacks that the state of this descriptor has changed
*/
Descriptor.prototype.update = function () {
var data, callbacks, i, length;
data = this.save();
callbacks = this.callbacks;
length = callbacks.length;
$.each(callbacks, function (index, callback) {
callback(data);
});
};
/*
* Return the current state of the descriptor (to be written to the module store)
*
* @method: save
* @returns {object} An object containing children and data attributes (both optional).
* The contents of the attributes will be saved to the server
*/
Descriptor.prototype.save = function () {
return {};
};
return Descriptor;
}());
this.XBlockToXModuleShim = function (runtime, element) {
/*
* Load a single module (either an edit module or a display module)
* from the supplied element, which should have a data-type attribute
* specifying the class to load
*/
var moduleType = $(element).data('type'),
module;
if (moduleType === 'None') {
return;
}
try {
module = new window[moduleType](element);
if ($(element).hasClass('xmodule_edit')) {
$(document).trigger('XModule.loaded.edit', [element, module]);
}
if ($(element).hasClass('xmodule_display')) {
$(document).trigger('XModule.loaded.display', [element, module]);
}
return module;
} catch (error) {
console.error('Unable to load ' + moduleType + ': ' + error.message);
}
};
// Export this module. We do it at the end when everything is ready
// because some RequireJS scripts require this module. If
// `window.XModule` appears as defined before this file has a chance
// to execute fully, then there is a chance that RequireJS will execute
// some script prematurely.
this.XModule = XModule;
}).call(this);
...@@ -64,10 +64,12 @@ class HTMLSnippet(object): ...@@ -64,10 +64,12 @@ class HTMLSnippet(object):
# this means we need to make sure that all xmodules include this dependency which had been previously implicitly # this means we need to make sure that all xmodules include this dependency which had been previously implicitly
# fulfilled in a different area of code # fulfilled in a different area of code
coffee = cls.js.setdefault('coffee', []) coffee = cls.js.setdefault('coffee', [])
fragment = resource_string(__name__, 'js/src/xmodule.coffee') js = cls.js.setdefault('js', [])
if fragment not in coffee: fragment = resource_string(__name__, 'js/src/xmodule.js')
coffee.insert(0, fragment)
if fragment not in js:
js.insert(0, fragment)
return cls.js return cls.js
......
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