1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2026-01-22 10:54:46 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
jeremy@jermolene.com
a79644bff3 Fix new selection tracker to return relative coordinates 2022-06-17 16:37:31 +01:00
jeremy@jermolene.com
9e8e93cbb7 Docs update 2022-04-08 12:29:59 +01:00
jeremy@jermolene.com
495c42c7cf Introduce new selection tracker 2022-03-29 17:12:48 +01:00
jeremy@jermolene.com
ffea61b345 Merge branch 'master' into dynannotate-improvements 2022-03-24 09:14:08 +00:00
jeremy@jermolene.com
5f4dc2a5fe Initial Commit 2022-03-11 10:25:16 +00:00
7 changed files with 620 additions and 99 deletions

View File

@@ -0,0 +1,167 @@
/*\
title: $:/core/modules/filters/json-ops.js
type: application/javascript
module-type: filteroperator
Filter operators for JSON operations
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports["getjson"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = options.wiki.getTiddlerDataCached(title);
if(data) {
var item = getDataItemValueAsStrings(data,operator.operands);
if(item !== undefined) {
results.push.apply(results,item);
}
}
});
return results;
};
exports["indexesjson"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = options.wiki.getTiddlerDataCached(title);
if(data) {
var item = getDataItemKeysAsStrings(data,operator.operands);
if(item !== undefined) {
results.push.apply(results,item);
}
}
});
return results;
};
exports["typejson"] = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var data = options.wiki.getTiddlerDataCached(title);
if(data) {
var item = getDataItemType(data,operator.operands);
if(item !== undefined) {
results.push(item);
}
}
});
return results;
};
/*
Given a JSON data structure and an array of index strings, return an array of the string representation of the values at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
function getDataItemValueAsStrings(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item as a string
return convertDataItemValueToStrings(item);
}
/*
Given a JSON data structure and an array of index strings, return an array of the string representation of the keys of the item at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
function getDataItemKeysAsStrings(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item keys as a string
return convertDataItemKeysToStrings(item);
}
/*
Return an array of the string representation of the values of a data item, or "undefined" if the item is undefined
*/
function convertDataItemValueToStrings(item) {
// Return the item as a string
if(item === undefined) {
return item;
}
if(typeof item === "object") {
if(item === null) {
return ["null"];
}
var results = [];
if($tw.utils.isArray(item)) {
$tw.utils.each(item,function(value) {
results.push.apply(results,convertDataItemValueToStrings(value));
});
return results;
} else {
$tw.utils.each(Object.keys(item).sort(),function(key) {
results.push.apply(results,convertDataItemValueToStrings(item[key]));
});
return results;
}
}
return [item.toString()];
}
/*
Return an array of the string representation of the keys of a data item, or "undefined" if the item is undefined
*/
function convertDataItemKeysToStrings(item) {
// Return the item as a string
if(item === undefined) {
return item;
} else if(typeof item === "object") {
if(item === null) {
return [];
}
var results = [];
if($tw.utils.isArray(item)) {
for(var i=0; i<item.length; i++) {
results.push(i.toString());
}
return results;
} else {
$tw.utils.each(Object.keys(item).sort(),function(key) {
results.push(key);
});
return results;
}
}
return [];
}
function getDataItemType(data,indexes) {
// Get the item
var item = getDataItem(data,indexes);
// Return the item type
if(item === undefined) {
return item;
} else if(item === null) {
return "null";
} else if($tw.utils.isArray(item)) {
return "array";
} else if(typeof item === "object") {
return "object";
} else {
return typeof item;
}
}
/*
Given a JSON data structure and an array of index strings, return the value at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
function getDataItem(data,indexes) {
if(indexes.length === 0 || (indexes.length === 1 && indexes[0] === "")) {
return data;
}
// Get the item
var item = data;
for(var i=0; i<indexes.length; i++) {
if(item !== undefined) {
item = item[indexes[i]];
}
}
return item;
}
})();

View File

@@ -0,0 +1,82 @@
/*\
title: test-json-filters.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the JSON filters.
\*/
(function(){
/* jslint node: true, browser: true */
/* eslint-env node, browser, jasmine */
/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/
/* global $tw, require */
"use strict";
describe("json filter tests", function() {
var wiki = new $tw.Wiki();
var tiddlers = [{
title: "First",
text: '{"a":"one","b":"","c":1.618,"d": {"e": "four","f": ["five","six",true,false,null]}}',
type: "application/json"
},{
title: "Second",
text: '["une","deux","trois"]',
type: "application/json"
}];
wiki.addTiddlers(tiddlers);
it("should support the getindex operator", function() {
expect(wiki.filterTiddlers("[[First]getindex[b]]")).toEqual([]);
});
it("should support the getjson operator", function() {
expect(wiki.filterTiddlers("[[First]getjson[]]")).toEqual(["one","","1.618","four","five","six","true","false","null"]);
expect(wiki.filterTiddlers("[[First]getjson[a]]")).toEqual(["one"]);
expect(wiki.filterTiddlers("[[First]getjson[b]]")).toEqual([""]);
expect(wiki.filterTiddlers("[[First]getjson[d]]")).toEqual(["four","five","six","true","false","null"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[e]]")).toEqual(["four"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f]]")).toEqual(["five","six","true","false","null"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f],[0]]")).toEqual(["five"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f],[1]]")).toEqual(["six"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[[First]getjson[d],[f],[4]]")).toEqual(["null"]);
});
it("should support the indexesjson operator", function() {
expect(wiki.filterTiddlers("[[Second]indexesjson[]]")).toEqual(["0","1","2"]);
expect(wiki.filterTiddlers("[[First]indexesjson[]]")).toEqual(["a","b","c","d"]);
expect(wiki.filterTiddlers("[[First]indexesjson[a]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[b]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d]]")).toEqual(["e","f"]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[e]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f]]")).toEqual(["0","1","2","3","4"]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f],[0]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f],[1]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f],[2]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f],[3]]")).toEqual([]);
expect(wiki.filterTiddlers("[[First]indexesjson[d],[f],[4]]")).toEqual([]);
});
it("should support the typejson operator", function() {
expect(wiki.filterTiddlers("[[First]typejson[]]")).toEqual(["object"]);
expect(wiki.filterTiddlers("[[First]typejson[a]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[[First]typejson[b]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[[First]typejson[c]]")).toEqual(["number"]);
expect(wiki.filterTiddlers("[[First]typejson[d]]")).toEqual(["object"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[e]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f]]")).toEqual(["array"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f],[0]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f],[1]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f],[2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f],[3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[[First]typejson[d],[f],[4]]")).toEqual(["null"]);
});
});
})();

View File

@@ -1,12 +1,10 @@
title: $:/plugins/tiddlywiki/dynannotate/readme
The ''Dynannotate'' plugin allows annotations on textual content to be created and displayed. It has three components:
The ''Dynannotate'' plugin allows annotations on textual content to be created and displayed. It has several components:
* The dynannotate widget overlays clickable textual annotations, search highlights and search snippets on the content that it contains
* The selection tracker displays a popup that tracks the selection, and keeps track of the selected text. It also tracks a prefix and suffix that can be used to disambiguate the selected text within the container
* The `<$action-popup>` widget is used for some specialised popup switching in the demo
''Note that the TiddlyWiki core plugin __Dynaview__ is required for correct operation of __Dynannotate__''
* 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
!! Dynannotate Widget
@@ -32,6 +30,10 @@ The `<$dynannotate>` widget uses the selection tracker to support a popup that d
|searchCaseSensitive |"no" (default) for a case insensitive search, or "yes" for a case sensitive search |
|searchClass |Optional CSS class to be added to search overlays |
|snippetContextLength |Optional length of search result contextual prefix/suffix |
The following attributes are only used with the legacy selection tracker:
|!Attribute |!Description |
|selection |Tiddler to which the currently selected text should be dynamically saved |
|selectionPrefix |Tiddler to which up to 50 characters preceding the currently selected text should be dynamically saved |
|selectionSuffix |Tiddler to which up to 50 characters succeeding the currently selected text should be dynamically saved |
@@ -91,9 +93,71 @@ An annotation tiddler is a tiddler describing an annotation to be overlaid over
Note that using the `annotate-tiddler` field to associate an annotation with the annotated tiddler is a lightweight convention employed by the examples; it isn't actually required by any of the JavaScript code. Thus authors can experiment with other techniques for recording the association.
!! Selection Tracker
!! Selection Trackers
The selection tracker is incorporated within the `<$dynannotate>` widget, but it can be used independently for specialised applications.
The following configuration tiddlers can be used to control whether the selection trackers are enabled when the following configuration tiddlers are set to ''yes'' (the default).
* $:/config/Dynannotate/SelectionTracker/Enable for the main selection tracker
* $:/config/Dynannotate/LegacySelectionTracker/Enable for the legacy selection tracker
Both selection trackers are enabled by default.
!!! Main Selection Tracker
The selection tracker triggers an action string whenever the browser text selection changes. The actions are delayed until the selection has not changed for 500ms (this means that the selection tracker is only triggered when the user pauses after completing a selection, and is not continuously invoked as the user drags the selection).
The selection tracker works within DOM subtrees that have the following structure:
* The outer wrapper element has the attribute `data-selection-action-title` containing the title of the tiddler containing the action string to be invoked when the selection changes
* Each child element of the outer element must have a unique `id` attribute to identify it
```
<div data-selection-action-title="{tiddler title}">
<div id="{title}">
Content text
</div>
...
</div>
```
The action string is invoked with the following variables:
|!Variable |!Description |
|`selection` |A JSON object representing the selection (see below) |
|`dom-*` |All DOM attributes of the outer wrapper node are made available as variables, with the prefix `dom-` |
|`tv-selection-posx` |X position of the selection in pixels |
|`tv-selection-posy` |Y position of the selection in pixels |
|`tv-selection-width` |Width of the selection in pixels |
|`tv-selection-height` |Height of the selection in pixels |
|`tv-selection-coords` |A co-ordinate string that can be used with the ActionPopupWidget to trigger a popup at the selection |
The JSON representation of the selection is as follows:
```
{
"chunks": [
{
"id": "id-of-first-chunk-of-selection",
"text": "text-of-first-chunk-of-selection",
"prefix": "optional-prefix-before-first-chunk-of-selection",
"suffix": "optional-suffix-after-last-chunk-of-selection"
}
...
],
"variables": {
<variables listed above>
}
}
```
Notes:
* Only the first chunk of the selection may have a "prefix" field which will contain any text at the start of the chunk preceding the selection
* Only the last chunk of the selection may have a "suffix" field which will contain any text at the end of the chunk following the selection
!!! Legacy Selection Tracker
The selection tracker is incorporated within the `<$dynannotate>` widget via the ''selection'', ''selectionPrefix'' ''selectionSuffix'' and ''selectionPopup'' attributes. It can be used independently for specialised applications.
Each selection container is marked with the class `tc-dynannotate-selection-container`, and should contain the following attributes:

View File

@@ -183,7 +183,16 @@ DynannotateWidget.prototype.applyAnnotations = function() {
// We'll dynamically build the click event handler so that we can reuse it
var clickHandlerFn = function(title) {
return function(event,domOverlay,modifierKey) {
self.invokeActionString(self.getAttribute("actions"),self,event,{annotationTiddler: title, modifier: modifierKey});
var bounds = domOverlay.getBoundingClientRect();
self.invokeActionString(self.getAttribute("actions"),self,event,{
annotationTiddler: title,
modifier: modifierKey,
"tv-selection-posx": (bounds.left).toString(),
"tv-selection-posy": (bounds.top).toString(),
"tv-selection-width": (bounds.width).toString(),
"tv-selection-height": (bounds.height).toString(),
"tv-selection-coords": "(" + bounds.left + "," + bounds.top + "," + bounds.width + "," + bounds.height + ")"
});
if(self.hasAttribute("popup")) {
$tw.popup.triggerPopup({
domNode: domOverlay,

View File

@@ -0,0 +1,104 @@
/*\
title: $:/plugins/tiddlywiki/dynannotate/legacy-selection-tracker.js
type: application/javascript
module-type: library
Legacy version of the dyannotate background daemon to track the selection
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var TextMap = require("$:/plugins/tiddlywiki/dynannotate/textmap.js").TextMap;
function LegacySelectionTracker(wiki,options) {
options = options || {};
var self = this;
this.wiki = wiki;
this.allowBlankSelectionPopup = options.allowBlankSelectionPopup;
this.selectionPopupTitle = null;
document.addEventListener("selectionchange",function(event) {
var selection = document.getSelection();
if(selection && (selection.type === "Range" || (self.allowBlankSelectionPopup && !self.selectionPopupTitle))) {
// Look for the selection containers for each of the two ends of the selection
var anchorContainer = self.findSelectionContainer(selection.anchorNode),
focusContainer = self.findSelectionContainer(selection.focusNode);
// If either end of the selection then we ignore it
if(!!anchorContainer || !!focusContainer) {
var selectionRange = selection.getRangeAt(0);
// Check for the selection spilling outside the starting container
if((anchorContainer !== focusContainer) || (selectionRange.startContainer.nodeType !== Node.TEXT_NODE && selectionRange.endContainer.nodeType !== Node.TEXT_NODE)) {
if(self.selectionPopupTitle) {
self.wiki.deleteTiddler(self.selectionPopupTitle);
self.selectionPopupTitle = null;
}
} else {
self.selectionSaveTitle = anchorContainer.getAttribute("data-annotation-selection-save");
self.selectionPrefixSaveTitle = anchorContainer.getAttribute("data-annotation-selection-prefix-save");
self.selectionSuffixSaveTitle = anchorContainer.getAttribute("data-annotation-selection-suffix-save");
self.selectionPopupTitle = anchorContainer.getAttribute("data-annotation-selection-popup");
// The selection is a range so we trigger the popup
if(self.selectionPopupTitle) {
var selectionRectangle = selectionRange.getBoundingClientRect(),
trackingRectangle = anchorContainer.getBoundingClientRect();
$tw.popup.triggerPopup({
domNode: null,
domNodeRect: {
left: selectionRectangle.left - trackingRectangle.left,
top: selectionRectangle.top - trackingRectangle.top,
width: selectionRectangle.width,
height: selectionRectangle.height
},
force: true,
floating: true,
title: self.selectionPopupTitle,
wiki: self.wiki
});
}
// Write the selection text to the specified tiddler
if(self.selectionSaveTitle) {
// Note that selection.toString() normalizes whitespace but selection.getRangeAt(0).toString() does not
var text = selectionRange.toString();
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionSaveTitle, text: text}));
// Build a textmap of the container so that we can find the prefix and suffix
var textMap = new TextMap(anchorContainer);
// Find the selection start in the text map and hence extract the prefix and suffix
var context = textMap.extractContext(selectionRange.startContainer,selectionRange.startOffset,text);
// Save the prefix and suffix
if(context) {
if(self.selectionPrefixSaveTitle) {
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionPrefixSaveTitle, text: context.prefix}));
}
if(self.selectionSuffixSaveTitle) {
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionSuffixSaveTitle, text: context.suffix}));
}
}
}
}
}
} else {
// If the selection is a caret we clear any active popup
if(self.selectionPopupTitle) {
self.wiki.deleteTiddler(self.selectionPopupTitle);
self.selectionPopupTitle = null;
}
}
});
}
LegacySelectionTracker.prototype.findSelectionContainer = function findSelectionContainer(domNode) {
if(domNode && domNode.nodeType === Node.ELEMENT_NODE && domNode.classList.contains("tc-dynannotation-selection-container")) {
return domNode;
}
if(domNode && domNode.parentNode) {
return findSelectionContainer(domNode.parentNode);
}
return null;
};
exports.LegacySelectionTracker = LegacySelectionTracker;
})();

View File

@@ -3,7 +3,7 @@ title: $:/plugins/tiddlywiki/dynannotate/selection-tracker.js
type: application/javascript
module-type: startup
Dyannotate background daemon to track the selection
Background daemon to track the selection
\*/
(function(){
@@ -12,105 +12,160 @@ Dyannotate background daemon to track the selection
/*global $tw: false */
"use strict";
// Export name and synchronous status
exports.name = "dyannotate-startup";
exports.platforms = ["browser"];
exports.after = ["render"];
exports.synchronous = true;
var TextMap = require("$:/plugins/tiddlywiki/dynannotate/textmap.js").TextMap;
exports.startup = function() {
$tw.dynannotate = {
selectionTracker: new SelectionTracker($tw.wiki,{
allowBlankSelectionPopup: true
})
};
};
function SelectionTracker(wiki,options) {
options = options || {};
var self = this;
this.wiki = wiki;
this.allowBlankSelectionPopup = options.allowBlankSelectionPopup;
this.selectionPopupTitle = null;
var timerId = null;
document.addEventListener("selectionchange",function(event) {
var selection = document.getSelection();
if(selection && (selection.type === "Range" || (self.allowBlankSelectionPopup && !self.selectionPopupTitle))) {
// Look for the selection containers for each of the two ends of the selection
var anchorContainer = self.findSelectionContainer(selection.anchorNode),
focusContainer = self.findSelectionContainer(selection.focusNode);
// If either end of the selection then we ignore it
if(!!anchorContainer || !!focusContainer) {
var selectionRange = selection.getRangeAt(0);
// Check for the selection spilling outside the starting container
if((anchorContainer !== focusContainer) || (selectionRange.startContainer.nodeType !== Node.TEXT_NODE && selectionRange.endContainer.nodeType !== Node.TEXT_NODE)) {
if(self.selectionPopupTitle) {
self.wiki.deleteTiddler(self.selectionPopupTitle);
self.selectionPopupTitle = null;
}
} else {
self.selectionSaveTitle = anchorContainer.getAttribute("data-annotation-selection-save");
self.selectionPrefixSaveTitle = anchorContainer.getAttribute("data-annotation-selection-prefix-save");
self.selectionSuffixSaveTitle = anchorContainer.getAttribute("data-annotation-selection-suffix-save");
self.selectionPopupTitle = anchorContainer.getAttribute("data-annotation-selection-popup");
// The selection is a range so we trigger the popup
if(self.selectionPopupTitle) {
var selectionRectangle = selectionRange.getBoundingClientRect(),
trackingRectangle = anchorContainer.getBoundingClientRect();
$tw.popup.triggerPopup({
domNode: null,
domNodeRect: {
left: selectionRectangle.left - trackingRectangle.left,
top: selectionRectangle.top - trackingRectangle.top,
width: selectionRectangle.width,
height: selectionRectangle.height
},
force: true,
floating: true,
title: self.selectionPopupTitle,
wiki: self.wiki
});
}
// Write the selection text to the specified tiddler
if(self.selectionSaveTitle) {
// Note that selection.toString() normalizes whitespace but selection.getRangeAt(0).toString() does not
var text = selectionRange.toString();
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionSaveTitle, text: text}));
// Build a textmap of the container so that we can find the prefix and suffix
var textMap = new TextMap(anchorContainer);
// Find the selection start in the text map and hence extract the prefix and suffix
var context = textMap.extractContext(selectionRange.startContainer,selectionRange.startOffset,text);
// Save the prefix and suffix
if(context) {
if(self.selectionPrefixSaveTitle) {
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionPrefixSaveTitle, text: context.prefix}));
}
if(self.selectionSuffixSaveTitle) {
self.wiki.addTiddler(new $tw.Tiddler({title: self.selectionSuffixSaveTitle, text: context.suffix}));
}
}
}
}
}
} else {
// If the selection is a caret we clear any active popup
if(self.selectionPopupTitle) {
self.wiki.deleteTiddler(self.selectionPopupTitle);
self.selectionPopupTitle = null;
}
if(timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(function() {
timerId = null;
self.handleSelectionChange();
},500);
});
}
SelectionTracker.prototype.findSelectionContainer = function findSelectionContainer(domNode) {
if(domNode && domNode.nodeType === Node.ELEMENT_NODE && domNode.classList.contains("tc-dynannotation-selection-container")) {
return domNode;
SelectionTracker.prototype.handleSelectionChange = function() {
var selection = document.getSelection();
if(selection && selection.type === "Range") {
// Helper to get the tiddler title corresponding to a chunk container
var getIdOfContainer = function(domNode) {
return domNode.id;
}
// Get information about the selection anchor and focus
var getSelectionInfo = function(targetDomNode,targetOffset) {
// Find the chunk container node
var domNode = targetDomNode;
if(domNode.nodeType === Node.TEXT_NODE) {
domNode = domNode.parentNode;
}
var container = domNode.closest(".dynannotate-chunk");
if(!container) {
return null;
}
// Find the index of the container within the child nodes of its parent
var childNodeIndex = Array.prototype.indexOf.call(container.parentNode.childNodes,container);
// Walk through the chunk collecting the text before and after the specified domNode and offset
var beforeText = null, afterText = [];
var splitTextResult = function() {
beforeText = afterText;
afterText = [];
},
processNode = function(domNode) {
// Check for a text node
if(domNode.nodeType === Node.TEXT_NODE) {
// If this is the target node then perform the split
if(domNode === targetDomNode) {
afterText.push(domNode.textContent.substring(0,targetOffset));
splitTextResult();
afterText.push(domNode.textContent.substring(targetOffset));
} else {
afterText.push(domNode.textContent);
}
} else {
// Process the child nodes
$tw.utils.each(domNode.childNodes,function(childNode,childNodeIndex) {
// Check whether we need to split on this child node
if(domNode === targetDomNode && childNodeIndex === targetOffset) {
splitTextResult();
}
processNode(childNode);
});
}
};
processNode(container);
if(beforeText === null) {
splitTextResult();
}
// Return results
return {
container: container,
childNodeIndex: childNodeIndex,
beforeText: beforeText.join(""),
afterText: afterText.join("")
}
}
var anchor = getSelectionInfo(selection.anchorNode,selection.anchorOffset),
focus = getSelectionInfo(selection.focusNode,selection.focusOffset);
// Check that the containers share a parent
if(anchor && focus && anchor.container.parentNode === focus.container.parentNode) {
// Make sure that the anchor is before the focus
if((anchor.childNodeIndex > focus.childNodeIndex) || (anchor.container === focus.container && anchor.beforeText.length > focus.beforeText.length)) {
var temp = anchor;
anchor = focus;
focus = temp;
}
var chunks = [];
// Check for the selection being all in one chunk
if(anchor.container === focus.container) {
chunks.push({
id: getIdOfContainer(anchor.container),
prefix: anchor.beforeText,
text: anchor.afterText.substring(0,anchor.afterText.length - focus.afterText.length),
suffix: focus.afterText
});
} else {
// We span two or more chunks
chunks.push({
id: getIdOfContainer(anchor.container),
prefix: anchor.beforeText,
text: anchor.afterText
});
// Get the titles and text of the intervening tiddlers
var domNode;
if(anchor.container !== focus.container) {
domNode = anchor.container.nextElementSibling;
while(domNode && domNode !== focus.container) {
chunks.push({
id: getIdOfContainer(domNode),
text: domNode.textContent
});
domNode = domNode.nextElementSibling;
}
}
chunks.push({
id: getIdOfContainer(focus.container),
text: focus.beforeText,
suffix: focus.afterText
});
}
// Get the title of the tiddler containing the actions to be executed
var actionsTiddler = anchor.container.parentNode.getAttribute("data-selection-actions-title");
// Assemble the variables to be passed to the action
var variables = {};
// Get the bounds of the container and the selection
var selectionRectangle = selection.getRangeAt(0).getBoundingClientRect(),
offsetParentRectangle = anchor.container.offsetParent.getBoundingClientRect();
variables["tv-selection-posx"] = (selectionRectangle.left - offsetParentRectangle.left).toString();
variables["tv-selection-posy"] = (selectionRectangle.top - offsetParentRectangle.top).toString();
variables["tv-selection-width"] = (selectionRectangle.width).toString();
variables["tv-selection-height"] = (selectionRectangle.height).toString();
variables["tv-selection-coords"] = "(" + variables["tv-selection-posx"] + "," + variables["tv-selection-posy"] + "," + variables["tv-selection-width"] + "," + variables["tv-selection-height"] + ")";
// Collect the attributes from the container
$tw.utils.each(anchor.container.parentNode.attributes,function(attribute) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
// Action the selection
this.performSelectionActions(chunks,variables,actionsTiddler);
}
}
if(domNode && domNode.parentNode) {
return findSelectionContainer(domNode.parentNode);
}
return null;
};
SelectionTracker.prototype.performSelectionActions = function(chunks,variables,actionsTiddler) {
// Invoke the actions, passing the extract tiddler title as a variable
if(actionsTiddler) {
var actions = $tw.wiki.getTiddlerText(actionsTiddler)
if(actions) {
var selection = JSON.stringify({chunks: chunks,variables: variables});
$tw.rootWidget.invokeActionString(actions,undefined,undefined,$tw.utils.extend({},variables,{selection: selection}));
}
}
};
exports.SelectionTracker = SelectionTracker;
})();

View File

@@ -0,0 +1,40 @@
/*\
title: $:/plugins/tiddlywiki/dynannotate/startup.js
type: application/javascript
module-type: startup
Startup the dyannotate background daemon to track the selection
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
// Export name and synchronous status
exports.name = "dyannotate-startup";
exports.platforms = ["browser"];
exports.after = ["render"];
exports.synchronous = true;
var CONFIG_SELECTION_TRACKER_TITLE = "$:/config/Dynannotate/SelectionTracker/Enable",
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;
exports.startup = function() {
$tw.dynannotate = {};
if($tw.wiki.getTiddlerText(CONFIG_SELECTION_TRACKER_TITLE,"yes") === "yes") {
$tw.dynannotate.selectionTracker = new SelectionTracker($tw.wiki);
}
if($tw.wiki.getTiddlerText(CONFIG_LEGACY_SELECTION_TRACKER_TITLE,"yes") === "yes") {
$tw.dynannotate.legacySelectionTracker = new LegacySelectionTracker($tw.wiki,{
allowBlankSelectionPopup: true
});
}
};
})();