UNPKG

betajs-browser

Version:

BetaJS-Browser is a client-side JavaScript framework for Browser-specific methods.

727 lines (651 loc) 28.1 kB
Scoped.define("module:Dom", [ "base:Types", "base:Objs", "module:Info", "base:Async" ], function(Types, Objs, Info, Async) { var TEMPLATE_TAG_MAP = { "tr": ["table", "tbody"], "td": ["table", "tbody", "tr"], "th": ["table", "thead", "tr"] }; var INTERACTION_EVENTS = ["click", "mousedown", "mouseup", "touchstart", "touchend", "keydown", "keyup", "keypress"]; var userInteractionCallbackList = []; var userInteractionCallbackWaiting = false; var userInteractionCallbackFunc = function() { userInteractionCallbackList.forEach(function(entry) { entry.callback.call(entry.context || this); }); userInteractionCallbackList = []; userInteractionCallbackWaiting = false; INTERACTION_EVENTS.forEach(function(event) { window.removeEventListener(event, userInteractionCallbackFunc); }); }; var Dom = { ready: function(callback, context) { if (document.readyState === "complete" || (document.readyState !== "loading" && !document.documentElement.doScroll)) { Async.eventually(callback, context); } else { var completed; var done = false; var timer = null; completed = function() { clearInterval(timer); document.removeEventListener("DOMContentLoaded", completed); window.removeEventListener("load", completed); if (done) return; done = true; callback.apply(context || this); }; document.addEventListener("DOMContentLoaded", completed); window.addEventListener("load", completed); timer = setInterval(function() { if (document.readyState === "complete" || (document.readyState !== "loading" && !document.documentElement.doScroll)) completed(); }, 10); } }, userInteraction: function(callback, context) { userInteractionCallbackList.push({ callback: callback, context: context }); if (!userInteractionCallbackWaiting) { userInteractionCallbackWaiting = true; INTERACTION_EVENTS.forEach(function(event) { window.addEventListener(event, userInteractionCallbackFunc); }); } }, isTabHidden: function() { return document.hidden || document.webkitHidden || document.mozHidden || document.msHidden; }, elementsByTemplate: function(template) { template = template.trim(); var polyfill = Info.isInternetExplorer() && Info.internetExplorerVersion() < 9; /* * TODO: This is probably not a good fix. * * Some tags, like tr, are not generated by the browser when under a generic tag like div. * In other words * * <div>.innerHTML = "<tr><p>foo</p></tr>" will become <div><p>foo</p></div> * * The quick fix here checks for an outer tag and picks the proper temporary parent tag. * * This needs to be fixed properly in the future. */ var parentTags = []; Objs.iter(TEMPLATE_TAG_MAP, function(value, key) { if (template.indexOf("<" + key) === 0) { parentTags = value; polyfill = false; } }); var outerTemplate = [ parentTags.map(function(t) { return "<" + t + ">"; }).join(""), polyfill ? "<br/>" : "", template, parentTags.map(function(t) { return "</" + t + ">"; }).join("") ].join(""); var element = document.createElement("div"); element.innerHTML = outerTemplate; parentTags.forEach(function() { element = element.children[0]; }); var result = []; for (var i = polyfill ? 1 : 0; i < element.children.length; ++i) result.push(element.children[i]); if (polyfill) { result = result.filter(function(el) { return !(el.tagName.indexOf("/") === 0 && template.toLowerCase().indexOf("<" + el.tagName.toLowerCase()) >= 0); }); } return result; }, elementByTemplate: function(template, encapsulate_in_div_if_needed) { var result = this.elementsByTemplate(template); if (result.length === 1) return result[0]; if (result.length === 0 || !encapsulate_in_div_if_needed) return null; var element = document.createElement("div"); result.forEach(element.appendChild, element); return element; }, changeTag: function(node, name) { var replacement = document.createElement(name); for (var i = 0; i < node.attributes.length; ++i) { var attr = node.attributes[i]; replacement.setAttribute(attr.nodeName, "value" in attr ? attr.value : attr.nodeValue); } while (node.firstChild) replacement.appendChild(node.firstChild); if (node.parentNode) node.parentNode.replaceChild(replacement, node); return replacement; }, traverseNext: function(node, skip_children) { node = this.unbox(node); if (node.firstChild && !skip_children) return node.firstChild; if (!node.parentNode) return null; if (node.nextSibling) return node.nextSibling; return this.traverseNext(node.parentNode, true); }, splitNode: function(node, start_offset, end_offset) { start_offset = start_offset || 0; end_offset = end_offset || (node.wholeText ? node.wholeText.length : 0); if (end_offset < (node.wholeText ? node.wholeText.length : 0)) node.splitText(end_offset); if (start_offset > 0) node = node.splitText(start_offset); return node; }, __FULLSCREEN_EVENTS: ["fullscreenchange", "webkitfullscreenchange", "mozfullscreenchange", "MSFullscreenChange"], __FULLSCREEN_METHODS: ["requestFullscreen", "webkitRequestFullscreen", "mozRequestFullScreen", "msRequestFullscreen", "webkitEnterFullScreen"], __FULLSCREEN_ATTRS: ["fullscreenElement", "webkitFullscreenElement", "mozFullScreenElement", "msFullscreenElement"], __FULLSCREEN_EXIT_METHODS: ["exitFullscreen", "mozCancelFullScreen", "webkitExitFullscreen", "msExitFullscreen"], elementSupportsFullscreen: function(element) { return element && this.__FULLSCREEN_METHODS.some(function(key) { return key in element; }); }, elementEnterFullscreen: function(element) { var done = false; this.__FULLSCREEN_METHODS.forEach(function(key) { if (!done && (key in element)) { element[key].call(element); done = true; } }); }, // Will exit from document's full screen mode documentExitFullscreen: function() { this.__FULLSCREEN_EXIT_METHODS.forEach(function(key) { if (document[key]) { document[key](); } }); }, elementIsFullscreen: function(element) { return this.__FULLSCREEN_ATTRS.some(function(key) { return document[key] === element; }); }, elementOnFullscreenChange: function(element, callback, context) { var self = this; var listener = function() { callback.call(context || this, element, self.elementIsFullscreen(element)); }; this.__FULLSCREEN_EVENTS.forEach(function(event) { element.addEventListener(event, listener, false); }); return listener; }, elementOffFullscreenChange: function(element, listener) { this.__FULLSCREEN_EVENTS.forEach(function(event) { element.removeEventListener(event, listener, false); }); }, __PIP_EVENTS: ["enterpictureinpicture", "leavepictureinpicture"], /** * If browser supports picture-in-picture * * INFO: to enable PIP in FF: about:config, set media.videocontrols.picture-in-picture.enabled, * media.videocontrols.picture-in-picture.video-toggle.enabled;true * and media.videocontrols.picture-in-picture.video-toggle.flyout-enabled * * @param {HTMLVideoElement =} videoElement * @returns {boolean} */ browserSupportsPIP: function(videoElement) { videoElement = videoElement || HTMLVideoElement.prototype || {}; if ('pictureInPictureEnabled' in document) return true; if (Info.isMacOS() && Info.safariVersion() >= 9) return !!(videoElement.webkitSupportsPresentationMode && typeof videoElement.webkitSetPresentationMode === "function"); return false; }, /** * * @param {HTMLVideoElement} videoElement */ videoElementEnterPIPMode: function(videoElement) { if (!videoElement || !(videoElement instanceof HTMLVideoElement)) return; videoElement = videoElement || HTMLVideoElement.prototype || {}; if ('pictureInPictureElement' in document) { if (!document.pictureInPictureElement && typeof videoElement.requestPictureInPicture === 'function') { try { videoElement.requestPictureInPicture(); } catch (err) { console.warn(err); } } } if (Info.isMacOS() && Info.safariVersion() >= 9 && typeof videoElement.webkitSetPresentationMode === 'function') videoElement.webkitSetPresentationMode("picture-in-picture"); }, /** * Video Will Exit From PIP Mode * @param {HTMLVideoElement} videoElement */ videoElementExitPIPMode: function(videoElement) { videoElement = videoElement || HTMLVideoElement.prototype || {}; if ('pictureInPictureElement' in document) { if (document.pictureInPictureElement && typeof document.exitPictureInPicture === 'function') { try { document.exitPictureInPicture(); } catch (err) { console.warn(err); } } } if (Info.isMacOS() && Info.safariVersion() >= 9 && typeof videoElement.webkitSetPresentationMode === 'function') { videoElement.webkitSetPresentationMode('inline'); } }, /** * Will check if Video Element in PIP Mode * @param {HTMLVideoElement} videoElement * @returns {boolean} */ videoIsInPIPMode: function(videoElement) { if ('pictureInPictureElement' in document) return !!document.pictureInPictureElement; if (Info.isMacOS() && Info.safariVersion() >= 9) return (videoElement.webkitPresentationMode === "picture-in-picture"); }, /** * * @param {HTMLVideoElement} videoElement * @param {function} callback * @param {object =} context * @returns {listener | null} */ videoAddPIPChangeListeners: function(videoElement, callback, context) { if (!videoElement || !(videoElement instanceof HTMLVideoElement)) return null; var self = this; var listener = function() { callback.call(context || this, videoElement, self.videoIsInPIPMode(videoElement)); }; this.__PIP_EVENTS.forEach(function(event) { videoElement.addEventListener(event, listener, false); }); return listener; }, /** * * @param {HTMLVideoElement} videoElement * @param {function} callback * @param {object =} context * @returns {listener} */ videoRemovePIPChangeListeners: function(videoElement) { if (!videoElement || !(videoElement instanceof HTMLVideoElement)) return null; this.__PIP_EVENTS.forEach(function(event) { videoElement.removeEventListener(event, videoElement); }); }, entitiesToUnicode: function(s) { if (!s || !Types.is_string(s) || s.indexOf("&") < 0) return s; return s.split(">").map(function(s) { return s.split("<").map(function(s) { var temp = document.createElement("span"); temp.innerHTML = s; s = temp.textContent || temp.innerText; if (temp.remove) temp.remove(); return s; }).join("<"); }).join(">"); }, unbox: function(element) { if (Types.is_string(element)) element = document.querySelector(element); return !element || element.nodeType ? element : element.get(0); }, triggerDomEvent: function(element, eventName, parameters, customEventParams) { element = this.unbox(element); eventName = eventName.toLowerCase(); var onEvent = "on" + eventName; var onEventHandler = null; var onEventCalled = false; if (element[onEvent]) { onEventHandler = element[onEvent]; element[onEvent] = function() { if (onEventCalled) return; onEventCalled = true; onEventHandler.apply(this, arguments); }; } try { var event; try { if (customEventParams) event = new CustomEvent(eventName, customEventParams); else event = new Event(eventName); } catch (e) { try { if (customEventParams) { event = document.createEvent('CustomEvent'); event.initCustomEvent(eventName, customEventParams.bubbles || false, customEventParams.cancelable || false, customEventParams.detail || false); } else { event = document.createEvent('Event'); event.initEvent(eventName, false, false); } } catch (e) { event = document.createEventObject(); event.type = eventName; } } Objs.extend(event, parameters); element.dispatchEvent(event); if (onEventHandler) { if (!onEventCalled) onEventHandler.call(element, event); element[onEvent] = onEventHandler; } } catch (e) { if (onEventHandler) element[onEvent] = onEventHandler; throw e; } }, elementOffset: function(element) { element = this.unbox(element); var top = 0; var left = 0; if (element.getBoundingClientRect) { var box = element.getBoundingClientRect(); top = box.top; left = box.left - (document.body.getBoundingClientRect ? document.body.getBoundingClientRect().left : 0); } docElem = document.documentElement; return { top: top + (window.pageYOffset || docElem.scrollTop) - (docElem.clientTop || 0), left: left + (window.pageXOffset || docElem.scrollLeft) - (docElem.clientLeft || 0) }; }, getRelativeCoordinates: function(event, element) { var position = { x: event.pageX, y: event.pageY }; var offset = { left: element.offsetLeft, top: element.offsetTop }; var reference = element.offsetParent; while (reference != null) { offset.left += reference.offsetLeft; offset.top += reference.offsetTop; reference = reference.offsetParent; } return { x: position.x - offset.left, y: position.y - offset.top }; }, elementDimensions: function(element) { element = this.unbox(element); var cs, w, h; if (element && window.getComputedStyle) { try { cs = window.getComputedStyle(element); } catch (e) {} if (cs) { w = parseInt(cs.width, 10); h = parseInt(cs.height, 10); if (w && h) { return { width: w, height: h }; } } } if (element && element.currentStyle) { cs = element.currentStyle; w = element.clientWidth - parseInt(cs.paddingLeft || 0, 10) - parseInt(cs.paddingRight || 0, 10); h = element.clientHeight - parseInt(cs.paddingTop || 0, 10) - parseInt(cs.paddingTop || 0, 10); if (w && h) { return { width: w, height: h }; } } if (element && element.getBoundingClientRect) { var box = element.getBoundingClientRect(); h = box.bottom - box.top; w = box.right - box.left; return { width: w, height: h }; } return { width: 0, height: 0 }; }, childContainingElement: function(parent, element) { parent = this.unbox(parent); element = this.unbox(element); while (element.parentNode != parent) { if (element == document.body) return null; element = element.parentNode; } return element; }, elementBoundingBox: function(element) { var offset = this.elementOffset(element); var dimensions = this.elementDimensions(element); return { left: offset.left, top: offset.top, right: offset.left + dimensions.width - 1, bottom: offset.top + dimensions.height - 1 }; }, pointWithinElement: function(x, y, element) { var bb = this.elementBoundingBox(element); return bb.left <= x && x <= bb.right && bb.top <= y && y <= bb.bottom; }, elementFromPoint: function(x, y, disregarding, parent) { disregarding = disregarding || []; if (!Types.is_array(disregarding)) disregarding = [disregarding]; var backup = []; for (var i = 0; i < disregarding.length; ++i) { disregarding[i] = this.unbox(disregarding[i]); backup.push(disregarding[i].style.zIndex); disregarding[i].style.zIndex = -1; } var element = document.elementFromPoint(x - window.pageXOffset, y - window.pageYOffset); for (i = 0; i < disregarding.length; ++i) disregarding[i].style.zIndex = backup[i]; while (element && parent && element.parentNode !== parent) element = element.parentNode; return element; }, elementAddClass: function(element, cls) { if (!element.className) element.className = cls; else if (!this.elementHasClass(element, cls)) element.className = element.className + " " + cls; }, elementHasClass: function(element, cls) { return element.className.split(" ").some(function(name) { return name === cls; }); }, elementRemoveClass: function(element, cls) { element.className = element.className.split(" ").filter(function(name) { return name !== cls; }).join(" "); }, elementInsertBefore: function(element, before) { before.parentNode.insertBefore(element, before); }, elementInsertAfter: function(element, after) { if (after.nextSibling) after.parentNode.insertBefore(element, after.nextSibling); else after.parentNode.appendChild(element); }, elementInsertAt: function(element, parent, index) { if (index >= parent.children.length) parent.appendChild(element); else parent.insertBefore(element, parent.children[Math.max(0, index)]); }, elementIndex: function(element) { var idx = 0; while (element.previousElementSibling) { idx++; element = element.previousElementSibling; } return idx; }, elementPrependChild: function(parent, child) { if (parent.children.length > 0) parent.insertBefore(child, parent.firstChild); else parent.appendChild(child); }, // Will find closest parent element, will stop on stopSelector // example: Dom.elementReplaceClasses(element, '.look-element-class-name', '.stop-on-class-name') elementFindClosestParent: function(element, selector, stopSelector) { var _returnVal = null; while (element) { if (element.className.indexOf(selector) > -1) { _returnVal = element; break; } else if (stopSelector && element.className.indexOf(stopSelector) > -1) { break; } element = element.parentElement; } return _returnVal; }, // Will replace class names on element elementReplaceClasses: function(element, replaceClass, replaceWith) { if (this.elementHasClass(element, replaceClass)) { this.elementRemoveClass(element, replaceClass); this.elementAddClass(element, replaceWith); } }, // When element in visible port view, will return true isElementVisible: function(element, fraction) { fraction = fraction || 0.8; var offset = this.elementOffset(element); var x = offset.left; var y = offset.top; var w = element.offsetWidth; var h = element.offsetHeight; var right = x + w; var bottom = y + h; var visibleX = Math.max(0, Math.min(w, window.pageXOffset + window.innerWidth - x, right - window.pageXOffset)); var visibleY = Math.max(0, Math.min(h, window.pageYOffset + window.innerHeight - y, bottom - window.pageYOffset)); var visible = visibleX * visibleY / (w * h); return (visible > fraction); }, keyboardUnfocus: function() { if (document.activeElement) document.activeElement.blur(); }, passiveEventsSupported: function() { return Info.isiOS(); }, containerStickyBottom: function(someElement) { var lastScrollHeight = someElement.scrollHeight; var critical = false; var observer = new MutationObserver(function() { if (critical) return; critical = true; var newScrollHeight = someElement.scrollHeight; var oldScrollHeight = lastScrollHeight; lastScrollHeight = newScrollHeight; if (newScrollHeight > oldScrollHeight) someElement.scrollTop = someElement.scrollTop + newScrollHeight - oldScrollHeight; critical = false; }); observer.observe(someElement, { childList: true, subtree: true, attributes: true, characterData: true }); return observer; }, isInputLikeElement: function(element) { return element.nodeName === "TEXTAREA" || element.nodeName === "INPUT"; }, copyStringToClipboard: function(s) { var input = document.createElement("input"); input.style.display = 'none'; document.body.appendChild(input); input.value = s; this.copyInputValueToClipboard(input); document.body.removeChild(input); }, copyInputValueToClipboard: function(element) { if (document.body.createTextRange) { var textRange = document.body.createTextRange(); textRange.moveToElementText(element); textRange.select(); return textRange.execCommand("Copy"); } else if (window.getSelection && document.createRange) { var oldContentEditable = element.contentEditable; var oldReadOnly = element.readOnly; element.contentEditable = true; element.readOnly = false; var range = document.createRange(); range.selectNodeContents(element); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); // Does not work for Firefox if a textarea or input if (this.isInputLikeElement(element)) element.select(); // Firefox will only select a form element with select() if (Info.isiOS()) element.setSelectionRange(0, 999999); element.contentEditable = oldContentEditable; element.readOnly = oldReadOnly; return document.execCommand("copy"); } return false; }, onScrollIntoView: function(element, visibilityFraction, callback, context) { if (this.isElementVisible(element, visibilityFraction)) { callback.call(context); return; } var cb = function() { if (Dom.isElementVisible(element, visibilityFraction)) { callback.call(context); document.removeEventListener("scroll", cb); } }; document.addEventListener("scroll", cb); } }; return Dom; });