src/dom/dom.js
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;
});