1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-30 13:29:56 +00:00
TiddlyWiki5/core/modules/widgets/reveal.js
FlashSystems 3918e59cc1
Fixed PR to fix popup position if popup is triggered from within an offsetParent element (#7013)
* Fix popup location for tables

This commit introduces the `popupAbsCoords` option to the $button widget
and implements an absolut coordinate format.

Coordinates for popups are stored in the format `(x,y,w,h)`. These
coordinates are relative to the offset parent of the element that
defines the popup.

This commits adds a second format `@(x,y,w,h)`. Coordinates specified in
this format a relative to the pages root element.

The `popupAbsCoords` option of the $button widget enables the use of
this coordinates.

* Unify the declaration of the RegEx for parsing the popup-position

The regular expression was declared in three locations with the same
content. This commit supplies a new function `parseCoordinates` in
`popup.js`. This function returns the parsed coordinates and understands
the classic/absolute coordinates.

This function is used in `reveal.js` and `action-popup.js` to parse the
coordinates.

* Add documentation for coordinate systems

* Consolidate creating coordinate strings

The Popup object now contains a `buildCoordinates` method that can be
used to build coordinate strings. It takes an "enum" for the coordinate-
system to use. This makes everything easily extensible and prevents the
use of magic values.

* Add tests for `parseCoordinates` and `buildCoordinates`

* Add `tv-popup-abs-coords` to `collectDOMVariables`

This will make the absolute coordinates available for the
`DraggableWidget` and the `EventCatcherWidget`.

* Add documentation for the `tv-popup-abs-coords`

... to the `DraggableWidget` and the `EventCatcherWidget`.

* Fix crash when generating a static version of the TW

The Popup class is not initialized in `startup.js` if `$tw.browser` is
not true. After having consolidated the facilities for parsing
coordinate strings into `popup.js` this breaks because the static build
needs to parse coordinate stings even if no Popup module is initialized.
This commit solves this problem by making `readPopupState`,
`parseCoordinates` and `buildCoordinates` static methods of `popup.js`.
It also adds a comment to these functions to show that these can be called
safely even if the Popup-Class is not initialized.
2022-12-01 21:16:44 +00:00

280 lines
8.5 KiB
JavaScript
Executable File

/*\
title: $:/core/modules/widgets/reveal.js
type: application/javascript
module-type: widget
Reveal widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var Widget = require("$:/core/modules/widgets/widget.js").widget;
var Popup = require("$:/core/modules/utils/dom/popup.js");
var RevealWidget = function(parseTreeNode,options) {
this.initialise(parseTreeNode,options);
};
/*
Inherit from the base widget class
*/
RevealWidget.prototype = new Widget();
/*
Render this widget into the DOM
*/
RevealWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
var tag = this.parseTreeNode.isBlock ? "div" : "span";
if(this.revealTag && $tw.config.htmlUnsafeElements.indexOf(this.revealTag) === -1) {
tag = this.revealTag;
}
var domNode = this.document.createElement(tag);
this.domNode = domNode;
this.assignDomNodeClasses();
if(this.style) {
domNode.setAttribute("style",this.style);
}
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
if(!domNode.isTiddlyWikiFakeDom && this.type === "popup" && this.isOpen) {
this.positionPopup(domNode);
$tw.utils.addClass(domNode,"tc-popup"); // Make sure that clicks don't dismiss popups within the revealed content
}
if(!this.isOpen) {
domNode.setAttribute("hidden","true");
}
this.domNodes.push(domNode);
};
RevealWidget.prototype.positionPopup = function(domNode) {
domNode.style.position = "absolute";
domNode.style.zIndex = "1000";
var left,top;
switch(this.position) {
case "left":
left = this.popup.left - domNode.offsetWidth;
top = this.popup.top;
break;
case "above":
left = this.popup.left;
top = this.popup.top - domNode.offsetHeight;
break;
case "aboveright":
left = this.popup.left + this.popup.width;
top = this.popup.top + this.popup.height - domNode.offsetHeight;
break;
case "belowright":
left = this.popup.left + this.popup.width;
top = this.popup.top + this.popup.height;
break;
case "right":
left = this.popup.left + this.popup.width;
top = this.popup.top;
break;
case "belowleft":
left = this.popup.left + this.popup.width - domNode.offsetWidth;
top = this.popup.top + this.popup.height;
break;
case "aboveleft":
left = this.popup.left - domNode.offsetWidth;
top = this.popup.top - domNode.offsetHeight;
break;
default: // Below
left = this.popup.left;
top = this.popup.top + this.popup.height;
break;
}
if(!this.positionAllowNegative) {
left = Math.max(0,left);
top = Math.max(0,top);
}
if (this.popup.absolute) {
// Traverse the offsetParent chain and correct the offset to make it relative to the parent node.
for (var offsetParentDomNode = domNode.offsetParent; offsetParentDomNode; offsetParentDomNode = offsetParentDomNode.offsetParent) {
left -= offsetParentDomNode.offsetLeft;
top -= offsetParentDomNode.offsetTop;
}
}
domNode.style.left = left + "px";
domNode.style.top = top + "px";
};
/*
Compute the internal state of the widget
*/
RevealWidget.prototype.execute = function() {
// Get our parameters
this.state = this.getAttribute("state");
this.revealTag = this.getAttribute("tag");
this.type = this.getAttribute("type");
this.text = this.getAttribute("text");
this.position = this.getAttribute("position");
this.positionAllowNegative = this.getAttribute("positionAllowNegative") === "yes";
// class attribute handled in assignDomNodeClasses()
this.style = this.getAttribute("style","");
this["default"] = this.getAttribute("default","");
this.animate = this.getAttribute("animate","no");
this.retain = this.getAttribute("retain","no");
this.openAnimation = this.animate === "no" ? undefined : "open";
this.closeAnimation = this.animate === "no" ? undefined : "close";
this.updatePopupPosition = this.getAttribute("updatePopupPosition","no") === "yes";
// Compute the title of the state tiddler and read it
this.stateTiddlerTitle = this.state;
this.stateTitle = this.getAttribute("stateTitle");
this.stateField = this.getAttribute("stateField");
this.stateIndex = this.getAttribute("stateIndex");
this.readState();
// Construct the child widgets
var childNodes = this.isOpen ? this.parseTreeNode.children : [];
this.hasChildNodes = this.isOpen;
this.makeChildWidgets(childNodes);
};
/*
Read the state tiddler
*/
RevealWidget.prototype.readState = function() {
// Read the information from the state tiddler
var state,
defaultState = this["default"];
if(this.stateTitle) {
var stateTitleTiddler = this.wiki.getTiddler(this.stateTitle);
if(this.stateField) {
state = stateTitleTiddler ? stateTitleTiddler.getFieldString(this.stateField) || defaultState : defaultState;
} else if(this.stateIndex) {
state = stateTitleTiddler ? this.wiki.extractTiddlerDataItem(this.stateTitle,this.stateIndex) || defaultState : defaultState;
} else if(stateTitleTiddler) {
state = this.wiki.getTiddlerText(this.stateTitle) || defaultState;
} else {
state = defaultState;
}
} else {
state = this.stateTiddlerTitle ? this.wiki.getTextReference(this.state,this["default"],this.getVariable("currentTiddler")) : this["default"];
}
if(state === null) {
state = this["default"];
}
switch(this.type) {
case "popup":
this.readPopupState(state);
break;
case "match":
this.isOpen = this.text === state;
break;
case "nomatch":
this.isOpen = this.text !== state;
break;
case "lt":
this.isOpen = !!(this.compareStateText(state) < 0);
break;
case "gt":
this.isOpen = !!(this.compareStateText(state) > 0);
break;
case "lteq":
this.isOpen = !(this.compareStateText(state) > 0);
break;
case "gteq":
this.isOpen = !(this.compareStateText(state) < 0);
break;
}
};
RevealWidget.prototype.compareStateText = function(state) {
return state.localeCompare(this.text,undefined,{numeric: true,sensitivity: "case"});
};
RevealWidget.prototype.readPopupState = function(state) {
this.popup = Popup.parseCoordinates(state);
// Check if the state matches the location regexp
if(this.popup) {
// If so, we're open
this.isOpen = true;
} else {
// If not, we're closed
this.isOpen = false;
}
};
RevealWidget.prototype.assignDomNodeClasses = function() {
var classes = this.getAttribute("class","").split(" ");
classes.push("tc-reveal");
this.domNode.className = classes.join(" ");
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
RevealWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.state || changedAttributes.type || changedAttributes.text || changedAttributes.position || changedAttributes.positionAllowNegative || changedAttributes["default"] || changedAttributes.animate || changedAttributes.stateTitle || changedAttributes.stateField || changedAttributes.stateIndex) {
this.refreshSelf();
return true;
} else {
var currentlyOpen = this.isOpen;
this.readState();
if(this.isOpen !== currentlyOpen) {
if(this.retain === "yes") {
this.updateState();
} else {
this.refreshSelf();
return true;
}
} else if(this.type === "popup" && this.isOpen && this.updatePopupPosition && (changedTiddlers[this.state] || changedTiddlers[this.stateTitle])) {
this.positionPopup(this.domNode);
}
if(changedAttributes.style) {
this.domNode.style = this.getAttribute("style","");
}
if(changedAttributes["class"]) {
this.assignDomNodeClasses();
}
return this.refreshChildren(changedTiddlers);
}
};
/*
Called by refresh() to dynamically show or hide the content
*/
RevealWidget.prototype.updateState = function() {
var self = this;
// Read the current state
this.readState();
// Construct the child nodes if needed
var domNode = this.domNodes[0];
if(this.isOpen && !this.hasChildNodes) {
this.hasChildNodes = true;
this.makeChildWidgets(this.parseTreeNode.children);
this.renderChildren(domNode,null);
}
// Animate our DOM node
if(!domNode.isTiddlyWikiFakeDom && this.type === "popup" && this.isOpen) {
this.positionPopup(domNode);
$tw.utils.addClass(domNode,"tc-popup"); // Make sure that clicks don't dismiss popups within the revealed content
}
if(this.isOpen) {
domNode.removeAttribute("hidden");
$tw.anim.perform(this.openAnimation,domNode);
} else {
$tw.anim.perform(this.closeAnimation,domNode,{callback: function() {
//make sure that the state hasn't changed during the close animation
self.readState()
if(!self.isOpen) {
domNode.setAttribute("hidden","true");
}
}});
}
};
exports.reveal = RevealWidget;
})();