Fixes to make nested popups work

Thus making the export button a lot more useful
This commit is contained in:
Jermolene 2014-11-21 17:07:03 +00:00
parent 27c9e7269e
commit b882a0dff1
9 changed files with 154 additions and 67 deletions

View File

@ -19,40 +19,7 @@ Creates a Popup object with these options:
var Popup = function(options) {
options = options || {};
this.rootElement = options.rootElement || document.body;
};
Popup.prototype.show = function(options) {
this.cancel();
this.title = options.title;
this.wiki = options.wiki;
this.anchorDomNode = options.domNode;
$tw.utils.addClass(this.anchorDomNode,"tc-popup");
this.rootElement.addEventListener("click",this,false);
};
Popup.prototype.handleEvent = function(event) {
// Dismiss the popup if we get a click on an element that doesn't have .tc-popup class
if(event.type === "click") {
var node = event.target;
while(node && !$tw.utils.hasClass(node,"tc-popup")) {
node = node.parentNode;
}
if(!node) {
this.cancel();
}
}
};
Popup.prototype.cancel = function() {
if(this.anchorDomNode) {
$tw.utils.removeClass(this.anchorDomNode,"tc-popup");
this.anchorDomNode = null;
}
this.rootElement.removeEventListener("click",this,false);
if(this.title) {
this.wiki.deleteTiddler(this.title);
this.title = null;
}
this.popups = []; // Array of {title:,wiki:,domNode:} objects
};
/*
@ -60,38 +27,134 @@ Trigger a popup open or closed. Parameters are in a hashmap:
title: title of the tiddler where the popup details are stored
domNode: dom node to which the popup will be positioned
wiki: wiki
force: if specified, forces the popup state to true or false
force: if specified, forces the popup state to true or false (instead of toggling it)
*/
Popup.prototype.triggerPopup = function(options) {
// Get the current popup state tiddler
var value = options.wiki.getTextReference(options.title,"");
// Check if the popup is open by checking whether it matches "(<x>,<y>)"
var state = !this.readPopupState(options.title,value);
if("force" in options) {
console.log("triggerPopup",options)
// Check if this popup is already active
var index = -1;
for(var t=0; t<this.popups.length; t++) {
if(this.popups[t].title === options.title) {
index = t;
}
}
// Compute the new state
var state = index === -1;
if(options.force !== undefined) {
state = options.force;
}
// Show or cancel the popup according to the new state
if(state) {
// Set the position if we're opening it
this.cancel();
options.wiki.setTextReference(options.title,
"(" + options.domNode.offsetLeft + "," + options.domNode.offsetTop + "," +
options.domNode.offsetWidth + "," + options.domNode.offsetHeight + ")");
this.show(options);
} else {
this.cancel();
this.cancel(index);
}
};
Popup.prototype.handleEvent = function(event) {
console.log("handleEvent",event)
if(event.type === "click") {
// Find out what was clicked on
var info = this.popupInfo(event.target),
cancelLevel = info.popupLevel - 1;
// Don't remove the level that was clicked on if we clicked on a handle
if(info.isHandle) {
cancelLevel++;
}
// Cancel
this.cancel(cancelLevel);
}
};
/*
Find the popup level containing a DOM node. Returns:
popupLevel: count of the number of nested popups containing the specified element
isHandle: true if the specified element is within a popup handle
*/
Popup.prototype.popupInfo = function(domNode) {
var isHandle = false,
popupCount = 0,
node = domNode;
// First check ancestors to see if we're within a popup handle
while(node) {
if($tw.utils.hasClass(node,"tc-popup-handle")) {
isHandle = true;
popupCount++;
}
if($tw.utils.hasClass(node,"tc-popup-keep")) {
isHandle = true;
}
node = node.parentNode;
}
// Then count the number of ancestor popups
node = domNode;
while(node) {
if($tw.utils.hasClass(node,"tc-popup")) {
popupCount++;
}
node = node.parentNode;
}
var info = {
popupLevel: popupCount,
isHandle: isHandle
};
console.log("Returning popupInfo",info)
return info;
};
/*
Display a popup by adding it to the stack
*/
Popup.prototype.show = function(options) {
console.log("show",options)
// Find out what was clicked on
var info = this.popupInfo(options.domNode);
// Cancel any higher level popups
this.cancel(info.popupLevel);
// Store the popup details
this.popups.push({
title: options.title,
wiki: options.wiki,
domNode: options.domNode
});
// Set the state tiddler
options.wiki.setTextReference(options.title,
"(" + options.domNode.offsetLeft + "," + options.domNode.offsetTop + "," +
options.domNode.offsetWidth + "," + options.domNode.offsetHeight + ")");
// Add the click handler if we have any popups
if(this.popups.length > 0) {
console.log("Adding click handler")
this.rootElement.addEventListener("click",this,true);
}
};
/*
Cancel all popups at or above a specified level or DOM node
level: popup level to cancel (0 cancels all popups)
*/
Popup.prototype.cancel = function(level) {
console.log("cancel",level)
var numPopups = this.popups.length;
level = Math.max(0,Math.min(level,numPopups));
for(var t=level; t<numPopups; t++) {
var popup = this.popups.pop();
if(popup.title) {
popup.wiki.deleteTiddler(popup.title);
}
}
if(this.popups.length === 0) {
console.log("Removing click handler")
this.rootElement.removeEventListener("click",this,false);
}
};
/*
Returns true if the specified title and text identifies an active popup
*/
Popup.prototype.readPopupState = function(title,text) {
var popupLocationRegExp = /^\((-?[0-9\.E]+),(-?[0-9\.E]+),(-?[0-9\.E]+),(-?[0-9\.E]+)\)$/,
result = false;
if(this.title === title) {
result = popupLocationRegExp.test(text);
}
return result;
Popup.prototype.readPopupState = function(text) {
console.log("readPopupState",text)
var popupLocationRegExp = /^\((-?[0-9\.E]+),(-?[0-9\.E]+),(-?[0-9\.E]+),(-?[0-9\.E]+)\)$/;
return popupLocationRegExp.test(text);
};
exports.Popup = Popup;

View File

@ -462,20 +462,24 @@ Returns an object with the following fields, all optional:
exports.parseTextReference = function(textRef) {
// Separate out the title, field name and/or JSON indices
var reTextRef = /^\s*([^!#]+)?(?:(?:!!([^\s]+))|(?:##(.+)))?\s*/mg,
match = reTextRef.exec(textRef);
match = reTextRef.exec(textRef),
result = {};
if(match && reTextRef.lastIndex === textRef.length) {
// Return the parts
return {
title: match[1],
field: match[2],
index: match[3]
};
if(match[1]) {
result.title = match[1];
}
if(match[2]) {
result.field = match[2];
}
if(match[3]) {
result.index = match[3];
}
} else {
// If we couldn't parse it (eg it started with a)
return {
title: textRef
};
// If we couldn't parse it
result.title = textRef
}
return result;
};
/*

View File

@ -36,15 +36,19 @@ ButtonWidget.prototype.render = function(parent,nextSibling) {
// Create element
var domNode = this.document.createElement("button");
// Assign classes
var classes = this["class"].split(" ") || [];
var classes = this["class"].split(" ") || [],
isPoppedUp = this.popup && this.isPoppedUp();
if(this.selectedClass) {
if(this.set && this.setTo && this.isSelected()) {
$tw.utils.pushTop(classes,this.selectedClass.split(" "));
}
if(this.popup && this.isPoppedUp()) {
if(isPoppedUp) {
$tw.utils.pushTop(classes,this.selectedClass.split(" "));
}
}
if(isPoppedUp) {
$tw.utils.pushTop(classes,"tc-popup-handle");
}
domNode.className = classes.join(" ");
// Assign other attributes
if(this.style) {
@ -101,7 +105,7 @@ ButtonWidget.prototype.isSelected = function() {
ButtonWidget.prototype.isPoppedUp = function() {
var tiddler = this.wiki.getTiddler(this.popup);
var result = tiddler && tiddler.fields.text ? $tw.popup.readPopupState(this.popup,tiddler.fields.text) : false;
var result = tiddler && tiddler.fields.text ? $tw.popup.readPopupState(tiddler.fields.text) : false;
return result;
};

View File

@ -6,6 +6,7 @@ description: {{$:/language/Buttons/Language/Hint}}
\define flag-title()
$(languagePluginTitle)$/icon
\end
<span class="tc-popup-keep">
<$button popup=<<qualify "$:/state/popup/language">> tooltip={{$:/language/Buttons/Language/Hint}} aria-label={{$:/language/Buttons/Language/Caption}} class=<<tv-config-toolbar-class>> selectedClass="tc-selected">
<$list filter="[<tv-config-toolbar-icons>prefix[yes]]">
<span class="tc-image-button">
@ -18,6 +19,7 @@ $(languagePluginTitle)$/icon
<span class="tc-btn-text"><$text text={{$:/language/Buttons/Language/Caption}}/></span>
</$list>
</$button>
</span>
<$reveal state=<<qualify "$:/state/popup/language">> type="popup" position="below" animate="yes">
<div class="tc-drop-down tc-drop-down-language-chooser">
<$linkcatcher to="$:/language">

View File

@ -6,6 +6,7 @@ description: {{$:/language/Buttons/StoryView/Hint}}
\define icon()
$:/core/images/storyview-$(storyview)$
\end
<span class="tc-popup-keep">
<$button popup=<<qualify "$:/state/popup/storyview">> tooltip={{$:/language/Buttons/StoryView/Hint}} aria-label={{$:/language/Buttons/StoryView/Caption}} class=<<tv-config-toolbar-class>> selectedClass="tc-selected">
<$list filter="[<tv-config-toolbar-icons>prefix[yes]]">
<$set name="storyview" value={{$:/view}}>
@ -16,6 +17,7 @@ $:/core/images/storyview-$(storyview)$
<span class="tc-btn-text"><$text text={{$:/language/Buttons/StoryView/Caption}}/></span>
</$list>
</$button>
</span>
<$reveal state=<<qualify "$:/state/popup/storyview">> type="popup" position="below" animate="yes">
<div class="tc-drop-down">
<$linkcatcher to="$:/view">

View File

@ -3,6 +3,7 @@ tags: $:/tags/PageControls
caption: {{$:/core/images/theme-button}} {{$:/language/Buttons/Theme/Caption}}
description: {{$:/language/Buttons/Theme/Hint}}
<span class="tc-popup-keep">
<$button popup=<<qualify "$:/state/popup/theme">> tooltip={{$:/language/Buttons/Theme/Hint}} aria-label={{$:/language/Buttons/Theme/Caption}} class=<<tv-config-toolbar-class>> selectedClass="tc-selected">
<$list filter="[<tv-config-toolbar-icons>prefix[yes]]">
{{$:/core/images/theme-button}}
@ -11,6 +12,7 @@ description: {{$:/language/Buttons/Theme/Hint}}
<span class="tc-btn-text"><$text text={{$:/language/Buttons/Theme/Caption}}/></span>
</$list>
</$button>
</span>
<$reveal state=<<qualify "$:/state/popup/theme">> type="popup" position="below" animate="yes">
<div class="tc-drop-down">
<$linkcatcher to="$:/theme">

View File

@ -29,7 +29,7 @@ $:/config/ViewToolbarButtons/Visibility/$(listItem)$
</$list>
</div>
<$reveal type="nomatch" text="" default="" state=<<tiddlerInfoState>> class="tc-tiddler-info tc-popup" animate="yes" retain="yes">
<$reveal type="nomatch" text="" default="" state=<<tiddlerInfoState>> class="tc-tiddler-info tc-popup-keep" animate="yes" retain="yes">
<$transclude tiddler="$:/core/ui/TiddlerInfo"/>

View File

@ -2,6 +2,7 @@ title: $:/core/macros/export
tags: $:/tags/Macro
\define exportButton(exportFilter:"[!is[system]sort[title]]",lingoBase)
<span class="tc-popup-keep">
<$button popup=<<qualify "$:/state/popup/export">> tooltip={{$lingoBase$Hint}} aria-label={{$lingoBase$Caption}} class=<<tv-config-toolbar-class>> selectedClass="tc-selected">
<$list filter="[<tv-config-toolbar-icons>prefix[yes]]">
{{$:/core/images/export-button}}
@ -10,6 +11,7 @@ tags: $:/tags/Macro
<span class="tc-btn-text"><$text text={{$lingoBase$Caption}}/></span>
</$list>
</$button>
</span>
<$reveal state=<<qualify "$:/state/popup/export">> type="popup" position="below" animate="yes">
<div class="tc-drop-down">
<$list filter="[all[shadows+tiddlers]tag[$:/tags/Exporter]]">

View File

@ -651,6 +651,10 @@ button.tc-untagged-label {
font-size: 0.6em;
}
.tc-tiddler-controls .tc-drop-down .tc-drop-down {
font-size: 1em;
}
.tc-tiddler-controls button {
vertical-align: baseline;
}
@ -917,6 +921,10 @@ canvas.tc-edit-bitmapeditor {
line-height: 1.4;
}
.tc-drop-down .tc-drop-down {
margin-left: 14px;
}
.tc-drop-down button svg, .tc-drop-down a svg {
fill: <<colour foreground>>;
}