329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
var forEach = require("./collection-utils").forEach;
|
|
var elementUtilsMaker = require("./element-utils");
|
|
var listenerHandlerMaker = require("./listener-handler");
|
|
var idGeneratorMaker = require("./id-generator");
|
|
var idHandlerMaker = require("./id-handler");
|
|
var reporterMaker = require("./reporter");
|
|
var browserDetector = require("./browser-detector");
|
|
var batchProcessorMaker = require("batch-processor");
|
|
var stateHandler = require("./state-handler");
|
|
|
|
//Detection strategies.
|
|
var objectStrategyMaker = require("./detection-strategy/object.js");
|
|
var scrollStrategyMaker = require("./detection-strategy/scroll.js");
|
|
|
|
function isCollection(obj) {
|
|
return Array.isArray(obj) || obj.length !== undefined;
|
|
}
|
|
|
|
function toArray(collection) {
|
|
if (!Array.isArray(collection)) {
|
|
var array = [];
|
|
forEach(collection, function (obj) {
|
|
array.push(obj);
|
|
});
|
|
return array;
|
|
} else {
|
|
return collection;
|
|
}
|
|
}
|
|
|
|
function isElement(obj) {
|
|
return obj && obj.nodeType === 1;
|
|
}
|
|
|
|
/**
|
|
* @typedef idHandler
|
|
* @type {object}
|
|
* @property {function} get Gets the resize detector id of the element.
|
|
* @property {function} set Generate and sets the resize detector id of the element.
|
|
*/
|
|
|
|
/**
|
|
* @typedef Options
|
|
* @type {object}
|
|
* @property {boolean} callOnAdd Determines if listeners should be called when they are getting added.
|
|
Default is true. If true, the listener is guaranteed to be called when it has been added.
|
|
If false, the listener will not be guarenteed to be called when it has been added (does not prevent it from being called).
|
|
* @property {idHandler} idHandler A custom id handler that is responsible for generating, setting and retrieving id's for elements.
|
|
If not provided, a default id handler will be used.
|
|
* @property {reporter} reporter A custom reporter that handles reporting logs, warnings and errors.
|
|
If not provided, a default id handler will be used.
|
|
If set to false, then nothing will be reported.
|
|
* @property {boolean} debug If set to true, the the system will report debug messages as default for the listenTo method.
|
|
*/
|
|
|
|
/**
|
|
* Creates an element resize detector instance.
|
|
* @public
|
|
* @param {Options?} options Optional global options object that will decide how this instance will work.
|
|
*/
|
|
module.exports = function(options) {
|
|
options = options || {};
|
|
|
|
//idHandler is currently not an option to the listenTo function, so it should not be added to globalOptions.
|
|
var idHandler;
|
|
|
|
if (options.idHandler) {
|
|
// To maintain compatability with idHandler.get(element, readonly), make sure to wrap the given idHandler
|
|
// so that readonly flag always is true when it's used here. This may be removed next major version bump.
|
|
idHandler = {
|
|
get: function (element) { return options.idHandler.get(element, true); },
|
|
set: options.idHandler.set
|
|
};
|
|
} else {
|
|
var idGenerator = idGeneratorMaker();
|
|
var defaultIdHandler = idHandlerMaker({
|
|
idGenerator: idGenerator,
|
|
stateHandler: stateHandler
|
|
});
|
|
idHandler = defaultIdHandler;
|
|
}
|
|
|
|
//reporter is currently not an option to the listenTo function, so it should not be added to globalOptions.
|
|
var reporter = options.reporter;
|
|
|
|
if(!reporter) {
|
|
//If options.reporter is false, then the reporter should be quiet.
|
|
var quiet = reporter === false;
|
|
reporter = reporterMaker(quiet);
|
|
}
|
|
|
|
//batchProcessor is currently not an option to the listenTo function, so it should not be added to globalOptions.
|
|
var batchProcessor = getOption(options, "batchProcessor", batchProcessorMaker({ reporter: reporter }));
|
|
|
|
//Options to be used as default for the listenTo function.
|
|
var globalOptions = {};
|
|
globalOptions.callOnAdd = !!getOption(options, "callOnAdd", true);
|
|
globalOptions.debug = !!getOption(options, "debug", false);
|
|
|
|
var eventListenerHandler = listenerHandlerMaker(idHandler);
|
|
var elementUtils = elementUtilsMaker({
|
|
stateHandler: stateHandler
|
|
});
|
|
|
|
//The detection strategy to be used.
|
|
var detectionStrategy;
|
|
var desiredStrategy = getOption(options, "strategy", "object");
|
|
var importantCssRules = getOption(options, "important", false);
|
|
var strategyOptions = {
|
|
reporter: reporter,
|
|
batchProcessor: batchProcessor,
|
|
stateHandler: stateHandler,
|
|
idHandler: idHandler,
|
|
important: importantCssRules
|
|
};
|
|
|
|
if(desiredStrategy === "scroll") {
|
|
if (browserDetector.isLegacyOpera()) {
|
|
reporter.warn("Scroll strategy is not supported on legacy Opera. Changing to object strategy.");
|
|
desiredStrategy = "object";
|
|
} else if (browserDetector.isIE(9)) {
|
|
reporter.warn("Scroll strategy is not supported on IE9. Changing to object strategy.");
|
|
desiredStrategy = "object";
|
|
}
|
|
}
|
|
|
|
if(desiredStrategy === "scroll") {
|
|
detectionStrategy = scrollStrategyMaker(strategyOptions);
|
|
} else if(desiredStrategy === "object") {
|
|
detectionStrategy = objectStrategyMaker(strategyOptions);
|
|
} else {
|
|
throw new Error("Invalid strategy name: " + desiredStrategy);
|
|
}
|
|
|
|
//Calls can be made to listenTo with elements that are still being installed.
|
|
//Also, same elements can occur in the elements list in the listenTo function.
|
|
//With this map, the ready callbacks can be synchronized between the calls
|
|
//so that the ready callback can always be called when an element is ready - even if
|
|
//it wasn't installed from the function itself.
|
|
var onReadyCallbacks = {};
|
|
|
|
/**
|
|
* Makes the given elements resize-detectable and starts listening to resize events on the elements. Calls the event callback for each event for each element.
|
|
* @public
|
|
* @param {Options?} options Optional options object. These options will override the global options. Some options may not be overriden, such as idHandler.
|
|
* @param {element[]|element} elements The given array of elements to detect resize events of. Single element is also valid.
|
|
* @param {function} listener The callback to be executed for each resize event for each element.
|
|
*/
|
|
function listenTo(options, elements, listener) {
|
|
function onResizeCallback(element) {
|
|
var listeners = eventListenerHandler.get(element);
|
|
forEach(listeners, function callListenerProxy(listener) {
|
|
listener(element);
|
|
});
|
|
}
|
|
|
|
function addListener(callOnAdd, element, listener) {
|
|
eventListenerHandler.add(element, listener);
|
|
|
|
if(callOnAdd) {
|
|
listener(element);
|
|
}
|
|
}
|
|
|
|
//Options object may be omitted.
|
|
if(!listener) {
|
|
listener = elements;
|
|
elements = options;
|
|
options = {};
|
|
}
|
|
|
|
if(!elements) {
|
|
throw new Error("At least one element required.");
|
|
}
|
|
|
|
if(!listener) {
|
|
throw new Error("Listener required.");
|
|
}
|
|
|
|
if (isElement(elements)) {
|
|
// A single element has been passed in.
|
|
elements = [elements];
|
|
} else if (isCollection(elements)) {
|
|
// Convert collection to array for plugins.
|
|
// TODO: May want to check so that all the elements in the collection are valid elements.
|
|
elements = toArray(elements);
|
|
} else {
|
|
return reporter.error("Invalid arguments. Must be a DOM element or a collection of DOM elements.");
|
|
}
|
|
|
|
var elementsReady = 0;
|
|
|
|
var callOnAdd = getOption(options, "callOnAdd", globalOptions.callOnAdd);
|
|
var onReadyCallback = getOption(options, "onReady", function noop() {});
|
|
var debug = getOption(options, "debug", globalOptions.debug);
|
|
|
|
forEach(elements, function attachListenerToElement(element) {
|
|
if (!stateHandler.getState(element)) {
|
|
stateHandler.initState(element);
|
|
idHandler.set(element);
|
|
}
|
|
|
|
var id = idHandler.get(element);
|
|
|
|
debug && reporter.log("Attaching listener to element", id, element);
|
|
|
|
if(!elementUtils.isDetectable(element)) {
|
|
debug && reporter.log(id, "Not detectable.");
|
|
if(elementUtils.isBusy(element)) {
|
|
debug && reporter.log(id, "System busy making it detectable");
|
|
|
|
//The element is being prepared to be detectable. Do not make it detectable.
|
|
//Just add the listener, because the element will soon be detectable.
|
|
addListener(callOnAdd, element, listener);
|
|
onReadyCallbacks[id] = onReadyCallbacks[id] || [];
|
|
onReadyCallbacks[id].push(function onReady() {
|
|
elementsReady++;
|
|
|
|
if(elementsReady === elements.length) {
|
|
onReadyCallback();
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
debug && reporter.log(id, "Making detectable...");
|
|
//The element is not prepared to be detectable, so do prepare it and add a listener to it.
|
|
elementUtils.markBusy(element, true);
|
|
return detectionStrategy.makeDetectable({ debug: debug, important: importantCssRules }, element, function onElementDetectable(element) {
|
|
debug && reporter.log(id, "onElementDetectable");
|
|
|
|
if (stateHandler.getState(element)) {
|
|
elementUtils.markAsDetectable(element);
|
|
elementUtils.markBusy(element, false);
|
|
detectionStrategy.addListener(element, onResizeCallback);
|
|
addListener(callOnAdd, element, listener);
|
|
|
|
// Since the element size might have changed since the call to "listenTo", we need to check for this change,
|
|
// so that a resize event may be emitted.
|
|
// Having the startSize object is optional (since it does not make sense in some cases such as unrendered elements), so check for its existance before.
|
|
// Also, check the state existance before since the element may have been uninstalled in the installation process.
|
|
var state = stateHandler.getState(element);
|
|
if (state && state.startSize) {
|
|
var width = element.offsetWidth;
|
|
var height = element.offsetHeight;
|
|
if (state.startSize.width !== width || state.startSize.height !== height) {
|
|
onResizeCallback(element);
|
|
}
|
|
}
|
|
|
|
if(onReadyCallbacks[id]) {
|
|
forEach(onReadyCallbacks[id], function(callback) {
|
|
callback();
|
|
});
|
|
}
|
|
} else {
|
|
// The element has been unisntalled before being detectable.
|
|
debug && reporter.log(id, "Element uninstalled before being detectable.");
|
|
}
|
|
|
|
delete onReadyCallbacks[id];
|
|
|
|
elementsReady++;
|
|
if(elementsReady === elements.length) {
|
|
onReadyCallback();
|
|
}
|
|
});
|
|
}
|
|
|
|
debug && reporter.log(id, "Already detecable, adding listener.");
|
|
|
|
//The element has been prepared to be detectable and is ready to be listened to.
|
|
addListener(callOnAdd, element, listener);
|
|
elementsReady++;
|
|
});
|
|
|
|
if(elementsReady === elements.length) {
|
|
onReadyCallback();
|
|
}
|
|
}
|
|
|
|
function uninstall(elements) {
|
|
if(!elements) {
|
|
return reporter.error("At least one element is required.");
|
|
}
|
|
|
|
if (isElement(elements)) {
|
|
// A single element has been passed in.
|
|
elements = [elements];
|
|
} else if (isCollection(elements)) {
|
|
// Convert collection to array for plugins.
|
|
// TODO: May want to check so that all the elements in the collection are valid elements.
|
|
elements = toArray(elements);
|
|
} else {
|
|
return reporter.error("Invalid arguments. Must be a DOM element or a collection of DOM elements.");
|
|
}
|
|
|
|
forEach(elements, function (element) {
|
|
eventListenerHandler.removeAllListeners(element);
|
|
detectionStrategy.uninstall(element);
|
|
stateHandler.cleanState(element);
|
|
});
|
|
}
|
|
|
|
function initDocument(targetDocument) {
|
|
detectionStrategy.initDocument && detectionStrategy.initDocument(targetDocument);
|
|
}
|
|
|
|
return {
|
|
listenTo: listenTo,
|
|
removeListener: eventListenerHandler.removeListener,
|
|
removeAllListeners: eventListenerHandler.removeAllListeners,
|
|
uninstall: uninstall,
|
|
initDocument: initDocument
|
|
};
|
|
};
|
|
|
|
function getOption(options, name, defaultValue) {
|
|
var value = options[name];
|
|
|
|
if((value === undefined || value === null) && defaultValue !== undefined) {
|
|
return defaultValue;
|
|
}
|
|
|
|
return value;
|
|
}
|