2012-06-20 21:16:41 +00:00
|
|
|
/*\
|
2012-07-14 14:57:36 +00:00
|
|
|
title: $:/core/modules/utils/dom.js
|
2012-06-20 21:16:41 +00:00
|
|
|
type: application/javascript
|
|
|
|
module-type: utils
|
|
|
|
|
|
|
|
Various static DOM-related utility functions.
|
|
|
|
|
|
|
|
\*/
|
|
|
|
(function(){
|
|
|
|
|
|
|
|
/*jslint node: true, browser: true */
|
|
|
|
/*global $tw: false */
|
|
|
|
"use strict";
|
|
|
|
|
2022-12-01 21:16:44 +00:00
|
|
|
var Popup = require("$:/core/modules/utils/dom/popup.js");
|
|
|
|
|
2012-06-20 21:16:41 +00:00
|
|
|
/*
|
|
|
|
Determines whether element 'a' contains element 'b'
|
|
|
|
Code thanks to John Resig, http://ejohn.org/blog/comparing-document-position/
|
|
|
|
*/
|
|
|
|
exports.domContains = function(a,b) {
|
|
|
|
return a.contains ?
|
2014-01-03 10:54:00 +00:00
|
|
|
a !== b && a.contains(b) :
|
2012-06-20 21:16:41 +00:00
|
|
|
!!(a.compareDocumentPosition(b) & 16);
|
|
|
|
};
|
|
|
|
|
2021-05-21 08:43:20 +00:00
|
|
|
exports.domMatchesSelector = function(node,selector) {
|
|
|
|
return node.matches ? node.matches(selector) : node.msMatchesSelector(selector);
|
|
|
|
};
|
|
|
|
|
2023-02-25 18:25:46 +00:00
|
|
|
/*
|
|
|
|
Select text in a an input or textarea (setSelectionRange crashes on certain input types)
|
|
|
|
*/
|
|
|
|
exports.setSelectionRangeSafe = function(node,start,end,direction) {
|
|
|
|
try {
|
|
|
|
node.setSelectionRange(start,end,direction);
|
|
|
|
} catch(e) {
|
|
|
|
node.select();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Select the text in an input or textarea by position
|
|
|
|
*/
|
|
|
|
exports.setSelectionByPosition = function(node,selectFromStart,selectFromEnd) {
|
|
|
|
$tw.utils.setSelectionRangeSafe(node,selectFromStart,node.value.length - selectFromEnd);
|
|
|
|
};
|
|
|
|
|
2012-12-13 21:31:41 +00:00
|
|
|
exports.removeChildren = function(node) {
|
|
|
|
while(node.hasChildNodes()) {
|
|
|
|
node.removeChild(node.firstChild);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-06-20 21:16:41 +00:00
|
|
|
exports.hasClass = function(el,className) {
|
2020-04-22 14:32:20 +00:00
|
|
|
return el && el.hasAttribute && el.hasAttribute("class") && el.getAttribute("class").split(" ").indexOf(className) !== -1;
|
2012-06-20 21:16:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.addClass = function(el,className) {
|
2020-04-22 14:32:20 +00:00
|
|
|
var c = (el.getAttribute("class") || "").split(" ");
|
2012-06-20 21:16:41 +00:00
|
|
|
if(c.indexOf(className) === -1) {
|
|
|
|
c.push(className);
|
2020-04-22 14:32:20 +00:00
|
|
|
el.setAttribute("class",c.join(" "));
|
2012-06-20 21:16:41 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.removeClass = function(el,className) {
|
2020-04-22 14:32:20 +00:00
|
|
|
var c = (el.getAttribute("class") || "").split(" "),
|
2012-06-20 21:16:41 +00:00
|
|
|
p = c.indexOf(className);
|
|
|
|
if(p !== -1) {
|
|
|
|
c.splice(p,1);
|
2020-04-22 14:32:20 +00:00
|
|
|
el.setAttribute("class",c.join(" "));
|
2012-06-20 21:16:41 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.toggleClass = function(el,className,status) {
|
|
|
|
if(status === undefined) {
|
|
|
|
status = !exports.hasClass(el,className);
|
|
|
|
}
|
|
|
|
if(status) {
|
|
|
|
exports.addClass(el,className);
|
|
|
|
} else {
|
|
|
|
exports.removeClass(el,className);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-22 07:36:29 +00:00
|
|
|
/*
|
|
|
|
Get the first parent element that has scrollbars or use the body as fallback.
|
|
|
|
*/
|
|
|
|
exports.getScrollContainer = function(el) {
|
|
|
|
var doc = el.ownerDocument;
|
2021-05-30 18:20:17 +00:00
|
|
|
while(el.parentNode) {
|
2016-04-22 07:36:29 +00:00
|
|
|
el = el.parentNode;
|
|
|
|
if(el.scrollTop) {
|
|
|
|
return el;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return doc.body;
|
|
|
|
};
|
|
|
|
|
2012-06-20 21:20:48 +00:00
|
|
|
/*
|
2012-07-07 16:14:50 +00:00
|
|
|
Get the scroll position of the viewport
|
|
|
|
Returns:
|
|
|
|
{
|
|
|
|
x: horizontal scroll position in pixels,
|
|
|
|
y: vertical scroll position in pixels
|
|
|
|
}
|
2012-06-20 21:20:48 +00:00
|
|
|
*/
|
2018-11-13 18:07:55 +00:00
|
|
|
exports.getScrollPosition = function(srcWindow) {
|
|
|
|
var scrollWindow = srcWindow || window;
|
|
|
|
if("scrollX" in scrollWindow) {
|
|
|
|
return {x: scrollWindow.scrollX, y: scrollWindow.scrollY};
|
2012-07-07 16:14:50 +00:00
|
|
|
} else {
|
2018-11-13 18:07:55 +00:00
|
|
|
return {x: scrollWindow.document.documentElement.scrollLeft, y: scrollWindow.document.documentElement.scrollTop};
|
2012-06-20 21:20:48 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-22 07:36:29 +00:00
|
|
|
/*
|
|
|
|
Adjust the height of a textarea to fit its content, preserving scroll position, and return the height
|
|
|
|
*/
|
|
|
|
exports.resizeTextAreaToFit = function(domNode,minHeight) {
|
|
|
|
// Get the scroll container and register the current scroll position
|
|
|
|
var container = $tw.utils.getScrollContainer(domNode),
|
|
|
|
scrollTop = container.scrollTop;
|
|
|
|
// Measure the specified minimum height
|
|
|
|
domNode.style.height = minHeight;
|
2018-01-23 16:22:35 +00:00
|
|
|
var measuredHeight = domNode.offsetHeight || parseInt(minHeight,10);
|
2016-04-22 07:36:29 +00:00
|
|
|
// Set its height to auto so that it snaps to the correct height
|
|
|
|
domNode.style.height = "auto";
|
|
|
|
// Calculate the revised height
|
|
|
|
var newHeight = Math.max(domNode.scrollHeight + domNode.offsetHeight - domNode.clientHeight,measuredHeight);
|
|
|
|
// Only try to change the height if it has changed
|
|
|
|
if(newHeight !== domNode.offsetHeight) {
|
|
|
|
domNode.style.height = newHeight + "px";
|
|
|
|
// Make sure that the dimensions of the textarea are recalculated
|
|
|
|
$tw.utils.forceLayout(domNode);
|
|
|
|
// Set the container to the position we registered at the beginning
|
|
|
|
container.scrollTop = scrollTop;
|
|
|
|
}
|
|
|
|
return newHeight;
|
|
|
|
};
|
|
|
|
|
2012-10-26 21:12:40 +00:00
|
|
|
/*
|
|
|
|
Gets the bounding rectangle of an element in absolute page coordinates
|
|
|
|
*/
|
|
|
|
exports.getBoundingPageRect = function(element) {
|
2018-11-13 18:07:55 +00:00
|
|
|
var scrollPos = $tw.utils.getScrollPosition(element.ownerDocument.defaultView),
|
2012-10-26 21:12:40 +00:00
|
|
|
clientRect = element.getBoundingClientRect();
|
|
|
|
return {
|
|
|
|
left: clientRect.left + scrollPos.x,
|
|
|
|
width: clientRect.width,
|
|
|
|
right: clientRect.right + scrollPos.x,
|
|
|
|
top: clientRect.top + scrollPos.y,
|
|
|
|
height: clientRect.height,
|
|
|
|
bottom: clientRect.bottom + scrollPos.y
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2012-07-13 12:02:34 +00:00
|
|
|
/*
|
|
|
|
Saves a named password in the browser
|
|
|
|
*/
|
|
|
|
exports.savePassword = function(name,password) {
|
2019-06-08 15:38:13 +00:00
|
|
|
var done = false;
|
2014-07-06 09:11:21 +00:00
|
|
|
try {
|
2019-06-08 15:38:13 +00:00
|
|
|
window.localStorage.setItem("tw5-password-" + name,password);
|
|
|
|
done = true;
|
2014-07-06 09:11:21 +00:00
|
|
|
} catch(e) {
|
2013-10-29 22:21:25 +00:00
|
|
|
}
|
2019-06-08 15:38:13 +00:00
|
|
|
if(!done) {
|
|
|
|
$tw.savedPasswords = $tw.savedPasswords || Object.create(null);
|
|
|
|
$tw.savedPasswords[name] = password;
|
|
|
|
}
|
2012-07-13 12:02:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Retrieve a named password from the browser
|
|
|
|
*/
|
|
|
|
exports.getPassword = function(name) {
|
2019-06-08 15:38:13 +00:00
|
|
|
var value;
|
2014-07-06 09:11:21 +00:00
|
|
|
try {
|
2019-06-08 15:38:13 +00:00
|
|
|
value = window.localStorage.getItem("tw5-password-" + name);
|
2014-07-06 09:11:21 +00:00
|
|
|
} catch(e) {
|
2019-06-08 15:38:13 +00:00
|
|
|
}
|
|
|
|
if(value !== undefined) {
|
|
|
|
return value;
|
|
|
|
} else {
|
|
|
|
return ($tw.savedPasswords || Object.create(null))[name] || "";
|
2014-07-06 09:11:21 +00:00
|
|
|
}
|
2012-07-13 12:02:34 +00:00
|
|
|
};
|
|
|
|
|
2012-08-02 21:32:34 +00:00
|
|
|
/*
|
|
|
|
Force layout of a dom node and its descendents
|
|
|
|
*/
|
|
|
|
exports.forceLayout = function(element) {
|
|
|
|
var dummy = element.offsetWidth;
|
|
|
|
};
|
|
|
|
|
2012-10-15 16:18:32 +00:00
|
|
|
/*
|
|
|
|
Pulse an element for debugging purposes
|
|
|
|
*/
|
|
|
|
exports.pulseElement = function(element) {
|
|
|
|
// Event handler to remove the class at the end
|
|
|
|
element.addEventListener($tw.browser.animationEnd,function handler(event) {
|
|
|
|
element.removeEventListener($tw.browser.animationEnd,handler,false);
|
|
|
|
$tw.utils.removeClass(element,"pulse");
|
|
|
|
},false);
|
|
|
|
// Apply the pulse class
|
|
|
|
$tw.utils.removeClass(element,"pulse");
|
2012-11-06 17:21:56 +00:00
|
|
|
$tw.utils.forceLayout(element);
|
2012-10-15 16:18:32 +00:00
|
|
|
$tw.utils.addClass(element,"pulse");
|
|
|
|
};
|
|
|
|
|
2012-12-13 21:31:41 +00:00
|
|
|
/*
|
|
|
|
Attach specified event handlers to a DOM node
|
2013-09-14 12:47:51 +00:00
|
|
|
domNode: where to attach the event handlers
|
|
|
|
events: array of event handlers to be added (see below)
|
|
|
|
Each entry in the events array is an object with these properties:
|
|
|
|
handlerFunction: optional event handler function
|
|
|
|
handlerObject: optional event handler object
|
|
|
|
handlerMethod: optionally specifies object handler method name (defaults to `handleEvent`)
|
2012-12-13 21:31:41 +00:00
|
|
|
*/
|
|
|
|
exports.addEventListeners = function(domNode,events) {
|
|
|
|
$tw.utils.each(events,function(eventInfo) {
|
|
|
|
var handler;
|
|
|
|
if(eventInfo.handlerFunction) {
|
|
|
|
handler = eventInfo.handlerFunction;
|
|
|
|
} else if(eventInfo.handlerObject) {
|
|
|
|
if(eventInfo.handlerMethod) {
|
|
|
|
handler = function(event) {
|
|
|
|
eventInfo.handlerObject[eventInfo.handlerMethod].call(eventInfo.handlerObject,event);
|
2021-05-30 18:20:17 +00:00
|
|
|
};
|
2012-12-13 21:31:41 +00:00
|
|
|
} else {
|
|
|
|
handler = eventInfo.handlerObject;
|
|
|
|
}
|
|
|
|
}
|
2022-06-22 07:18:13 +00:00
|
|
|
domNode.addEventListener(eventInfo.name,handler,false);
|
2012-12-13 21:31:41 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-04-22 07:36:29 +00:00
|
|
|
/*
|
|
|
|
Get the computed styles applied to an element as an array of strings of individual CSS properties
|
|
|
|
*/
|
|
|
|
exports.getComputedStyles = function(domNode) {
|
|
|
|
var textAreaStyles = window.getComputedStyle(domNode,null),
|
2016-05-17 21:45:06 +00:00
|
|
|
styleDefs = [],
|
|
|
|
name;
|
|
|
|
for(var t=0; t<textAreaStyles.length; t++) {
|
|
|
|
name = textAreaStyles[t];
|
2016-05-09 20:11:08 +00:00
|
|
|
styleDefs.push(name + ": " + textAreaStyles.getPropertyValue(name) + ";");
|
2016-05-17 21:45:06 +00:00
|
|
|
}
|
2016-04-22 07:36:29 +00:00
|
|
|
return styleDefs;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Apply a set of styles passed as an array of strings of individual CSS properties
|
|
|
|
*/
|
|
|
|
exports.setStyles = function(domNode,styleDefs) {
|
|
|
|
domNode.style.cssText = styleDefs.join("");
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
Copy the computed styles from a source element to a destination element
|
|
|
|
*/
|
|
|
|
exports.copyStyles = function(srcDomNode,dstDomNode) {
|
|
|
|
$tw.utils.setStyles(dstDomNode,$tw.utils.getComputedStyles(srcDomNode));
|
|
|
|
};
|
2012-12-13 21:31:41 +00:00
|
|
|
|
2017-12-15 15:08:18 +00:00
|
|
|
/*
|
|
|
|
Copy plain text to the clipboard on browsers that support it
|
|
|
|
*/
|
|
|
|
exports.copyToClipboard = function(text,options) {
|
|
|
|
options = options || {};
|
2024-07-17 20:49:40 +00:00
|
|
|
text = text || "";
|
2017-12-15 15:08:18 +00:00
|
|
|
var textArea = document.createElement("textarea");
|
|
|
|
textArea.style.position = "fixed";
|
|
|
|
textArea.style.top = 0;
|
|
|
|
textArea.style.left = 0;
|
2017-12-17 21:37:29 +00:00
|
|
|
textArea.style.fontSize = "12pt";
|
2017-12-15 15:08:18 +00:00
|
|
|
textArea.style.width = "2em";
|
|
|
|
textArea.style.height = "2em";
|
|
|
|
textArea.style.padding = 0;
|
|
|
|
textArea.style.border = "none";
|
|
|
|
textArea.style.outline = "none";
|
|
|
|
textArea.style.boxShadow = "none";
|
|
|
|
textArea.style.background = "transparent";
|
|
|
|
textArea.value = text;
|
|
|
|
document.body.appendChild(textArea);
|
|
|
|
textArea.select();
|
2017-12-17 21:37:29 +00:00
|
|
|
textArea.setSelectionRange(0,text.length);
|
2017-12-15 15:08:18 +00:00
|
|
|
var succeeded = false;
|
|
|
|
try {
|
|
|
|
succeeded = document.execCommand("copy");
|
2024-07-17 20:49:40 +00:00
|
|
|
} catch(err) {
|
2017-12-15 15:08:18 +00:00
|
|
|
}
|
|
|
|
if(!options.doNotNotify) {
|
2024-05-29 14:06:33 +00:00
|
|
|
var successNotification = options.successNotification || "$:/language/Notifications/CopiedToClipboard/Succeeded",
|
|
|
|
failureNotification = options.failureNotification || "$:/language/Notifications/CopiedToClipboard/Failed"
|
|
|
|
$tw.notifier.display(succeeded ? successNotification : failureNotification);
|
2017-12-15 15:08:18 +00:00
|
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
|
|
};
|
|
|
|
|
2018-08-16 18:39:18 +00:00
|
|
|
exports.getLocationPath = function() {
|
|
|
|
return window.location.toString().split("#")[0];
|
|
|
|
};
|
|
|
|
|
2022-04-15 12:46:09 +00:00
|
|
|
/*
|
|
|
|
Collect DOM variables
|
|
|
|
*/
|
|
|
|
exports.collectDOMVariables = function(selectedNode,domNode,event) {
|
|
|
|
var variables = {},
|
|
|
|
selectedNodeRect,
|
|
|
|
domNodeRect;
|
|
|
|
if(selectedNode) {
|
|
|
|
$tw.utils.each(selectedNode.attributes,function(attribute) {
|
|
|
|
variables["dom-" + attribute.name] = attribute.value.toString();
|
|
|
|
});
|
2022-05-06 16:11:13 +00:00
|
|
|
|
2023-11-21 11:54:37 +00:00
|
|
|
if("offsetLeft" in selectedNode) {
|
2022-12-01 21:16:44 +00:00
|
|
|
// Add variables with a (relative and absolute) popup coordinate string for the selected node
|
|
|
|
var nodeRect = {
|
|
|
|
left: selectedNode.offsetLeft,
|
|
|
|
top: selectedNode.offsetTop,
|
|
|
|
width: selectedNode.offsetWidth,
|
|
|
|
height: selectedNode.offsetHeight
|
|
|
|
};
|
|
|
|
variables["tv-popup-coords"] = Popup.buildCoordinates(Popup.coordinatePrefix.csOffsetParent,nodeRect);
|
|
|
|
|
|
|
|
var absRect = $tw.utils.extend({}, nodeRect);
|
2024-07-17 20:49:40 +00:00
|
|
|
for(var currentNode = selectedNode.offsetParent; currentNode; currentNode = currentNode.offsetParent) {
|
2022-12-01 21:16:44 +00:00
|
|
|
absRect.left += currentNode.offsetLeft;
|
|
|
|
absRect.top += currentNode.offsetTop;
|
|
|
|
}
|
|
|
|
variables["tv-popup-abs-coords"] = Popup.buildCoordinates(Popup.coordinatePrefix.csAbsolute,absRect);
|
2022-04-15 12:46:09 +00:00
|
|
|
|
2022-05-06 16:11:13 +00:00
|
|
|
// Add variables for offset of selected node
|
|
|
|
variables["tv-selectednode-posx"] = selectedNode.offsetLeft.toString();
|
|
|
|
variables["tv-selectednode-posy"] = selectedNode.offsetTop.toString();
|
|
|
|
variables["tv-selectednode-width"] = selectedNode.offsetWidth.toString();
|
|
|
|
variables["tv-selectednode-height"] = selectedNode.offsetHeight.toString();
|
|
|
|
}
|
2022-04-15 12:46:09 +00:00
|
|
|
}
|
2022-05-09 09:42:23 +00:00
|
|
|
|
2023-11-21 11:54:37 +00:00
|
|
|
if(domNode && ("offsetWidth" in domNode)) {
|
2022-05-09 09:42:23 +00:00
|
|
|
variables["tv-widgetnode-width"] = domNode.offsetWidth.toString();
|
|
|
|
variables["tv-widgetnode-height"] = domNode.offsetHeight.toString();
|
|
|
|
}
|
2022-04-15 12:46:09 +00:00
|
|
|
|
2023-11-21 11:54:37 +00:00
|
|
|
if(event && ("clientX" in event) && ("clientY" in event)) {
|
2022-04-15 12:46:09 +00:00
|
|
|
if(selectedNode) {
|
|
|
|
// Add variables for event X and Y position relative to selected node
|
|
|
|
selectedNodeRect = selectedNode.getBoundingClientRect();
|
|
|
|
variables["event-fromselected-posx"] = (event.clientX - selectedNodeRect.left).toString();
|
|
|
|
variables["event-fromselected-posy"] = (event.clientY - selectedNodeRect.top).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(domNode) {
|
|
|
|
// Add variables for event X and Y position relative to event catcher node
|
|
|
|
domNodeRect = domNode.getBoundingClientRect();
|
|
|
|
variables["event-fromcatcher-posx"] = (event.clientX - domNodeRect.left).toString();
|
|
|
|
variables["event-fromcatcher-posy"] = (event.clientY - domNodeRect.top).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add variables for event X and Y position relative to the viewport
|
|
|
|
variables["event-fromviewport-posx"] = event.clientX.toString();
|
|
|
|
variables["event-fromviewport-posy"] = event.clientY.toString();
|
|
|
|
}
|
|
|
|
return variables;
|
|
|
|
};
|
|
|
|
|
2023-05-06 10:54:54 +00:00
|
|
|
/*
|
|
|
|
Make sure the CSS selector is not invalid
|
|
|
|
*/
|
|
|
|
exports.querySelectorSafe = function(selector,baseElement) {
|
|
|
|
baseElement = baseElement || document;
|
|
|
|
try {
|
|
|
|
return baseElement.querySelector(selector);
|
|
|
|
} catch(e) {
|
|
|
|
console.log("Invalid selector: ",selector);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.querySelectorAllSafe = function(selector,baseElement) {
|
|
|
|
baseElement = baseElement || document;
|
|
|
|
try {
|
|
|
|
return baseElement.querySelectorAll(selector);
|
|
|
|
} catch(e) {
|
|
|
|
console.log("Invalid selector: ",selector);
|
|
|
|
}
|
|
|
|
};
|
2018-08-16 18:39:18 +00:00
|
|
|
|
2012-06-20 21:16:41 +00:00
|
|
|
})();
|