| "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; |
| } |