Source: util/domUtils.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Utility object for dom-related utilities. Note that this is not a Class, and so does
 * not need to be instantiated before use.
 *
 * @module alfresco/util/domUtils
 * @author Martin Doyle
 * @since 1.0.42
 */
define([
      "alfresco/util/functionUtils",
      "dojo/_base/array",
      "dojo/_base/lang",
      "dojo/dom-style",
      "dojo/dom-class",
      "dojo/on",
      "dojo/sniff"
   ],
   function(funcUtils, array, lang, domStyle, domClass, on, has) {

      // An object for containing resize-monitoring state information
      var resizeMonitor = {
         nodes: {},
         removeObj: null
      };

      // Define the class to decorate a node with when resizes on it should be ignored
      var ignoreResizeClass = "dom-utils--ignore-resize",
         ignoreResizeRegexp = new RegExp("\\b" + ignoreResizeClass + "\\b");

      // The private container for the functionality and properties of the util
      var util = {

         // See API below
         addObjectResizeListener: function alfresco_util_domUtils__addObjectResizeListener(node, resizeHandler) {

            // Setup the listener-removal object
            var listenerRemovalObj = {};

            // Do the rest of the work asynchronously to prevent widget-loading problems
            setTimeout(function() {

               // Setup the resize listener function
               var resizeObj,
                  onLoadListener,
                  removeListenerFunc;

               // NOTE: Dojo IE detection not working very well here, so done manually instead
               var ieRegex = /(Trident\/)|(Edge\/)/,
                  isIE = has("IE") || ieRegex.test(navigator.userAgent);

               // Create the resize object
               resizeObj = document.createElement("object");
               resizeObj.className = "alfresco-core-ResizeMixin__resize-object";
               resizeObj.type = "text/html";
               domStyle.set(resizeObj, {
                  "display": "block",
                  "height": "100%",
                  "left": 0,
                  "overflow": "hidden",
                  "position": "absolute",
                  "top": 0,
                  "width": "100%",
                  "z-index": -1
               });

               // Setup the resize listener functionality
               onLoadListener = on(resizeObj, "load", function() {
                  resizeObj.contentDocument.defaultView.addEventListener("resize", resizeHandler);
                  removeListenerFunc = function() {
                     resizeObj.contentDocument.defaultView.removeEventListener("resize", resizeHandler);
                  };
                  onLoadListener.remove();
               });

               // Node cannot have position static
               var nodeStyle = getComputedStyle(node),
                  nodeIsStatic = nodeStyle.position === "static";
               if (nodeIsStatic) {
                  node.style.position = "relative";
               }

               // FF doesn't like visibility hidden (interferes with the resize event), Chrome doesn't care, IE needs it
               if (isIE) {
                  resizeObj.style.visibility = "hidden";
               }

               // Normal browsers do this before appending to the DOM
               if (!isIE) {
                  resizeObj.data = "about:blank";
               }

               // Add the object to the document
               if (node.hasChildNodes()) {
                  node.insertBefore(resizeObj, node.firstChild);
               } else {
                  node.appendChild(resizeObj);
               }

               // IE needs to do this after
               if (isIE) {
                  resizeObj.data = "about:blank";
               }

               // Update the listener removal object to add the remove function
               listenerRemovalObj.remove = function() {
                  /*jshint devel:true*/
                  try {
                     removeListenerFunc();
                     node.removeChild(resizeObj);
                  } catch (e) {
                     console.warn("Error attempting to remove resize listener", e);
                  }
               };
            }, 0);

            // Pass back the listener-removal object
            return listenerRemovalObj;
         },

         // See API below
         addPollingResizeListener: function alfresco_util_domUtils__addPollingResizeListener(nodeToMonitor, resizeHandler) {

            // Add the new monitoring nodes
            var nodes = resizeMonitor.nodes,
               monitorKey = Date.now();
            while (nodes.hasOwnProperty(monitorKey)) {
               monitorKey = Date.now();
            }
            nodes[monitorKey] = {
               domNode: nodeToMonitor,
               height: nodeToMonitor.offsetHeight,
               width: nodeToMonitor.offsetWidth,
               callbackFunc: resizeHandler
            };

            // If there is no monitoring running, start it up
            if (!resizeMonitor.removeObj) {
               resizeMonitor.removeObj = funcUtils.addRepeatingFunction(this._checkNodesForResize, "SHORT");
            }

            // Pass back the removal object
            return {
               remove: function() {
                  delete nodes[monitorKey]; // Remove this monitored node
                  if (resizeMonitor.removeObj && !Object.keys(nodes).length) {
                     resizeMonitor.removeObj.remove(); // If no nodes are monitored, remove the monitoring function
                     resizeMonitor.removeObj = null;
                  }
               }
            };
         },

         // See API below
         describeNode: function alfresco_util_domUtils__describeNode(node, includeAttrs) {

            // If we have a falsy node, just pass it straight back
            if (!node) {
               return node;
            }

            // Initialise description as for text nodes
            var description = "\"" + node.textContent + "\"";

            // We only deal with text nodes (already done) and element nodes
            if (node.nodeType === 1) {

               // Start with the tag name
               description = node.tagName;

               // Add the ID
               if (node.id) {
                  description += "#" + node.id;
               }

               // Add the classes
               var classes = (node.className && node.className.split(" ")) || [];
               array.forEach(classes, function(clazz) {
                  description += "." + clazz;
               });

               // Optionally add the attributes
               var nextAttr, i;
               if (includeAttrs && node.attributes.length) {
                  for (i = 0; i < node.attributes.length; i++) {
                     nextAttr = node.attributes[i];
                     description += "[" + nextAttr.name + "=\"" + nextAttr.value + "\"]";
                  }
               }

            } else if (node.nodeType !== 3) {

               // Not text or element node!
               console.warn("[alfresco_util_domUtils__describeNode] Unhandle node type (" + node.nodeType + "): ", node);
            }

            // Pass back the constructed description
            return description;
         },

         // See API below
         noticeResizes: function alfresco_util_domUtils__noticeResizes(nodes) {
            this._setResizeIgnored(nodes, false);
         },

         // See API below
         ignoreResizes: function alfresco_util_domUtils__ignoreResizes(nodes) {
            this._setResizeIgnored(nodes, true);
         },

         // Run through the nodes registered to notify on resize, calling the function if necessary
         // NOTE: All of the size-checks are completed before any callback functions are called because
         // if the callbacks cause any redraws then each access of offsetXXX will cause another re-calculation
         // of the current layout (i.e. it could be wildly inefficient if we don't do all the size-checking first)
         _checkNodesForResize: function alfresco_util_domUtils___checkNodesForResize() {
            var resizedNodeKeys = [],
               nodeKeys = Object.keys(resizeMonitor.nodes);
            array.forEach(nodeKeys, function(nodeKey) {
               var node = resizeMonitor.nodes[nodeKey],
                  newWidth = node.domNode.offsetWidth,
                  newHeight = node.domNode.offsetHeight,
                  sizeChanged = node.width !== newWidth || node.height !== newHeight,
                  notificationDisabled = ignoreResizeRegexp.test(node.domNode.className);
               if (sizeChanged) {
                  node.width = newWidth;
                  node.height = newHeight;
                  if (!notificationDisabled) {
                     resizedNodeKeys.push(nodeKey);
                  }
               }
            });
            array.forEach(resizedNodeKeys, function(nodeKey) {
               try {
                  resizeMonitor.nodes[nodeKey].callbackFunc();
               } catch (e) {
                  console.error("Error occurred while executing resize-callback (node will no longer be monitored): ", resizeMonitor.nodes[nodeKey], e);
                  delete resizeMonitor.nodes[nodeKey];
               }
            });
         },

         // See API below
         _setResizeIgnored: function alfresco_util_domUtils___setResizeIgnored(nodeOrNodes, doIgnore) {
            var nodes = nodeOrNodes.constructor === Array ? nodeOrNodes : [nodeOrNodes];
            array.forEach(nodes, function(node) {
               domClass[doIgnore ? "add" : "remove"](node, ignoreResizeClass);
            });
         }
      };

      /**
       * The public API for this utility class
       *
       * @alias module:alfresco/util/domUtils
       */
      return {

         /**
          * <p>In IE9+ and Chrome/FF, setup a listener on a specific node that will call the supplied handler-function whenever that node resizes</p>
          *
          * <p>NOTE: This is based on a technique described at
          * {@link http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection}</p>
          *
          * <p><strong>WARNING:</strong> This is NOT scalable, as it adds approx 500KB to the page's memory usage every time it's used, however
          * it has been left here in case it's more suitable than polling, which might be the case for very limited usage as there's no polling
          * overhead associated with this technique.</p>
          *
          * @instance
          * @function
          * @param {object} node The node to monitor
          * @param {function} resizeHandler The function to call on resize
          * @returns {object} A pointer to the listener, with a remove function on it (i.e. it can be passed to this.own)
          * @since 1.0.43
          */
         addObjectResizeListener: lang.hitch(util, util.addObjectResizeListener),

         /**
          * Setup a listener on a specific node that will call the supplied handler-function whenever that node resizes. This works
          * by having a single "process" polling every 100ms and then checking all registered nodes for size-changes.
          *
          * @instance
          * @function
          * @param {object} node The node to monitor
          * @param {function} resizeHandler The function to call on resize
          * @returns {object} A pointer to the listener, with a remove function on it (i.e. it can be passed to this.own)
          * @since 1.0.43
          */
         addPollingResizeListener: lang.hitch(util, util.addPollingResizeListener),

         /**
          * See the replacement [addPollingResizeListener]{@link module:alfresco/util/domUtils#addPollingResizeListener} for more
          * information.
          *
          * @instance
          * @function
          * @param {object} node The node to monitor
          * @param {function} resizeHandler The function to call on resize
          * @returns {object} A pointer to the listener, with a remove function on it (i.e. it can be passed to this.own)
          * @deprecated Since 1.0.43 - Use [addPollingResizeListener]{@link module:alfresco/util/domUtils#addPollingResizeListener} instead
          */
         addResizeListener: lang.hitch(util, util.addPollingResizeListener),

         /**
          * Given a node in the document, return a string that identifies that node as much as possible.
          *
          * @instance
          * @function
          * @param {object} node The node to describe
          * @param {boolean} [includeAttrs=false] Include the attributes in the description
          * @returns {string} The description of the node, e.g. 'div#myId.class1.class2[tabindex=0]' or "my string"
          * @since 1.0.48
          */
         describeNode: lang.hitch(util, util.describeNode),

         /**
          * Given a domNode, disable resize listening on that node temporarily.
          *
          * @instance
          * @function
          * @param {object|object[]} nodes The node or nodes to disable resize-listening on.
          * @since 1.0.46
          */
         ignoreResizes: lang.hitch(util, util.ignoreResizes),

         /**
          * Given a domNode, re-enable resize listening on that node. Note that this will not add a new resize listener
          * to a node, merely re-enable an existing, disabled one.
          *
          * @instance
          * @function
          * @param {object|object[]} nodes The node or nodes to re-enable resize-listening on.
          * @since 1.0.46
          */
         noticeResizes: lang.hitch(util, util.noticeResizes)
      };
   });