/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of "Scrivo" nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * $Id: browser.js 783 2013-08-09 10:17:59Z geert $ */ "use strict"; /** * Holds a set of browser specific functions. * @namespace */ SUI.browser = { /** * Is the code executed by a Mozilla type browser? * @type boolean */ isGecko: false, /** * Is the code executed by an IE type browser? * @type boolean */ isIE: false, /** * Is the code executed by an Opera type browser? * @type boolean */ isOpera: false, /** * Is the code executed by a WebKit type browser? * @type boolean */ isWebKit: false, /** * What's the version of the browser? * TODO not really supported yet. * @type boolean */ version: null, /** * Some older browsers (notably IE 8) don't implement Array.indexOf. This * is the fix for a missing Array.indexOf method as proposed on MDN. */ patchNoArrayIndexOf: function() { if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(searchElement /*, fromIddx */) { "use strict"; if (this === void 0 || this === null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (len === 0) { return -1; } var n = 0; if (arguments.length > 0) { n = Number(arguments[1]); if (n !== n) { n = 0; } else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { n = (n > 0 || -1) * Math.floor(Math.abs(n)); } } if (n >= len) { return -1; } var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); for (; k < len; k++) { if (k in t && t[k] === searchElement) return k; } return -1; }; } }, /** * Fix for String.substr implementations (IE) that do not support a * negative index. */ patchSubstrIE: function() { if (SUI.browser.isIE) { String.prototype.substr = function(off, len) { // make the implicit len parameter explicit if (!len) { len = this.length; } if (off < 0) { // if offset is negative work from other side of string ... off = this.length+off; // ... but correct if (abs) offset was larger than string if (off < 0) { off = 0; } } return this.substring(off, off+len); }; } }, /** * Get the browser type and version. The function has no return values, but * it sets the isIE, isGEcko, isOpera, isWebKit and version fields of the * SUI.browser object. * TODO version not really supported yet */ getBrowser: function() { var ua, s, i; // we'll use the user agent string to determine the browser ua = navigator.userAgent; if ((i = ua.indexOf("Opera")) >= 0) { // Opera first because it as also MSIE in the identification string this.isOpera = true; } else if ((i = ua.indexOf("MSIE")) >= 0) { this.isIE = true; this.version = parseFloat(ua.substr(i + 4)); } else if ((i = ua.indexOf("WebKit")) >= 0) { this.isWebKit = true; } else { // Assume a gecko browser ?? // TODO look into this this.isGecko = true; this.version = 6.1; } }, /** * Add an event listener to an HTML element node. This is basically a * wrapper for HTMLElementNode.addEventListener() because we would like to * use the standard mechamism but can't because of IE. * Also note that we really can't 'add' eventlisteners because of IE, * unless we're adding event listeners to the window object. That's * because in this library we normally store the listener element in an * SUI.Event object. Using attachEvent (what you want to use if you want * to add listeners) will leave you with a this pointer that points to * the window object instead of the element that handles te event. * So except for the window object there is no posibility to add multiple * event listners to a HTML element. IE will only recognize the last one * added when using this function. * @param {HTMLElementNode} el The HTML element node to add the event * listener to. * @param {String} id An event id (like: "click", "mouseover", etc.). * @param {Function} fn Listener function with signature ({DOMEvent}). * Note that in IE the listener will be called with no parameter and * that access to the DOMEvent will be provided by the widow object. */ addEventListener: function(el, id, fn) { if (this.isIE && this.version < 9) { // can't use attachEvent: loses el that has the event // listener (this) if (el == window || el.tagName == "IFRAME") { el.attachEvent("on"+id, fn); } else { // IE has no input event but propertychange works more or less // like it if (id == "input") { id = "propertychange"; } el["on"+id] = fn; } } else { // the standard way, no capturing bubbling only el.addEventListener(id, fn, false); } }, /** * Remove an event listener from an HTML element node that was previously * added with addEventListener. It is basically a wrapper for * HTMLElementNode.removeEventListener() needed for IE compatibility. * @param {HTMLElementNode} el The HTML element node to add the event * listener to. * @param {String} id An event id (like: "click", "mouseover", etc.). * @param {Function} fn A valid reference to the listener function that * earlier was added with addEventListener method. */ removeEventListener: function(el, id, fn) { if (this.isIE && this.version < 9) { // Only use detachEvent on the window object if (el == window) { el.detachEvent("on"+id, fn); } else { // IE has no input event but propertychange works more or less // like it if (id == "input") { id = "propertychange"; } el["on"+id] = null; } } else { // the standard way, no capturing bubbling only el.removeEventListener(id, fn, false); } }, /** * Stop the propagation (bubbling) of the event upwards the DOM tree. * This is basically a wrapper for DOMEvent.stopPropagation needed for * IE compatibility. */ noPropagation: function(e) { // If the event was not passed (what will normally be the case in // IE) use the window.event. Is some circumstances we will use // contentWindow.event (iframe) and then we'll set e explictly. if (!e) { e = window.event; } if (this.isIE && this.version < 9) { // the MS way to stop propagation e.cancelBubble = true; } else if (e){ // the standard way to stop propagation e.stopPropagation(); } }, /** * Counter to create unique id's within the scope of the page. * @type int * @private */ _id: 1, /** * Return a new unique id. Note that the id sequence is initialized when * this module is loaded. * @return {String} An unique id. */ newId: function() { return "scrivo-ui-"+this._id++; }, /** * <p>Create an HTML element with a set of default properties set. In the * SUI library most elements are created by this function to ensure some * standard settings. The following element properties are set:</p> * <ul> * <li>not selectable unless it's a from element,</li> * <li>set a standard font (class),</li> * <li>disable system context menu's,</li> * <li>no margin or padding,</li> * <li>the default cursor,</li> * <li>absolute positioning</li> * <li>generate a unique id</li> * </ul> * @param {String} [tag="DIV"] HTML tag name of the element to create * @param {Object} [attr] Object containing the name and value pairs * for the elements attributes. */ createElement: function(tag, attr) { // create an the element var div = document.createElement(tag ? tag : "DIV"); // set the attributes as given in the arguments (setAttribute sets - // not adds - to the className so do it here) if (attr) { this.setAttributes(div, attr); } // set the font class SUI.style.addClass(div, "sui-font"); // IE uses unselectable and it does not inherit so set the property // on all created elements. if (this.isIE) { div.unselectable = true; } // if we've created a div element ... if (!tag) { // ...set standard style for divs SUI.style.addClass(div, "no-select"); div.style.overflow = "hidden"; } else { // ... else was it an input field? if (tag == "INPUT" || tag == "TEXTAREA") { // ... set styles for input fields ... SUI.style.addClass(div, "select"); // ... and make them selectable in IE if (this.isIE) { div.unselectable = false; } } } // disable the default context menu SUI.browser.addEventListener(div, "contextmenu", function(e) { if (SUI.browser.isIE) { window.event.returnValue = false; } else { e.preventDefault(); } }); // set the other styles div.style.position = "absolute"; div.style.margin = "0px"; div.style.padding = "0px"; div.style.cursor = "default"; // and generate a unique id for the element div.id = this.newId(); return div; }, /** * Get the current/computed style of an HTML element. You can either * retrieve the style object itself or a single property. * Note: You can only use this function if the HTML content of the page * is fully rendered because the currentStyle is asynchronious in IE. * @param {HTMLElementNode} el HTML element node to get the computed * style (property) from. * @param {String} [property] The style property to retrieve. * @return {Object|*} The style object if no specific property was set, * or else the requested style property. */ currentStyle: function(el, property) { if (SUI.browser.isIE) { return property ? el.currentStyle[property] : el.currentStyle; } else { property = property ? property.replace(/([A-Z])/g, "-$1").toLowerCase() : null; var st = el.ownerDocument.defaultView.getComputedStyle(el, null); return property ? st.getPropertyValue(property) : st; } }, /** * Add and overwrite the attributes of some HTML element node with the * attributes of another. * @param {HTMLElementNode} newMode The node to copy the attributes to. * @param {HTMLElementNode} oldMode The node to copy the attributes from. */ mergeAttributes: function(newNode, oldNode) { if (SUI.browser.isIE) { newNode.mergeAttributes(oldNode); } else { for(var i=0; i<oldNode.attributes.length; i++) { var a = oldNode.attributes[i]; if (a.name === "class" || a.name === "className") { SUI.style.addClass(newNode.className, oldNode.className); } else { newNode.setAttribute(a.name, oldNode.getAttribute(a.name)); } } } }, /** * Set/unset a number of attributes using an object. The attributes get * the values from the corresonding properties. A property value of null * will remove the attribute. * @param {HTMLElementNode} e Element node of which the attributes to set. * @param {Object} attr Object containing the new values for the * attributes. */ setAttributes: function(e, attr) { for (var i in attr) { if (attr.hasOwnProperty(i)) { if (i === "class" || i === "className") { SUI.style.setClass(e, attr[i]===null ? "" : attr[i]); } else { if (attr[i] === null) { e.removeAttribute(i); } else { e.setAttribute(i, attr[i]); } } } } }, /** * Remove an element from the document tree. Either remove the node * completely or keep the nodes child nodes in place. * @param {HTMLElementNode} e Element node to remove. * @param {boolean} removeChildren Also remove the children of the node. */ removeNode: function(node, removeChildren) { if (node.removeNode) { // MS method return node.removeNode(removeChildren); } else { if (removeChildren) { // remove the node completely return node.parentNode.removeChild(node); } else { // replace the node with its contents var range = document.createRange(); range.selectNodeContents(node); return node.parentNode.replaceChild( range.extractContents(), node); } } }, /** * The current width of the browser window (viewport). * @type int */ viewportWidth: 0, /** * The current height of the browser window (viewport). * @type int */ viewportHeight: 0, /** * Get the browser window (viewport) size and set the viewportWidth and * viewportHeight members of the SUI.browser object. Note that this * function returns no values. */ getVieportSize: function() { if(SUI.browser.isIE) { // standards compliant mode of IE this.viewportWidth = document.documentElement.clientWidth; this.viewportHeight = document.documentElement.clientHeight; } else { this.viewportWidth = window.innerWidth; this.viewportHeight = window.innerHeight; } }, /** * Get the vertical scroll offset of the viewport. * @return {int} The vertical scroll offset of the viewport. */ viewportScrollX: function() { return SUI.browser.isIE ? document.documentElement.scrollLeft : window.scrollX; }, /** * Get the horizontal scroll offset of the viewport. * @return {int} The horizontal scroll offset of the viewport. */ viewportScrollY: function() { return SUI.browser.isIE ? document.documentElement.scrollTop : window.scrollY; }, /** * Get the horizontal mouse position from a mouse event with respect to * the left of the document. * @param {DOMEvent} e A mouse event. * @return {int} The distance from the left side of the document to the * mouse click in pixels. */ getX: function(e) { if (SUI.browser.isIE && !e) { // If the event was not passed (what will normally be the case in // IE) use the window.event. Is some circumstances we will use // contentWindow.event (iframe) and then we'll set e explictly. e = window.event; } return e.clientX + this.viewportScrollX(); }, /** * Get the vertical mouse position from a mouse event with respect to * the top of the document. * @param {DOMEvent} e A mouse event. * @return {int} The distance from the top side of the document to the * mouse click in pixels. */ getY: function(e) { if (SUI.browser.isIE && !e) { // If the event was not passed (what will normally be the case in // IE) use the window.event. Is some circumstances we will use // contentWindow.event (iframe) and then we'll set e explictly. e = window.event; } return e.clientY + this.viewportScrollY(); } };