Permalink
Browse files

XWIKI-12038: Add support for triggering XWiki's custom Prototype.js e…

…vents from jQuery

* Bridge XWiki's custom events between Prototype.js and jQuery (both ways).
* Add integration test using Jasmine + RequireJS + WebJars.
* Split Jasmine tests into non-AMD and AMD.
  • Loading branch information...
mflorea committed Apr 21, 2015
1 parent 71edd00 commit a65618beeacb6c36aa5e6de178d7a495b6e18879
@@ -35,6 +35,18 @@
<!-- Don't run CLIRR on this module since there's no Java code. -->
<xwiki.clirr.skip>true</xwiki.clirr.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>requirejs</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Javascript and CSS files compression -->
@@ -130,22 +142,45 @@
<artifactId>jasmine-maven-plugin</artifactId>
<executions>
<execution>
<id>non-amd</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<sourceIncludes>
<include>js/prototype/prototype.js</include>
<include>uicomponents/widgets/list/xlist.js</include>
<include>uicomponents/suggest/suggest.js</include>
<include>uicomponents/model/entityReference.js</include>
</sourceIncludes>
<specIncludes>
<include>non-amd/**/*.js</include>
</specIncludes>
</configuration>
</execution>
<execution>
<!-- Tests for JavaScript code that follows the Asynchronous Module Definition pattern -->
<id>amd</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<specRunnerTemplate>REQUIRE_JS</specRunnerTemplate>
<customRunnerConfiguration>
${project.basedir}/src/test/resources/jasmine-require-config.txt
</customRunnerConfiguration>
<preloadSources>
<source>webjars/require.js</source>
</preloadSources>
<specIncludes>
<include>amd/**/*.js</include>
</specIncludes>
</configuration>
</execution>
</executions>
<configuration>
<sourceIncludes>
<include>js/prototype/prototype.js</include>
<include>uicomponents/widgets/list/xlist.js</include>
<include>uicomponents/suggest/suggest.js</include>
<include>uicomponents/model/entityReference.js</include>
</sourceIncludes>
<specIncludes>
<include>spec/**/*.js</include>
</specIncludes>
<jsSrcDir>${project.basedir}/src/main/webapp/resources</jsSrcDir>
<timeout>10</timeout>
</configuration>
</plugin>
</plugins>
@@ -1,32 +1,52 @@
// Bridge custom XWiki events fired from Prototype.js to jQuery.
require(['jquery'], function($) {
var triggerJQueryEvent = function(event) {
if (event.eventName.substr(0, 6) === 'xwiki:') {
var jQueryEvent = $.Event(event.eventName);
// This function is executed in the context of the target element.
$(this).trigger(jQueryEvent, event.memo);
if (jQueryEvent.isDefaultPrevented()) {
event.stop();
// Bridge custom XWiki events between Prototype.js and jQuery.
define(['jquery'], function($) {
var oldJQueryTrigger = $.event.trigger;
var oldPrototypeFire = Element.fire;
var shouldBridgeEvent = function(eventName) {
return eventName && eventName.substr(0, 6) === 'xwiki:';
};
var newJQueryTrigger = function(event, data, element, onlyHandlers) {
var result = oldJQueryTrigger(event, data, element, onlyHandlers);
var jQueryEvent, eventName;
if (event && typeof(event) === 'object' && event.type) {
jQueryEvent = event;
eventName = event.type;
} else if (typeof(event) === 'string') {
eventName = event;
}
var propagationStopped = jQueryEvent && typeof(jQueryEvent.isPropagationStopped) === 'function'
&& jQueryEvent.isPropagationStopped();
if (!propagationStopped && element && shouldBridgeEvent(eventName)) {
var memo = $.isArray(data) ? data[0] : data;
var bubble = !onlyHandlers;
var prototypeEvent = oldPrototypeFire(element, eventName, memo, bubble);
// Make sure the jQuery event can be canceled from Prototype.
if (prototypeEvent.stopped && jQueryEvent && typeof(jQueryEvent.preventDefault) === 'function') {
jQueryEvent.preventDefault();
}
}
return event;
};
return result;
}
var bridge = function(oldPrototypeFire) {
return function() {
// Prototype doesn't extend the event object if there are no registered event listeners. In IE8 the event object is
// missing the target element for custom events, if the event object is not extended. As a consequence calling
// Event#element() throws an exception. We need to know the target element in this case, and for this we execute
// triggerJQueryEvent() in the context of the target element.
//
// If this is an element or the document then use it as target, otherwise use the first method argument.
var target = (this.nodeType === 1 || this.nodeType === 9) ? this : arguments[0];
return triggerJQueryEvent.call(target, oldPrototypeFire.apply(this, arguments));
};
var newPrototypeFire = function(element, eventName, memo, bubble) {
var prototypeEvent = oldPrototypeFire(element, eventName, memo, bubble);
if (!prototypeEvent.stopped && shouldBridgeEvent(eventName)) {
var jQueryEvent = $.Event(eventName);
var data = memo ? [memo] : null;
var onlyHandlers = bubble === undefined ? false : !bubble;
oldJQueryTrigger(jQueryEvent, data, element, onlyHandlers);
// Make sure the Prototype event can be canceled from jQuery.
if (jQueryEvent.isDefaultPrevented()) {
prototypeEvent.stop();
}
}
return prototypeEvent;
};
$.each([Event, Element, document], function() {
this.fire = bridge(this.fire);
});
$.event.trigger = newJQueryTrigger;
Element.addMethods({fire: newPrototypeFire});
Object.extend(Event, {fire: newPrototypeFire});
Object.extend(document, {fire: newPrototypeFire.methodize()});
});
@@ -0,0 +1,142 @@
define(['jquery', 'prototype', 'xwiki-events-bridge'], function($j, $p) {
describe('Events Bridge', function() {
it('Dependencies', function() {
expect(typeof $j).toBe('function');
expect(typeof $p).toBe('function');
expect($).toBe($p);
});
describe('Prototype -> jQuery', function() {
it('document.fire', function() {
var counter = 1;
document.observe('xwiki:test', function(event) {
document.stopObserving('xwiki:test');
counter *= event.memo.delta;
});
$j(document).one('xwiki:test', function(event, data) {
counter += data.delta;
});
document.fire('xwiki:test', {'delta': 3});
expect(counter).toBe(6);
});
it('$(element).fire', function() {
var counter = 7;
$p(document.body).observe('xwiki:test', function(event) {
$p(document.body).stopObserving('xwiki:test');
counter -= event.memo;
});
$j(document.body).one('xwiki:test', function(event, data) {
counter *= data;
});
$p(document.body).fire('xwiki:test', 2);
expect(counter).toBe(10);
});
it('event.stop', function() {
var counter = 1;
document.observe('xwiki:test', function(event) {
document.stopObserving('xwiki:test');
event.stop();
counter += event.memo.delta;
});
$j(document).on('xwiki:test', function(event, data) {
counter += data.delta;
});
document.fire('xwiki:test', {'delta': 3});
$j(document).off('xwiki:test');
expect(counter).toBe(4);
});
it('event.preventDefault', function() {
$j(document).one('xwiki:test', function(event) {
event.preventDefault();
});
var event = document.fire('xwiki:test');
expect(event.stopped).toBe(true);
});
it('filter xwiki:* events', function() {
var counter = 7;
$p(document.body).observe('wiki:test', function(event) {
$p(document.body).stopObserving('wiki:test');
counter -= event.memo;
});
$j(document.body).one('wiki:test', function(event, data) {
counter *= data;
});
$p(document.body).fire('wiki:test', 2);
expect(counter).toBe(5);
});
});
describe('jQuery -> Prototype', function() {
it('$(document).trigger', function() {
var counter = -6;
$j(document).one('xwiki:test', function(event, data) {
counter += data;
});
document.observe('xwiki:test', function(event) {
document.stopObserving('xwiki:test');
counter *= event.memo;
});
$j(document).trigger('xwiki:test', 4);
expect(counter).toBe(-8);
});
it('$(element).trigger', function() {
var counter = 6;
$j(document.body).one('xwiki:test', function(event, data) {
counter += data.delta;
});
$p(document.body).observe('xwiki:test', function(event) {
$p(document.body).stopObserving('xwiki:test');
counter /= event.memo.delta;
});
$j(document.body).trigger('xwiki:test', {'delta': 2});
expect(counter).toBe(4);
});
it('event.stopPropagation', function() {
var counter = 1;
$j(document).one('xwiki:test', function(event, data) {
event.stopPropagation();
counter += data.delta;
});
document.observe('xwiki:test', function(event) {
counter *= event.memo.delta;
});
$j(document).trigger($j.Event('xwiki:test'), [{'delta': 3}]);
document.stopObserving('xwiki:test');
expect(counter).toBe(4);
});
it('event.stop', function() {
document.observe('xwiki:test', function(event) {
document.stopObserving('xwiki:test');
event.stop();
});
var event = $j.Event('xwiki:test');
$j(document).trigger(event);
expect(event.isDefaultPrevented()).toBe(true);
});
it('filter xwiki:* events', function() {
var counter = 7;
$j(document.body).one('wiki:test', function(event, data) {
counter -= data;
});
$p(document.body).observe('wiki:test', function(event) {
$p(document.body).stopObserving('wiki:test');
counter *= event.memo;
});
$j(document.body).trigger('wiki:test', 2);
expect(counter).toBe(5);
});
});
});
});
@@ -0,0 +1,4 @@
define('jQueryNoConflict', ['jquery'], function($) {
$.noConflict();
return $;
});
@@ -0,0 +1,24 @@
paths: {
// I assume the '../../../' prefix is needed in order to get out of 'main/webapp/resources', but I'm not 100% sure.
// See http://stackoverflow.com/questions/24654534/jasmine-maven-plugin-load-some-other-test-utilities-common-script
'jquery': '../../../webjars/jquery',
'jQueryNoConflict': '../../../spec/jQueryNoConflict',
'prototype': 'js/prototype/prototype',
'xwiki-events-bridge': 'js/xwiki/eventsBridge'
},
map: {
// '*' means all modules will get 'jQueryNoConflict' for their 'jquery' dependency.
'*': {'jquery': 'jQueryNoConflict'},
// 'jQueryNoConflict' wants the real jQuery module though. If this line was not here, there would be an unresolvable
// cyclic dependency.
'jQueryNoConflict': {'jquery': 'jquery'}
},
shim: {
'prototype': {
exports: '$'
},
'xwiki-events-bridge': {
// The dependency on Prototype.js is not declared.
deps: ['jquery', 'prototype']
}
}

0 comments on commit a65618b

Please sign in to comment.