blob: 918296820bdc963fda247880eeb0904ab22508b9 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
var utils = require('cordova/utils');
var nextGuid = 1;
/**
* Custom pub-sub "channel" that can have functions subscribed to it
* This object is used to define and control firing of events for
* cordova initialization, as well as for custom events thereafter.
*
* The order of events during page load and Cordova startup is as follows:
*
* onDOMContentLoaded* Internal event that is received when the web page is loaded and parsed.
* onNativeReady* Internal event that indicates the Cordova native side is ready.
* onCordovaReady* Internal event fired when all Cordova JavaScript objects have been created.
* onDeviceReady* User event fired to indicate that Cordova is ready
* onResume User event fired to indicate a start/resume lifecycle event
* onPause User event fired to indicate a pause lifecycle event
*
* The events marked with an * are sticky. Once they have fired, they will stay in the fired state.
* All listeners that subscribe after the event is fired will be executed right away.
*
* The only Cordova events that user code should register for are:
* deviceready Cordova native code is initialized and Cordova APIs can be called from JavaScript
* pause App has moved to background
* resume App has returned to foreground
*
* Listeners can be registered as:
* document.addEventListener("deviceready", myDeviceReadyListener, false);
* document.addEventListener("resume", myResumeListener, false);
* document.addEventListener("pause", myPauseListener, false);
*
* The DOM lifecycle events should be used for saving and restoring state
* window.onload
* window.onunload
*
*/
/**
* Channel
* @constructor
* @param type String the channel name
*/
var Channel = function (type, sticky) {
this.type = type;
// Map of guid -> function.
this.handlers = {};
// 0 = Non-sticky, 1 = Sticky non-fired, 2 = Sticky fired.
this.state = sticky ? 1 : 0;
// Used in sticky mode to remember args passed to fire().
this.fireArgs = null;
// Used by onHasSubscribersChange to know if there are any listeners.
this.numHandlers = 0;
// Function that is called when the first listener is subscribed, or when
// the last listener is unsubscribed.
this.onHasSubscribersChange = null;
};
var channel = {
/**
* Calls the provided function only after all of the channels specified
* have been fired. All channels must be sticky channels.
*/
join: function (h, c) {
var len = c.length;
var i = len;
var f = function () {
if (!(--i)) h();
};
for (var j = 0; j < len; j++) {
if (c[j].state === 0) {
throw Error('Can only use join with sticky channels.');
}
c[j].subscribe(f);
}
if (!len) h();
},
/* eslint-disable no-return-assign */
create: function (type) {
return channel[type] = new Channel(type, false);
},
createSticky: function (type) {
return channel[type] = new Channel(type, true);
},
/* eslint-enable no-return-assign */
/**
* cordova Channels that must fire before "deviceready" is fired.
*/
deviceReadyChannelsArray: [],
deviceReadyChannelsMap: {},
/**
* Indicate that a feature needs to be initialized before it is ready to be used.
* This holds up Cordova's "deviceready" event until the feature has been initialized
* and Cordova.initComplete(feature) is called.
*
* @param feature {String} The unique feature name
*/
waitForInitialization: function (feature) {
if (feature) {
var c = channel[feature] || this.createSticky(feature);
this.deviceReadyChannelsMap[feature] = c;
this.deviceReadyChannelsArray.push(c);
}
},
/**
* Indicate that initialization code has completed and the feature is ready to be used.
*
* @param feature {String} The unique feature name
*/
initializationComplete: function (feature) {
var c = this.deviceReadyChannelsMap[feature];
if (c) {
c.fire();
}
}
};
function checkSubscriptionArgument (argument) {
if (typeof argument !== 'function' && typeof argument.handleEvent !== 'function') {
throw new Error(
'Must provide a function or an EventListener object ' +
'implementing the handleEvent interface.'
);
}
}
/**
* Subscribes the given function to the channel. Any time that
* Channel.fire is called so too will the function.
* Optionally specify an execution context for the function
* and a guid that can be used to stop subscribing to the channel.
* Returns the guid.
*/
Channel.prototype.subscribe = function (eventListenerOrFunction, eventListener) {
checkSubscriptionArgument(eventListenerOrFunction);
var handleEvent, guid;
if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') {
// Received an EventListener object implementing the handleEvent interface
handleEvent = eventListenerOrFunction.handleEvent;
eventListener = eventListenerOrFunction;
} else {
// Received a function to handle event
handleEvent = eventListenerOrFunction;
}
if (this.state === 2) {
handleEvent.apply(eventListener || this, this.fireArgs);
return;
}
guid = eventListenerOrFunction.observer_guid;
if (typeof eventListener === 'object') {
handleEvent = utils.close(eventListener, handleEvent);
}
if (!guid) {
// First time any channel has seen this subscriber
guid = '' + nextGuid++;
}
handleEvent.observer_guid = guid;
eventListenerOrFunction.observer_guid = guid;
// Don't add the same handler more than once.
if (!this.handlers[guid]) {
this.handlers[guid] = handleEvent;
this.numHandlers++;
if (this.numHandlers === 1) {
this.onHasSubscribersChange && this.onHasSubscribersChange();
}
}
};
/**
* Unsubscribes the function with the given guid from the channel.
*/
Channel.prototype.unsubscribe = function (eventListenerOrFunction) {
checkSubscriptionArgument(eventListenerOrFunction);
var handleEvent, guid, handler;
if (eventListenerOrFunction && typeof eventListenerOrFunction === 'object') {
// Received an EventListener object implementing the handleEvent interface
handleEvent = eventListenerOrFunction.handleEvent;
} else {
// Received a function to handle event
handleEvent = eventListenerOrFunction;
}
guid = handleEvent.observer_guid;
handler = this.handlers[guid];
if (handler) {
delete this.handlers[guid];
this.numHandlers--;
if (this.numHandlers === 0) {
this.onHasSubscribersChange && this.onHasSubscribersChange();
}
}
};
/**
* Calls all functions subscribed to this channel.
*/
Channel.prototype.fire = function (e) {
var fail = false; // eslint-disable-line no-unused-vars
var fireArgs = Array.prototype.slice.call(arguments);
// Apply stickiness.
if (this.state === 1) {
this.state = 2;
this.fireArgs = fireArgs;
}
if (this.numHandlers) {
// Copy the values first so that it is safe to modify it from within
// callbacks.
var toCall = [];
for (var item in this.handlers) {
toCall.push(this.handlers[item]);
}
for (var i = 0; i < toCall.length; ++i) {
toCall[i].apply(this, fireArgs);
}
if (this.state === 2 && this.numHandlers) {
this.numHandlers = 0;
this.handlers = {};
this.onHasSubscribersChange && this.onHasSubscribersChange();
}
}
};
// defining them here so they are ready super fast!
// DOM event that is received when the web page is loaded and parsed.
channel.createSticky('onDOMContentLoaded');
// Event to indicate the Cordova native side is ready.
channel.createSticky('onNativeReady');
// Event to indicate that all Cordova JavaScript objects have been created
// and it's time to run plugin constructors.
channel.createSticky('onCordovaReady');
// Event to indicate that all automatically loaded JS plugins are loaded and ready.
// FIXME remove this
channel.createSticky('onPluginsReady');
// Event to indicate that Cordova is ready
channel.createSticky('onDeviceReady');
// Event to indicate a resume lifecycle event
channel.create('onResume');
// Event to indicate a pause lifecycle event
channel.create('onPause');
// Channels that must fire before "deviceready" is fired.
channel.waitForInitialization('onCordovaReady');
channel.waitForInitialization('onDOMContentLoaded');
module.exports = channel;