1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-12-25 01:20:30 +00:00

Add element spotlight to dynannotate plugin

Useful for highlighting on screen elements for the user
This commit is contained in:
jeremy@jermolene.com 2023-02-01 17:12:06 +00:00
parent 75af83174b
commit 1d32ef44e5
5 changed files with 258 additions and 1 deletions

View File

@ -5,6 +5,7 @@ The ''Dynannotate'' plugin allows annotations on textual content to be created a
* The dynannotate widget draws clickable textual annotations, search highlights and search snippets as overlays over the top of the content that it contains
* The selection tracker keeps track of changes to the selected text in the main browser window. It triggers an action string when the selection changes, passing it the details of the selection. It can be used to display a popup menu
** The original legacy selection tracker is also provided for backwards compatibility. It is much more limited, and not recommended for new projects
* The element spotlight highlights on screen elements using a spotlight animation
!! Dynannotate Widget
@ -170,3 +171,12 @@ Notes:
* The selection popup will disappear if the selection is cancelled; this will happen if the user clicks on any other element apart than a button. Thus it is not possible to have any interactive controls within the popup apart from buttons
!! Element Spotlight
The `tm-spotlight-element` message causes a spotlight effect to briefly appear to highlight a specified element. The message accepts the following parameters:
|!Parameter |!Description |
|`selector` |CSS selector of the element to highlight |
|{//Any parameter names starting with `selector-`}// |Fallback CSS selectors to be used if the primary selector does not resolve to an element |
The fallback CSS selectors are case-insensitively sorted by title before use, with uppercase letters sorting before lower case letters. The usual convention is to use numeric suffixes: `selector-00`, `selector-01` etc.

View File

@ -0,0 +1,71 @@
title: $:/plugins/tiddlywiki/dynannotate/examples/spotlight
tags: $:/tags/dynannotateExamples
caption: Spotlight
\define show-example(example)
<$codeblock code=<<__example__>>/>
//''Displays as:''//
$example$
\end
<div class="tc-dynannotation-example-info">
!! Spotlighting an Image
</div>
<<show-example """
<$button>
<$action-sendmessage $message="tm-spotlight-element" selector=".tc-dynannotate-spotlight-image-example"/>
Spotlight this image
</$button>
<div class="tc-dynannotate-spotlight-image-example" style="display:inline-block;">
{{$:/core/images/globe}}
</div>
""">>
<div class="tc-dynannotation-example-info">
!! Spotlighting a Button
</div>
<<show-example """
<$button class="tc-dynannotate-spotlight-button-example">
<$action-sendmessage $message="tm-spotlight-element" selector=".tc-dynannotate-spotlight-button-example"/>
Spotlight this button
</$button>
""">>
<div class="tc-dynannotation-example-info">
!! Spotlighting a Text Area
</div>
<<show-example """
<$button>
<$action-sendmessage $message="tm-spotlight-element" selector=".tc-dynannotate-spotlight-textarea-example"/>
Spotlight this text area
</$button>
<$edit-text class="tc-dynannotate-spotlight-textarea-example" tag="textarea" tiddler="$:/temp/dynannotate/spotlight/demo/text"/>
""">>
<div class="tc-dynannotation-example-info">
!! Spotlighting the Sidebar Search Input
This button will spotlight the sidebar search, but if the sidebar is hidden then it will spotlight the button for showing the sidebar.
</div>
<<show-example """
<$button>
<$action-sendmessage $message="tm-spotlight-element" selector=".tc-sidebar-search .tc-popup-handle" selector-fallback=".tc-menubar .tc-show-sidebar-btn"/>
Spotlight the sidebar search input
</$button>
""">>

View File

@ -0,0 +1,136 @@
/*\
title: $:/plugins/tiddlywiki/dynannotate/element-spotlight.js
type: application/javascript
module-type: library
Manages the element spotlight effect
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
function ElementSpotlight() {
this.animationStartTime; // Undefined if no animation is in progress
// Create DOM nodes
this.spotlightElement = $tw.utils.domMaker("div",{
"class": "tc-dynannotate-spotlight"
});
this.spotlightWrapper = $tw.utils.domMaker("div",{
"class": "tc-dynannotate-spotlight-wrapper",
children: [
this.spotlightElement
]
});
document.body.appendChild(this.spotlightWrapper);
}
/*
Return the first visible element that matches a selector
*/
ElementSpotlight.prototype.querySelectorSafe = function(selector) {
var targetNodes;
// Get the matching elements
try {
targetNodes = document.querySelectorAll(selector);
} catch(e) {
console.log("Error with selector: " + selector);
}
if(!targetNodes) {
return undefined;
}
// Remove any elements from the start of the list that are hidden, or have hidden ancestors
var didRemoveFirstEntry;
do {
didRemoveFirstEntry = false;
var hasHiddenAncestor = false,
n = targetNodes[0];
while(n) {
if(n.hidden || (n instanceof Element && window.getComputedStyle(n).display === "none")) {
hasHiddenAncestor = true;
break;
}
n = n.parentNode;
}
if(hasHiddenAncestor) {
// Remove first entry from targetNodes array
targetNodes = [].slice.call(targetNodes, 1);
didRemoveFirstEntry = true;
}
} while(didRemoveFirstEntry)
// Return the first result
return targetNodes[0];
};
ElementSpotlight.prototype.positionSpotlight = function(x,y,innerRadius,outerRadius,opacity) {
this.spotlightElement.style.display = "block";
this.spotlightElement.style.backgroundImage = "radial-gradient(circle at " + (x / window.innerWidth * 100) + "% " + (y / window.innerHeight * 100) + "%, transparent " + innerRadius + "px, rgba(0, 0, 0, " + opacity + ") " + outerRadius + "px)";
};
ElementSpotlight.prototype.easeInOut = function(v) {
return (Math.sin((v - 0.5) * Math.PI) + 1) / 2;
};
/*
Shine a spotlight on the first element that matches an array of selectors
*/
ElementSpotlight.prototype.shineSpotlight = function(selectors) {
var self = this;
function animationLoop(selectors) {
// Calculate how far through the animation we are
// 0...1 = zoom in
// 1...2 = hold
// 2...3 = fade out
var now = new Date(),
t = (now - self.animationStartTime) / ($tw.utils.getAnimationDuration() * 2);
t = t >= 3 ? 3 : t;
// Query the selector for the target element
var targetNode, selectorIndex = 0;
while(!targetNode && selectorIndex < selectors.length) {
targetNode = self.querySelectorSafe(selectors[selectorIndex]);
selectorIndex += 1;
}
// Position the spotlight if we've got the target
if(targetNode) {
var rect = targetNode.getBoundingClientRect();
var innerRadius, outerRadius, opacity;
if(t <= 1) {
t = self.easeInOut(t);
innerRadius = rect.width / 2 + (window.innerWidth * 2 * (1 - t));
outerRadius = rect.width + (window.innerWidth * 3 * (1 - t));
opacity = 0.2 + t / 4;
} else if(t <= 2) {
innerRadius = rect.width / 2;
outerRadius = rect.width;
opacity = 0.45;
} else {
t = self.easeInOut(3 - t);
innerRadius = rect.width / 2 + (window.innerWidth * 2 * (1 - t));
outerRadius = rect.width + (window.innerWidth * 3 * (1 - t));
opacity = t / 3;
}
self.positionSpotlight((rect.left + rect.right) / 2,(rect.top + rect.bottom) / 2,innerRadius,outerRadius,opacity);
} else {
self.spotlightElement.style.display = "none";
}
// Call the next frame unless we're at the end
if(t <= 3) {
window.requestAnimationFrame(function () {
animationLoop(selectors);
});
} else {
// End the animation if we've exceeded the time limit
self.animationStartTime = undefined;
}
}
this.animationStartTime = new Date();
window.requestAnimationFrame(function () {
animationLoop(selectors);
});
};
exports.ElementSpotlight = ElementSpotlight;
})();

View File

@ -1,3 +1,5 @@
const { ElementSpotlight } = require("./element-spotlight");
/*\
title: $:/plugins/tiddlywiki/dynannotate/startup.js
type: application/javascript
@ -22,18 +24,39 @@ var CONFIG_SELECTION_TRACKER_TITLE = "$:/config/Dynannotate/SelectionTracker/Ena
CONFIG_LEGACY_SELECTION_TRACKER_TITLE = "$:/config/Dynannotate/LegacySelectionTracker/Enable";
var SelectionTracker = require("$:/plugins/tiddlywiki/dynannotate/selection-tracker.js").SelectionTracker,
LegacySelectionTracker = require("$:/plugins/tiddlywiki/dynannotate/legacy-selection-tracker.js").LegacySelectionTracker;
LegacySelectionTracker = require("$:/plugins/tiddlywiki/dynannotate/legacy-selection-tracker.js").LegacySelectionTracker,
ElementSpotlight = require("$:/plugins/tiddlywiki/dynannotate/element-spotlight.js").ElementSpotlight;
exports.startup = function() {
$tw.dynannotate = {};
// Setup selection tracker
if($tw.wiki.getTiddlerText(CONFIG_SELECTION_TRACKER_TITLE,"yes") === "yes") {
$tw.dynannotate.selectionTracker = new SelectionTracker($tw.wiki);
}
// Setup legacy selection tracker
if($tw.wiki.getTiddlerText(CONFIG_LEGACY_SELECTION_TRACKER_TITLE,"yes") === "yes") {
$tw.dynannotate.legacySelectionTracker = new LegacySelectionTracker($tw.wiki,{
allowBlankSelectionPopup: true
});
}
// Set up the element spotlight
$tw.dynannotate.elementSpotlight = new ElementSpotlight();
$tw.rootWidget.addEventListener("tm-spotlight-element",function(event) {
var selectors = [];
if(event.paramObject.selector) {
selectors.push(event.paramObject.selector);
}
$tw.utils.each(Object.keys(event.paramObject).sort(),function(name) {
var SELECTOR_PROPERTY_PREFIX = "selector-";
if($tw.utils.startsWith(name,SELECTOR_PROPERTY_PREFIX)) {
selectors.push(event.paramObject[name]);
}
});
if(event.paramObject["selector-fallback"]) {
selectors.push(event.paramObject["selector-fallback"]);
}
$tw.dynannotate.elementSpotlight.shineSpotlight(selectors);
});
};
})();

View File

@ -42,3 +42,20 @@ tags: [[$:/tags/Stylesheet]]
background: #ffa;
padding: 1em;
}
.tc-dynannotate-spotlight-wrapper {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1000;
pointer-events: none;
}
.tc-dynannotate-spotlight {
position: absolute;
height: 100%;
width: 100%;
display: none;
}