Source: core/DomElementUtils.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/>.
 */

/**
 * A mixin class that provides utility functions for manipulating objects in the DOM.
 * 
 * @module alfresco/core/DomElementUtils
 * @author Dave Draper
 */
define(["dojo/_base/declare",
   "dojo/dom-style",
   "dojo/_base/lang",
   "dojo/_base/array",
   "dojo/dom-geometry",
   "dojo/json"],
        function(declare, domStyle, lang, array, domGeom, json) {

   var stylePropertiesCache = {};

   /**
    * This class can be mixed into other classes to provide additional DOM element utility functions.
    */
   return declare(null, {
      
      /**
       * This function was taken from the following answer found on StackOverflow: 
       * http://stackoverflow.com/questions/3053542/how-to-get-the-start-and-end-points-of-selection-in-text-area/3053640#3053640
       * 
       * The purpose of this function is to return information on selection of text in an element.
       * 
       * @instance
       * @param {element} el The element to find the text selection for.
       * @returns {Object} An object with the attributes "start" and "end" that indicate the text selection.
       */
      getInputSelection: function(el) {
         var start = 0, end = 0, normalizedValue, range,
         textInputRange, len, endRange;

         if (typeof el.selectionStart === "number" && typeof el.selectionEnd === "number") 
         {
            start = el.selectionStart;
            end = el.selectionEnd;
         }
         else 
         {
            range = document.selection.createRange();
   
            if (range && range.parentElement() === el) 
            {
               len = el.value.length;
               normalizedValue = el.value.replace(/\r\n/g, "\n");
   
               // Create a working TextRange that lives only in the input
               textInputRange = el.createTextRange();
               textInputRange.moveToBookmark(range.getBookmark());
   
               // Check if the start and end of the selection are at the very end
               // of the input, since moveStart/moveEnd doesn't return what we want
               // in those cases
               endRange = el.createTextRange();
               endRange.collapse(false);
   
               if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) 
               {
                  start = end = len;
               } 
               else 
               {
                  start = -textInputRange.moveStart("character", -len);
                  start += normalizedValue.slice(0, start).split("\n").length - 1;
   
                  if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) 
                  {
                     end = len;
                  } 
                  else 
                  {
                     end = -textInputRange.moveEnd("character", -len);
                     end += normalizedValue.slice(0, end).split("\n").length - 1;
                  }
               }
            }
        }
   
        return {
            start: start,
            end: end
        };
      },
      
      /**
       * This function is based on the answer given to the following StackOverflow question:
       * http://stackoverflow.com/questions/1336585/howto-place-cursor-at-beginning-of-textarea
       * 
       * The purpose is to set the carat in the supplied element.
       * 
       * @instance
       * @param {element} el The element to set the carat position on
       * @param {number} position The index at which to set the carat
       */
      setCaretPosition: function(el, position) {
         if (el.setSelectionRange) 
         { 
             el.focus(); 
             el.setSelectionRange(position, position); 
         }
         else if (el.createTextRange) 
         { 
             var range = el.createTextRange();
             range.moveStart("character", position); 
             range.select(); 
         }
      },

      /**
       * Returns the height of the document.
       * based on jQuery function (http://code.jquery.com/jquery-2.1.0.js), MIT licensed.
       *
       * @instance
       * @returns {number}
       */
      getDocumentHeight: function() {
         var doc = document.documentElement,
               body = document.body;

         return Math.max(
               body.scrollHeight,
               body.offsetHeight,
               doc.scrollHeight,
               doc.offsetHeight,
               doc.clientHeight
         );
      },

      /**
       * Returns the result of a calculation applied to an initial value.
       *
       * Use i.e. by [IFrame]{@link module:alfresco/integration/IFrame} to be able to set a dynamic height of the
       * iframe where the value of heightConfig would get passed into this method:
       *
       * "heightConfig": {
       *    "initialValue": 0,
       *    "domCalculation": [
       *      { "operator": "+", "selector": "body",                         "function": "marginBox", "property": "h" },
       *      { "operator": "-", "selector": ".footerContent",               "function": "marginBox", "property": "h" },
       *      { "operator": "-", "selector": ".alfresco-integration-IFrame", "function": "marginBox", "property": "t" }
       *   ]
       * }
       *
       * @instance
       * @param config {object}
       * @param config.initialValue {object}
       * @param config.domCalculation {array}
       * @returns {*}
       */
      resolveDomCalculation: function(config) {
         var result = config.initialValue;
         if (config.domCalculation) {
            var c, tmp;
            for (var i = 0, il = config.domCalculation.length; i < il; i++) {
               c = config.domCalculation[i];

               // Find element
               tmp = document.querySelector(c.selector);

               // Function
               if (this.domCalculationFunctions[c["function"]]) {
                  tmp = this.domCalculationFunctions[c["function"]].call(this, tmp);
               }
               else
               {
                  throw new Error("Failed calculation due to missing function " + c["function"]);
               }

               // Property
               if (c.property) {
                  tmp = tmp[c.property];
               }

               // Operator
               if (this.domCalculationOperators[c.operator]) {
                  result = this.domCalculationOperators[c.operator].call(this, result, tmp);
               }
               else {
                  throw new Error("Failed calculation due to missing operator " + c.operator);
               }
            }
         }
         return result;
      },

      /**
       * The functions that can be used form a domCalculation.
       *
       * @instance
       */
      domCalculationFunctions: {
         "marginBox": function(tmp) {
            return domGeom.getMarginBox(tmp);
         }
      },

      /**
       * The operators that can be used form a domCalculation.
       *
       * @instance
       */
      domCalculationOperators: {
         "+": function(result, value) {
            return result + value;
         },
         "-": function(result, value) {
            return result - value;
         }
      },

      /**
       * Certain widgets (i.e. 3rd party svg chart elements) weants to get the colors to use specified as a javascript
       * attribute rather than using css. This helper method makes it possible to theme such colors using css by
       * creating elements and adding certain class names to the elements and then inspect the computed style
       * properties.
       *
       * I.e.
       *
       * var styles = this.resolveCssStyles("alfresco-charts-ccc-Chart--color", [1,2,3,4,5,6,7,8], {
       *   backgroundColor: ["rgba(0, 0, 0, 0)", "transparent"]
       * });
       * config.colors = styles.backgroundColor;
       *
       * Will collect all the background colors for the css class "alfresco-charts-ccc-Chart--color1",
       * "alfresco-charts-ccc-Chart--color2" and so on but exclude values matching "rgba(0, 0, 0, 0)" or "transparent".
       *
       * @instance
       * @param prefix {string} The common "base" name to prefix before each of the name in names, which will create
       *    the css selector to look for style properties in
       * @param names {array} The names that will get added to the prefix when creating the css selector to look for
       *    style properties in
       * @param styleProperties {object} An object holding the name of the style properties to look for as keys and
       *    specific values to ignore (if any) as the value
       * @param cache {boolean} Set to false if cache shouldn't be used
       * @return {Object} An object with a "byName" property that contains all values for each name but also a property
       *    for each the keys specified in styleProperties with all values for that key (except the ones matching the
       *    values to ignore).
       */
      resolveCssStyles: function(prefix, names, styleProperties, cache){
         /*jshint maxcomplexity:false,maxstatements:false*/
         cache = typeof cache === "boolean" ? cache : true;

         var result;
         if (cache) {
            var cacheKey = json.stringify([prefix, names, styleProperties]);
            result = stylePropertiesCache[cacheKey];
            if (result) {
               return result;
            }
         }

         result = {
            byName: []
         };
         var el = document.createElement("div");
         document.body.appendChild(el);
         var computedStyle;
         var styleProperty;
         var stylePropertiesPerName;
         for (var i = 0, il = names.length; i < il; i++) {
            el.className = (prefix || "") + names[i];
            computedStyle = domStyle.getComputedStyle(el);
            stylePropertiesPerName = {};
            for (var s in styleProperties) {
               if(styleProperties.hasOwnProperty(s)) {
                  styleProperty = computedStyle[s];

                  // Make sure there is an array to store result in
                  if (!result[s]) {
                     result[s] = [];
                  }

                  // Make sure we are not adding a valu that shall be ignored
                  if (lang.isArray(styleProperties[s])) {
                     if (array.indexOf(styleProperties[s], styleProperty) === -1) {
                        result[s].push(styleProperty);
                     }
                  }
                  else if (styleProperty !== styleProperties) {
                     result[s].push(styleProperty);
                  }

                  // Store style property value on a per name basis as well
                  stylePropertiesPerName[s] = styleProperty;
               }
            }
            result.byName.push({
               name: names[i],
               style: stylePropertiesPerName
            });
         }

         if (cache) {
            stylePropertiesCache[cacheKey] = result;
         }

         return result;
      }
   });
});