diff --git a/plugins/tiddlywiki/dynannotate/docs/readme.tid b/plugins/tiddlywiki/dynannotate/docs/readme.tid
index 83875ca4d..2cd9ccd3e 100644
--- a/plugins/tiddlywiki/dynannotate/docs/readme.tid
+++ b/plugins/tiddlywiki/dynannotate/docs/readme.tid
@@ -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.
diff --git a/plugins/tiddlywiki/dynannotate/examples/spotlight.tid b/plugins/tiddlywiki/dynannotate/examples/spotlight.tid
new file mode 100644
index 000000000..6e6316b1b
--- /dev/null
+++ b/plugins/tiddlywiki/dynannotate/examples/spotlight.tid
@@ -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
+
+
+
+!! Spotlighting an Image
+
+
+
+<
+<$action-sendmessage $message="tm-spotlight-element" selector=".tc-dynannotate-spotlight-image-example"/>
+Spotlight this image
+$button>
+
+{{$:/core/images/globe}}
+
+""">>
+
+
+
+!! Spotlighting a Button
+
+
+
+<
+<$action-sendmessage $message="tm-spotlight-element" selector=".tc-dynannotate-spotlight-button-example"/>
+Spotlight this button
+$button>
+""">>
+
+
+
+!! Spotlighting a Text Area
+
+
+
+<
+<$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"/>
+
+""">>
+
+
+
+!! 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.
+
+
+
+<
+<$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>
+""">>
\ No newline at end of file
diff --git a/plugins/tiddlywiki/dynannotate/modules/element-spotlight.js b/plugins/tiddlywiki/dynannotate/modules/element-spotlight.js
new file mode 100644
index 000000000..cbf4e4679
--- /dev/null
+++ b/plugins/tiddlywiki/dynannotate/modules/element-spotlight.js
@@ -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;
+
+})();
diff --git a/plugins/tiddlywiki/dynannotate/modules/startup.js b/plugins/tiddlywiki/dynannotate/modules/startup.js
index a2fe6a7f6..4324c5893 100644
--- a/plugins/tiddlywiki/dynannotate/modules/startup.js
+++ b/plugins/tiddlywiki/dynannotate/modules/startup.js
@@ -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);
+ });
};
})();
diff --git a/plugins/tiddlywiki/dynannotate/styles.tid b/plugins/tiddlywiki/dynannotate/styles.tid
index cd635eed5..305f7b16a 100644
--- a/plugins/tiddlywiki/dynannotate/styles.tid
+++ b/plugins/tiddlywiki/dynannotate/styles.tid
@@ -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;
+}