mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2024-12-24 00:50:28 +00:00
Introducing "Dynannotate" plugin for overlaying annotations
This commit is contained in:
parent
592922d399
commit
9b48a1c829
@ -12,6 +12,7 @@
|
||||
"tiddlywiki/savetrail",
|
||||
"tiddlywiki/external-attachments",
|
||||
"tiddlywiki/dynaview",
|
||||
"tiddlywiki/dynannotate",
|
||||
"tiddlywiki/codemirror",
|
||||
"tiddlywiki/comments",
|
||||
"tiddlywiki/menubar"
|
||||
|
33
plugins/tiddlywiki/dynannotate/docs/history.tid
Normal file
33
plugins/tiddlywiki/dynannotate/docs/history.tid
Normal file
@ -0,0 +1,33 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/history
|
||||
|
||||
!! v0.0.5
|
||||
|
||||
* Added support for displaying search snippets
|
||||
* Fixed animated popups in the combined demo
|
||||
* Added minimum length for dynannotate search string
|
||||
* Added custom classes for search overlays
|
||||
* Fix crash for malformed regexps
|
||||
|
||||
!! v0.0.4
|
||||
|
||||
* Fix crash with Chrome search-in-page
|
||||
* Improve docs
|
||||
|
||||
!! v0.0.3
|
||||
|
||||
* Add support for showing the selection popup even for a zero length selection (ie clicking within the text without dragging)
|
||||
* Add support for searching
|
||||
* Refresh when browser or wrapper resizes
|
||||
** Note that Dynannotate now requires the core TiddlyWiki plugin Dynaview
|
||||
* Fixes problem with selections within HTML textareas or inputs
|
||||
* Improved presentation of examples
|
||||
|
||||
!! v0.0.2
|
||||
|
||||
* Adds support for Mobile Safari
|
||||
* Split demo into multiple chunks
|
||||
* Only show the selection popup when the selection is entirely within a selection container
|
||||
|
||||
!! v0.0.1
|
||||
|
||||
Initial release
|
108
plugins/tiddlywiki/dynannotate/docs/readme.tid
Normal file
108
plugins/tiddlywiki/dynannotate/docs/readme.tid
Normal file
@ -0,0 +1,108 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/readme
|
||||
|
||||
The ''Dynannotate'' plugin allows annotations on textual content to be created and displayed. It has three 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__''
|
||||
|
||||
!! Dynannotate Widget
|
||||
|
||||
The attributes of the `<$dynannotate>` widget describe annotations to be overlaid over the text contained within its child widgets. A single annotation can be directly applied using the attributes or multiple annotations can be applied by providing a filter identifying the "annotation tiddlers" that specify each annotation.
|
||||
|
||||
The content of the `<$dynannotate>` widget should not contain HTML `<input>` or `<textarea>` text editing elements (and therefore should not contain TiddlyWiki's `<$edit-text>` widget)
|
||||
|
||||
The `<$dynannotate>` widget uses the selection tracker to support a popup that dynamically tracks selected text within it.
|
||||
|
||||
!!! Attributes
|
||||
|
||||
|!Attribute |!Description |
|
||||
|target |Optional text to be annotated |
|
||||
|targetPrefix |Optional prefix text to disambiguate the target |
|
||||
|targetSuffix |Optional suffix text to disambiguate the target |
|
||||
|filter |Filter identifying the annotation tiddlers applying to this content (see below) |
|
||||
|actions |Action string to be executed when an annotation is clicked. The variable `annotationTiddler` contains the title of the tiddler corresponding to the annotation that was clicked, and the variable `modifierKey` contains "ctrl", "shift", "ctrl-shift", "normal" according to which modifier keys were pressed |
|
||||
|popup |Popup state tiddler to be used to trigger a popup when an annotation is clicked |
|
||||
|search |Search text to be highlighted within the widget |
|
||||
|searchDisplay |"overlay" or "snippet" (see below) |
|
||||
|searchMode |"normal" (default), "regexp" or "whitespace" (see below) |
|
||||
|searchMinLength |Optional minimum length of search string |
|
||||
|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 |
|
||||
|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 |
|
||||
|selectionPopup |Popup state tiddler to be used to trigger a popup when text is selected |
|
||||
|
||||
The values supported by the `searchDisplay` attribute are:
|
||||
|
||||
* `overlay` - display search results as overlays over the contained text
|
||||
* `snippet` - display search results as a sequence of highlighted snippets, and the original text is hidden. Selecting this option therefore disables the annotation functionality
|
||||
|
||||
The search modes supported by the `searchMode` attribute are:
|
||||
|
||||
* `normal` - a literal string of plain text to match
|
||||
* `regexp` - a JavaScript-style regular expression (without the quoting backslashes and flags)
|
||||
* `whitespace` - a literal string to match while normalising runs of whitespace. This allows `a. b` to match `a. b`
|
||||
|
||||
When the selection popup is triggered, the currently selected text can be found in the tiddler named in the `selection` attribute, with the disambiguating prefix and suffix in the tiddlers named in the `selectionPrefix` and `selectionPopup` tiddlers. Note that the selection text will be an empty string if the selection popup was triggered in response to a click (ie zero width selection).
|
||||
|
||||
Here's a simple example that highlights the first occurrence of the word "ut" within the text contained within it:
|
||||
|
||||
```
|
||||
<$dynannotate target="ut">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
|
||||
</$dynannotate>
|
||||
```
|
||||
|
||||
A prefix and/or suffix can be specified to disambiguate the annotation. For example, here we target the second occurrence of the word "ut":
|
||||
|
||||
```
|
||||
<$dynannotate target="ut" targetPrefix="ullamco laboris nisi " targetSuffix=" aliquip ex ea commodo consequat">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum
|
||||
</$dynannotate>
|
||||
```
|
||||
|
||||
The widget works by scanning the rendered text of its content, so it works even if the text is built dynamically:
|
||||
|
||||
```
|
||||
<$dynannotate target="HelloThere">
|
||||
<<list-links "[tag[Work]]">>
|
||||
</$dynannotate>
|
||||
```
|
||||
|
||||
!!! Annotation Tiddlers
|
||||
|
||||
An annotation tiddler is a tiddler describing an annotation to be overlaid over another tiddler. Their fields are used as follows:
|
||||
|
||||
|!Field |!Description |
|
||||
|title |By convention the prefix `$:/annotations/<username>/` is used, but any title can be used |
|
||||
|text |The text of the annotation |
|
||||
|created, creator, modified, modifier |As per TiddlyWiki normal behaviour |
|
||||
|annotate-tiddler |The title of the target tiddler being annotated (optional, see below) |
|
||||
|annotate-text |The text being annotated in the target tiddler |
|
||||
|annotate-prefix |Optional prefix to disambiguate the target annotation |
|
||||
|annotate-suffix |Optional suffix to disambiguate the target annotation |
|
||||
|annotate-colour |CSS colour for the annotation (defaults to `rgba(255,255,0,0.3)`) |
|
||||
|annotate-blend-mode |CSS [[mix blend mode|https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode]] for the annotation (defaults to `multiply`) |
|
||||
|
||||
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
|
||||
|
||||
The selection tracker is incorporated within the `<$dynannotate>` widget, but 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:
|
||||
|
||||
* `data-annotation-selection-save`: title of tiddler to which the selected text should be saved
|
||||
* `data-annotation-selection-prefix-save`: title of tiddler to which up to 50 characters preceding the currently selected text should be dynamically saved
|
||||
* `data-annotation-selection-suffix-save`: title of tiddler to which up to 50 characters succeeding the currently selected text should be dynamically saved
|
||||
* `data-annotation-selection-popup`: title of state tiddler used to trigger the selection popup
|
||||
|
||||
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
|
||||
|
111
plugins/tiddlywiki/dynannotate/examples/combined.tid
Normal file
111
plugins/tiddlywiki/dynannotate/examples/combined.tid
Normal file
@ -0,0 +1,111 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/examples/combined
|
||||
tags: $:/tags/dynannotateExamples
|
||||
caption: Combined
|
||||
|
||||
\define click-annotation-actions()
|
||||
<$action-setfield $tiddler="$:/temp/dynannotate/demo/annotation-title" $value=<<annotationTiddler>>/>
|
||||
\end
|
||||
|
||||
\define create-annotation-actions()
|
||||
<$action-createtiddler
|
||||
$basetitle="$:/plugins/tiddlywiki/dynannotate/demo-annotation"
|
||||
$savetitle={{{ [<chunk>addprefix[$:/state/dynannotate/temp-save-title/]] }}}
|
||||
annotate-tiddler=<<chunk>>
|
||||
annotate-text=<<text>>
|
||||
annotate-prefix=<<prefix>>
|
||||
annotate-suffix=<<suffix>>
|
||||
annotate-colour=<<colour>>
|
||||
/>
|
||||
<$set name="popup-coords" value={{{ [<chunk>addprefix[$:/state/dynannotate/popup-selection/]get[text]] }}}>
|
||||
<$action-deletetiddler $tiddler={{{ [<chunk>addprefix[$:/state/dynannotate/popup-selection/]] }}}/>
|
||||
<$action-setfield $tiddler="$:/temp/dynannotate/demo/annotation-title" $value={{{ [<chunk>addprefix[$:/state/dynannotate/temp-save-title/]get[text]] }}}/>
|
||||
<$action-popup $state={{{ [<chunk>addprefix[$:/state/dynannotate/popup-annotation/]] }}} $coords=<<popup-coords>>/>
|
||||
</$set>
|
||||
\end
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
This example combines many of the features of the dynannotate plugin:
|
||||
|
||||
* using annotation tiddlers to store the details of each annotation
|
||||
* triggering actions when the annotations are clicked
|
||||
* attaching a popup to the annotations
|
||||
* tracking the selection with another popup
|
||||
|
||||
See the [[source|$:/plugins/tiddlywiki/dynannotate/examples/combined]] for details
|
||||
|
||||
</div>
|
||||
|
||||
Search: <$edit-text tiddler="$:/temp/search" tag="input"/>
|
||||
|
||||
<$list filter="[all[tiddlers+shadows]tag[DynannotateDemo]sort[title]]" variable="chunk">
|
||||
<div style="position:relative;"><!-- Needed for the popups to work -->
|
||||
<$dynannotate
|
||||
filter="[all[shadows+tiddlers]!has[draft.of]annotate-tiddler<chunk>]"
|
||||
actions=<<click-annotation-actions>>
|
||||
popup={{{ [<chunk>addprefix[$:/state/dynannotate/popup-annotation/]] }}}
|
||||
selection={{{ [<chunk>addprefix[$:/state/dynannotate/selection/]] }}}
|
||||
selectionPrefix={{{ [<chunk>addprefix[$:/state/dynannotate/selection-prefix/]] }}}
|
||||
selectionSuffix={{{ [<chunk>addprefix[$:/state/dynannotate/selection-suffix/]] }}}
|
||||
selectionPopup={{{ [<chunk>addprefix[$:/state/dynannotate/popup-selection/]] }}}
|
||||
search={{$:/temp/search}}
|
||||
searchClass="tc-dynannotation-search-overlay-blurred"
|
||||
searchMinLength={{$:/config/Search/MinLength}}
|
||||
>
|
||||
<$transclude tiddler=<<chunk>> mode="block"/>
|
||||
</$dynannotate>
|
||||
<$reveal type="popup" state={{{ [<chunk>addprefix[$:/state/dynannotate/popup-annotation/]] }}} position="belowright" animate="yes" retain="yes" style="overflow-y:hidden;">
|
||||
<div class="tc-drop-down-wrapper">
|
||||
<div class="tc-drop-down tc-popup-keep" style="max-width:550px;white-space: normal;overflow-y:hidden;">
|
||||
<$tiddler tiddler={{$:/temp/dynannotate/demo/annotation-title}}>
|
||||
<p>
|
||||
<h2>
|
||||
This is an annotation
|
||||
</h2>
|
||||
</p>
|
||||
<p>
|
||||
The annotation is stored in the tiddler:
|
||||
</p>
|
||||
<p>
|
||||
<$link><$view field="title"/></$link>
|
||||
</p>
|
||||
<p>
|
||||
The annotated text is ''<$view field="annotate-text"/>''.
|
||||
</p>
|
||||
<p>
|
||||
Annotation Colour:
|
||||
<$macrocall $name='colour-picker' actions="""
|
||||
<$action-setfield $field="annotate-colour" $value=<<colour-picker-value>>/>
|
||||
"""/>
|
||||
</p>
|
||||
</$tiddler>
|
||||
</div>
|
||||
</div>
|
||||
</$reveal>
|
||||
<$reveal type="popup" state={{{ [<chunk>addprefix[$:/state/dynannotate/popup-selection/]] }}} position="belowright" animate="yes" retain="yes" style="overflow-y:hidden;">
|
||||
<div class="tc-drop-down-wrapper">
|
||||
<div class="tc-drop-down tc-popup-keep" style="max-width:550px;white-space:normal;">
|
||||
<$vars
|
||||
text={{{ [<chunk>addprefix[$:/state/dynannotate/selection/]get[text]] }}}
|
||||
prefix={{{ [<chunk>addprefix[$:/state/dynannotate/selection-prefix/]get[text]] }}}
|
||||
suffix={{{ [<chunk>addprefix[$:/state/dynannotate/selection-suffix/]get[text]] }}}
|
||||
colour={{{ [<chunk>addprefix[$:/state/dynannotate/annotation-colour/]get[text]] }}}
|
||||
>
|
||||
<$button actions=<<create-annotation-actions>>>
|
||||
Create annotation
|
||||
</$button>
|
||||
<p>
|
||||
Text: <$text text=<<text>>/>
|
||||
</p>
|
||||
<p>
|
||||
Prefix: <$text text=<<prefix>>/>
|
||||
</p>
|
||||
<p>
|
||||
Suffix: <$text text=<<suffix>>/>
|
||||
</p>
|
||||
</$vars>
|
||||
</div>
|
||||
</div>
|
||||
</$reveal>
|
||||
</div>
|
||||
</$list>
|
@ -0,0 +1,5 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/example-annotation-1
|
||||
annotate-tiddler: $:/plugins/tiddlywiki/dynannotate/example-text-1
|
||||
annotate-text: memory is transitory. Yet the speed of action
|
||||
annotate-colour: SkyBlue
|
||||
annotate-blend-mode: multiply
|
@ -0,0 +1,5 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/example-annotation-2
|
||||
annotate-tiddler: $:/plugins/tiddlywiki/dynannotate/example-text-3
|
||||
annotate-text: It needs a name, and to coin one at random, "memex" will do
|
||||
annotate-colour: rgba(255,0,255,0.45)
|
||||
annotate-blend-mode: multiply
|
@ -0,0 +1,5 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/example-annotation-3
|
||||
annotate-tiddler: $:/plugins/tiddlywiki/dynannotate/example-text-5
|
||||
annotate-text: it would take him hundreds of years to fill the repository
|
||||
annotate-colour: #fff
|
||||
annotate-blend-mode: difference
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"title": "$:/plugins/tiddlywiki/dynannotate/example-annotation-4",
|
||||
"annotate-tiddler": "$:/plugins/tiddlywiki/dynannotate/example-text-1",
|
||||
"annotate-text": "that",
|
||||
"annotate-prefix": "It has other characteristics, of course; trails ",
|
||||
"annotate-suffix": " are not frequently followed are prone to fade",
|
||||
"annotate-colour": "rgba(255,0,255,0.45)",
|
||||
"annotate-blend-mode": "difference",
|
||||
"text": "(This tiddler is in .json format so that we can have field values that start with a whitespace"
|
||||
}
|
19
plugins/tiddlywiki/dynannotate/examples/example-text.multids
Normal file
19
plugins/tiddlywiki/dynannotate/examples/example-text.multids
Normal file
@ -0,0 +1,19 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/example-text-
|
||||
source: https://www.w3.org/History/1945/vbush/vbush.txt
|
||||
tags: DynannotateDemo
|
||||
|
||||
1: The human mind does not work that way. It operates by association. With one item in its grasp, it snaps instantly to the next that is suggested by the association of thoughts, in accordance with some intricate web of trails carried by the cells of the brain. It has other characteristics, of course; trails that are not frequently followed are prone to fade, items are not fully permanent, memory is ''transitory''. Yet the speed of action, the intricacy of trails, the detail of mental pictures, is awe-inspiring beyond all else in nature.
|
||||
|
||||
2: Man cannot hope fully to duplicate this mental process artificially, but he certainly ought to be able to learn from it. In minor ways he may even improve, for his records have relative permanency. The first idea, however, to be drawn from the analogy concerns selection. Selection by association, rather than by indexing, may yet be mechanized. One cannot hope thus to equal the speed and flexibility with which the mind follows an associative trail, but it should be possible to beat the mind decisively in regard to the permanence and clarity of the items resurrected from storage.
|
||||
|
||||
3: Consider a future device for individual use, which is a sort of mechanized private file and library. It needs a name, and to coin one at random, "memex" will do. A memex is a device in which an individual stores all his books, records, and communications, and which is mechanized so that it may be consulted with exceeding speed and flexibility. It is an enlarged intimate supplement to his memory.
|
||||
|
||||
4: It consists of a desk, and while it can presumably be operated from a distance, it is primarily the piece of furniture at which he works. On the top are slanting translucent screens, on which material can be projected for convenient reading. There is a keyboard, and sets of buttons and levers. Otherwise it looks like an ordinary desk.
|
||||
|
||||
5: In one end is the stored material. The matter of bulk is well taken care of by improved microfilm. Only a small part of the interior of the memex is devoted to storage, the rest to mechanism. Yet if the user inserted 5000 pages of material a day it would take him hundreds of years to fill the repository, so he can be profligate and enter material freely.
|
||||
|
||||
6: Most of the memex contents are purchased on microfilm ready for insertion. Books of all sorts, pictures, current periodicals, newspapers, are thus obtained and dropped into place. Business correspondence takes the same path. And there is provision for direct entry. On the top of the memex is a transparent platen. On this are placed longhand notes, photographs, memoranda, all sort of things. When one is in place, the depression of a lever causes it to be photographed onto the next blank space in a section of the memex film, dry photography being employed.
|
||||
|
||||
7: There is, of course, provision for consultation of the record by the usual scheme of indexing. If the user wishes to consult a certain book, he taps its code on the keyboard, and the title page of the book promptly appears before him, projected onto one of his viewing positions. Frequently-used codes are mnemonic, so that he seldom consults his code book; but when he does, a single tap of a key projects it for his use. Moreover, he has supplemental levers. On deflecting one of these levers to the right he runs through the book before him, each page in turn being projected at a speed which just allows a recognizing glance at each. If he deflects it further to the right, he steps through the book 10 pages at a time; still further at 100 pages at a time. Deflection to the left gives him the same control backwards.
|
||||
|
||||
8: A special button transfers him immediately to the first page of the index. Any given book of his library can thus be called up and consulted with far greater facility than if it were taken from a shelf. As he has several projection positions, he can leave one item in position while he calls up another. He can add marginal notes and comments, taking advantage of one possible type of dry photography, and it could even be arranged so that he can do this by a stylus scheme, such as is now employed in the telautograph seen in railroad waiting rooms, just as though he had the physical page before him.
|
4
plugins/tiddlywiki/dynannotate/examples/examples.tid
Normal file
4
plugins/tiddlywiki/dynannotate/examples/examples.tid
Normal file
@ -0,0 +1,4 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/examples
|
||||
|
||||
<<tabs "[all[tiddlers+shadows]tag[$:/tags/dynannotateExamples]!has[draft.of]]" "$:/plugins/tiddlywiki/dynannotate/examples/snippets">>
|
||||
|
100
plugins/tiddlywiki/dynannotate/examples/simple.tid
Normal file
100
plugins/tiddlywiki/dynannotate/examples/simple.tid
Normal file
@ -0,0 +1,100 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/examples/simple
|
||||
tags: $:/tags/dynannotateExamples
|
||||
caption: Simple
|
||||
|
||||
\define show-example(example)
|
||||
<$codeblock code=<<__example__>>/>
|
||||
|
||||
//''Displays as:''//
|
||||
|
||||
$example$
|
||||
\end
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Simple annotation
|
||||
|
||||
We use the `target*` attributes to specify a target string for the annotation and optionally a prefix and suffix for disambiguating multiple occurances.
|
||||
|
||||
</div>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
target="the"
|
||||
targetPrefix="Yet "
|
||||
targetSuffix=" speed"
|
||||
>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Plain text searching
|
||||
|
||||
We use the `search` attribute to specify a search string for highlighting:
|
||||
|
||||
</div>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
search="the"
|
||||
>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Regular expression searching
|
||||
|
||||
We use the `mode` attribute set to `regexp` to highlight matches of a regular expression:
|
||||
|
||||
</div>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
search="the|an"
|
||||
searchMode="regexp"
|
||||
searchClass="tc-dynannotation-search-overlay-blurred"
|
||||
>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Normalised whitespace searching
|
||||
|
||||
We use the `mode` attribute set to `whitespace` to search for a string with whitespace normalised (ie runs of whitespace are collapsed to a single space for matching purposes):
|
||||
|
||||
</div>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
search="does not work that way. It operates"
|
||||
searchMode="whitespace"
|
||||
searchClass="tc-dynannotation-search-overlay-animated"
|
||||
>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Using annotation tiddlers
|
||||
|
||||
Annotation tiddlers can be used to describe annotations. This example references the following annotation tiddlers:
|
||||
|
||||
</div>
|
||||
|
||||
<<list-links "[all[shadows+tiddlers]annotate-tiddler[$:/plugins/tiddlywiki/dynannotate/example-text-1]]">>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
filter="[all[shadows+tiddlers]annotate-tiddler[$:/plugins/tiddlywiki/dynannotate/example-text-1]]"
|
||||
>
|
||||
<$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
56
plugins/tiddlywiki/dynannotate/examples/snippets.tid
Normal file
56
plugins/tiddlywiki/dynannotate/examples/snippets.tid
Normal file
@ -0,0 +1,56 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/examples/snippets
|
||||
tags: $:/tags/dynannotateExamples
|
||||
caption: Snippets
|
||||
|
||||
\define show-example(example)
|
||||
<$codeblock code=<<__example__>>/>
|
||||
|
||||
//''Displays as:''//
|
||||
|
||||
$example$
|
||||
\end
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Search result snippets
|
||||
|
||||
The `searchDisplay` attribute can be set to `snippet` (instead of the default `overlay`) in order to display contextual snippets around search results.
|
||||
|
||||
</div>
|
||||
|
||||
<<show-example """
|
||||
<$dynannotate
|
||||
search="the"
|
||||
searchDisplay="snippet"
|
||||
><$transclude tiddler="$:/plugins/tiddlywiki/dynannotate/example-text-1" mode="block"/>
|
||||
</$dynannotate>
|
||||
""">>
|
||||
|
||||
<div class="tc-dynannotation-example-info">
|
||||
|
||||
!! Multiple search result snippets
|
||||
|
||||
This example searches across multiple tiddlers and shows snippets for those tiddlers that match.
|
||||
|
||||
</div>
|
||||
|
||||
<$macrocall $name="show-example" example="""
|
||||
Search: <$edit-text tiddler="$:/temp/search" tag="input"/>
|
||||
<$list filter="[all[tiddlers+shadows]tag[DynannotateDemo]search:text{$:/temp/search}sort[title]]">
|
||||
<dl>
|
||||
<dt>
|
||||
<$link>
|
||||
<$text text=<<currentTiddler>>/>
|
||||
</$link>
|
||||
</dt>
|
||||
<dd>
|
||||
<$dynannotate
|
||||
search={{$:/temp/search}}
|
||||
searchMode="whitespace"
|
||||
searchDisplay="snippet"
|
||||
><$transclude tiddler=<<currentTiddler>> mode="block"/>
|
||||
</$dynannotate>
|
||||
</dd>
|
||||
</dl>
|
||||
</$list>
|
||||
"""/>
|
418
plugins/tiddlywiki/dynannotate/modules/dynannotate.js
Normal file
418
plugins/tiddlywiki/dynannotate/modules/dynannotate.js
Normal file
@ -0,0 +1,418 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/dynannotate/dynannotate.js
|
||||
type: application/javascript
|
||||
module-type: widget
|
||||
|
||||
Dynannotate widget
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var TextMap = require("$:/plugins/tiddlywiki/dynannotate/textmap.js").TextMap;
|
||||
|
||||
var Widget = require("$:/core/modules/widgets/widget.js").widget;
|
||||
|
||||
var DynannotateWidget = function(parseTreeNode,options) {
|
||||
this.initialise(parseTreeNode,options);
|
||||
};
|
||||
|
||||
/*
|
||||
Inherit from the base widget class
|
||||
*/
|
||||
DynannotateWidget.prototype = new Widget();
|
||||
|
||||
/*
|
||||
Render this widget into the DOM
|
||||
*/
|
||||
DynannotateWidget.prototype.render = function(parent,nextSibling) {
|
||||
var self = this;
|
||||
this.parentDomNode = parent;
|
||||
this.computeAttributes();
|
||||
this.execute();
|
||||
// Create our DOM nodes
|
||||
var isSnippetMode = this.isSnippetMode();
|
||||
this.domContent = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotation-selection-container"
|
||||
});
|
||||
if(isSnippetMode) {
|
||||
this.domContent.setAttribute("hidden","hidden");
|
||||
}
|
||||
this.domAnnotations = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotation-annotation-wrapper"
|
||||
});
|
||||
this.domSnippets = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotation-snippet-wrapper"
|
||||
});
|
||||
this.domSearches = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotation-search-wrapper"
|
||||
});
|
||||
this.domWrapper = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotation-wrapper",
|
||||
children: [this.domContent,this.domAnnotations,this.domSnippets,this.domSearches]
|
||||
})
|
||||
parent.insertBefore(this.domWrapper,nextSibling);
|
||||
this.domNodes.push(this.domWrapper);
|
||||
// Apply the selection tracker data to the DOM
|
||||
if(!isSnippetMode) {
|
||||
this.applySelectionTrackerData();
|
||||
}
|
||||
// Render our child widgets
|
||||
this.renderChildren(this.domContent,null);
|
||||
if(isSnippetMode) {
|
||||
// Apply search snippets
|
||||
this.applySnippets();
|
||||
} else {
|
||||
// Get the list of annotation tiddlers
|
||||
this.getAnnotationTiddlers();
|
||||
// Apply annotations
|
||||
this.applyAnnotations();
|
||||
// Apply search overlays
|
||||
this.applySearch();
|
||||
}
|
||||
// Save the width of the wrapper so that we can tell when it changes
|
||||
this.wrapperWidth = this.domWrapper.offsetWidth;
|
||||
};
|
||||
|
||||
/*
|
||||
Compute the internal state of the widget
|
||||
*/
|
||||
DynannotateWidget.prototype.execute = function() {
|
||||
// Make the child widgets
|
||||
this.makeChildWidgets();
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.isSnippetMode = function() {
|
||||
return this.getAttribute("searchDisplay") === "snippet";
|
||||
}
|
||||
|
||||
/*
|
||||
Save the data attributes required by the selection tracker
|
||||
*/
|
||||
DynannotateWidget.prototype.applySelectionTrackerData = function() {
|
||||
if(this.hasAttribute("selection")) {
|
||||
this.domContent.setAttribute("data-annotation-selection-save",this.getAttribute("selection"));
|
||||
} else {
|
||||
this.domContent.removeAttribute("data-annotation-selection-save");
|
||||
}
|
||||
if(this.hasAttribute("selectionPopup")) {
|
||||
this.domContent.setAttribute("data-annotation-selection-popup",this.getAttribute("selectionPopup"));
|
||||
} else {
|
||||
this.domContent.removeAttribute("data-annotation-selection-popup");
|
||||
}
|
||||
if(this.hasAttribute("selectionPrefix")) {
|
||||
this.domContent.setAttribute("data-annotation-selection-prefix-save",this.getAttribute("selectionPrefix"));
|
||||
} else {
|
||||
this.domContent.removeAttribute("data-annotation-selection-prefix-save");
|
||||
}
|
||||
if(this.hasAttribute("selectionSuffix")) {
|
||||
this.domContent.setAttribute("data-annotation-selection-suffix-save",this.getAttribute("selectionSuffix"));
|
||||
} else {
|
||||
this.domContent.removeAttribute("data-annotation-selection-suffix-save");
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Create overlay dom elements to cover a specified range
|
||||
|
||||
options include:
|
||||
startNode: Start node of range
|
||||
startOffset: Start offset of range
|
||||
endNode: End node of range
|
||||
endOffset: End offset of range
|
||||
className: Optional classname for the overlay
|
||||
wrapper: Wrapper dom node for the overlays
|
||||
colour: Optional CSS colour for the overlay
|
||||
blendMode: Optional CSS mix blend mode for the overlay
|
||||
onclick: Optional click event handler for the overlay
|
||||
*/
|
||||
DynannotateWidget.prototype.createOverlay = function(options) {
|
||||
var self = this;
|
||||
// Create a range covering the text
|
||||
var range = this.document.createRange();
|
||||
range.setStart(options.startNode,options.startOffset);
|
||||
range.setEnd(options.endNode,options.endOffset);
|
||||
// Get the position of the range
|
||||
var rects = range.getClientRects();
|
||||
if(rects) {
|
||||
// Paint each rectangle
|
||||
var parentRect = this.domContent.getBoundingClientRect();
|
||||
$tw.utils.each(rects,function(rect) {
|
||||
var domOverlay = self.document.createElement("div");
|
||||
domOverlay.className = (options.className || "") + " tc-dynaview-request-refresh-on-resize";
|
||||
domOverlay.style.top = (rect.top - parentRect.top) + "px";
|
||||
domOverlay.style.left = (rect.left - parentRect.left) + "px";
|
||||
domOverlay.style.width = rect.width + "px";
|
||||
domOverlay.style.height = rect.height + "px";
|
||||
domOverlay.style.backgroundColor = options.colour;
|
||||
domOverlay.style.mixBlendMode = options.blendMode;
|
||||
if(options.onclick) {
|
||||
domOverlay.addEventListener("click",function(event) {
|
||||
var modifierKey = event.ctrlKey && !event.shiftKey ? "ctrl" : event.shiftKey && !event.ctrlKey ? "shift" : event.ctrlKey && event.shiftKey ? "ctrl-shift" : "normal";
|
||||
options.onclick(event,domOverlay,modifierKey);
|
||||
},false);
|
||||
}
|
||||
options.wrapper.appendChild(domOverlay);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.getAnnotationTiddlers = function() {
|
||||
this.annotationTiddlers = this.wiki.filterTiddlers(this.getAttribute("filter",""),this);
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.removeAnnotations = function() {
|
||||
while(this.domAnnotations.hasChildNodes()) {
|
||||
this.domAnnotations.removeChild(this.domAnnotations.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.applyAnnotations = function() {
|
||||
var self = this;
|
||||
// Remove any previous annotation overlays
|
||||
this.removeAnnotations();
|
||||
// Don't do anything if there are no annotations to apply
|
||||
if(this.annotationTiddlers.length === 0 && !this.hasAttribute("target")) {
|
||||
return;
|
||||
}
|
||||
// Build the map of the text content
|
||||
var textMap = new TextMap(this.domContent);
|
||||
// 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});
|
||||
if(self.hasAttribute("popup")) {
|
||||
$tw.popup.triggerPopup({
|
||||
domNode: domOverlay,
|
||||
title: self.getAttribute("popup"),
|
||||
wiki: self.wiki
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
// Draw the overlay for the "target" attribute
|
||||
if(this.hasAttribute("target")) {
|
||||
var result = textMap.findText(this.getAttribute("target"),this.getAttribute("targetPrefix"),this.getAttribute("targetSuffix"));
|
||||
if(result) {
|
||||
this.createOverlay({
|
||||
startNode: result.startNode,
|
||||
startOffset: result.startOffset,
|
||||
endNode: result.endNode,
|
||||
endOffset: result.endOffset,
|
||||
wrapper: self.domAnnotations,
|
||||
className: "tc-dynannotation-annotation-overlay",
|
||||
onclick: clickHandlerFn(null)
|
||||
});
|
||||
}
|
||||
}
|
||||
// Draw the overlays for each annotation tiddler
|
||||
$tw.utils.each(this.annotationTiddlers,function(title) {
|
||||
var tiddler = self.wiki.getTiddler(title),
|
||||
annotateText = tiddler.fields["annotate-text"],
|
||||
annotatePrefix = tiddler.fields["annotate-prefix"],
|
||||
annotateSuffix = tiddler.fields["annotate-suffix"];
|
||||
if(tiddler && annotateText) {
|
||||
var result = textMap.findText(annotateText,annotatePrefix,annotateSuffix);
|
||||
if(result) {
|
||||
self.createOverlay({
|
||||
startNode: result.startNode,
|
||||
startOffset: result.startOffset,
|
||||
endNode: result.endNode,
|
||||
endOffset: result.endOffset,
|
||||
wrapper: self.domAnnotations,
|
||||
className: "tc-dynannotation-annotation-overlay",
|
||||
colour: tiddler.fields["annotate-colour"],
|
||||
blendMode: tiddler.fields["annotate-blend-mode"],
|
||||
onclick: clickHandlerFn(title)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.removeSearch = function() {
|
||||
while(this.domSearches.hasChildNodes()) {
|
||||
this.domSearches.removeChild(this.domSearches.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.applySearch = function() {
|
||||
var self = this;
|
||||
// Remove any previous search overlays
|
||||
this.removeSearch();
|
||||
// Gather parameters
|
||||
var searchString = this.getAttribute("search",""),
|
||||
searchMode = this.getAttribute("searchMode"),
|
||||
searchCaseSensitive = this.getAttribute("searchCaseSensitive","yes") === "yes",
|
||||
searchMinLength = parseInt(this.getAttribute("searchMinLength","1"),10) || 1;
|
||||
// Bail if search string too short
|
||||
if(searchString.length < searchMinLength) {
|
||||
return;
|
||||
}
|
||||
// Build the map of the text content
|
||||
var textMap = new TextMap(this.domContent);
|
||||
// Search for the string
|
||||
var matches = textMap.search(this.getAttribute("search",""),{
|
||||
mode: this.getAttribute("searchMode"),
|
||||
caseSensitive: this.getAttribute("searchCaseSensitive","yes") === "yes"
|
||||
});
|
||||
// Create overlays for each match
|
||||
$tw.utils.each(matches,function(match) {
|
||||
self.createOverlay({
|
||||
startNode: match.startNode,
|
||||
startOffset: match.startOffset,
|
||||
endNode: match.endNode,
|
||||
endOffset: match.endOffset,
|
||||
wrapper: self.domSearches,
|
||||
className: "tc-dynannotation-search-overlay " + self.getAttribute("searchClass","")
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.removeSnippets = function() {
|
||||
while(this.domSnippets.hasChildNodes()) {
|
||||
this.domSnippets.removeChild(this.domSnippets.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
DynannotateWidget.prototype.applySnippets = function() {
|
||||
var self = this,
|
||||
contextLength = parseInt(this.getAttribute("snippetContextLength","33"),10) || 0;
|
||||
// Build the map of the text content
|
||||
var textMap = new TextMap(this.domContent);
|
||||
// Remove any previous snippets
|
||||
this.removeSnippets();
|
||||
// Gather parameters
|
||||
var searchString = this.getAttribute("search",""),
|
||||
searchMode = this.getAttribute("searchMode"),
|
||||
searchCaseSensitive = this.getAttribute("searchCaseSensitive","yes") === "yes",
|
||||
searchMinLength = parseInt(this.getAttribute("searchMinLength","1"),10) || 1;
|
||||
// Build the map of the text content
|
||||
var textMap = new TextMap(this.domContent);
|
||||
// Search for the string
|
||||
var matches = textMap.search(this.getAttribute("search",""),{
|
||||
mode: this.getAttribute("searchMode"),
|
||||
caseSensitive: this.getAttribute("searchCaseSensitive","no") === "yes"
|
||||
});
|
||||
// Output a snippet for each match
|
||||
if(matches && matches.length > 0) {
|
||||
var merged = false, // Keep track of whether the context of the previous match merges into this one
|
||||
ellipsis = String.fromCharCode(8230),
|
||||
container = null; // Track the container so that we can reuse the same container for merged matches
|
||||
$tw.utils.each(matches,function(match,index) {
|
||||
// Create a container if we're not reusing it
|
||||
if(!container) {
|
||||
container = $tw.utils.domMaker("div",{
|
||||
"class": "tc-dynannotate-snippet"
|
||||
});
|
||||
self.domSnippets.appendChild(container);
|
||||
}
|
||||
// Output the preceding context if it wasn't merged into the previous match
|
||||
if(!merged) {
|
||||
container.appendChild($tw.utils.domMaker("span",{
|
||||
text: (match.startPos < contextLength ? "" : ellipsis) +
|
||||
textMap.string.slice(Math.max(match.startPos - contextLength,0),match.startPos),
|
||||
"class": "tc-dynannotate-snippet-context"
|
||||
}));
|
||||
}
|
||||
// Output the match
|
||||
container.appendChild($tw.utils.domMaker("span",{
|
||||
text: textMap.string.slice(match.startPos,match.endPos),
|
||||
"class": "tc-dynannotate-snippet-highlight " + self.getAttribute("searchClass")
|
||||
}));
|
||||
// Does the context of this match merge into the next?
|
||||
merged = index < matches.length - 1 && matches[index + 1].startPos - match.endPos <= 2 * contextLength;
|
||||
if(merged) {
|
||||
// If they're merged, use the context up until the next match
|
||||
container.appendChild($tw.utils.domMaker("span",{
|
||||
text: textMap.string.slice(match.endPos,matches[index + 1].startPos),
|
||||
"class": "tc-dynannotate-snippet-context"
|
||||
}));
|
||||
} else {
|
||||
// If they're not merged, use the context up to the end
|
||||
container.appendChild($tw.utils.domMaker("span",{
|
||||
text: textMap.string.slice(match.endPos,match.endPos + contextLength) +
|
||||
((match.endPos + contextLength) >= textMap.string.length ? "" : ellipsis),
|
||||
"class": "tc-dynannotate-snippet-context"
|
||||
}));
|
||||
}
|
||||
// Reuse the next container if we're merged
|
||||
if(!merged) {
|
||||
container = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
|
||||
*/
|
||||
DynannotateWidget.prototype.refresh = function(changedTiddlers) {
|
||||
// Get the changed attributes
|
||||
var changedAttributes = this.computeAttributes();
|
||||
// Refresh completely if the "searchDisplay" attribute has changed
|
||||
if(changedAttributes.searchDisplay) {
|
||||
this.refreshSelf();
|
||||
return true;
|
||||
}
|
||||
// Check whether we're in snippet mode
|
||||
var isSnippetMode = this.isSnippetMode();
|
||||
// Refresh the child widgets
|
||||
var childrenDidRefresh = this.refreshChildren(changedTiddlers);
|
||||
// Reapply the selection tracker data to the DOM
|
||||
if(changedAttributes.selection || changedAttributes.selectionPrefix || changedAttributes.selectionSuffix || changedAttributes.selectionPopup) {
|
||||
this.applySelectionTrackerData();
|
||||
}
|
||||
// Reapply the annotations if the children refreshed or the main wrapper resized
|
||||
var wrapperWidth = this.domWrapper.offsetWidth,
|
||||
hasResized = wrapperWidth !== this.wrapperWidth || changedTiddlers["$:/state/DynaView/ViewportDimensions/ResizeCount"],
|
||||
oldAnnotationTiddlers = this.annotationTiddlers;
|
||||
this.getAnnotationTiddlers();
|
||||
if(!isSnippetMode && (
|
||||
childrenDidRefresh ||
|
||||
hasResized ||
|
||||
changedAttributes.target ||
|
||||
changedAttributes.targetPrefix ||
|
||||
changedAttributes.targetSuffix ||
|
||||
changedAttributes.filter ||
|
||||
changedAttributes.actions ||
|
||||
changedAttributes.popup ||
|
||||
!$tw.utils.isArrayEqual(oldAnnotationTiddlers,this.annotationTiddlers) ||
|
||||
this.annotationTiddlers.find(function(title) {
|
||||
return changedTiddlers[title];
|
||||
}) !== undefined
|
||||
)) {
|
||||
this.applyAnnotations();
|
||||
}
|
||||
if(!isSnippetMode && (
|
||||
childrenDidRefresh ||
|
||||
hasResized ||
|
||||
changedAttributes.search ||
|
||||
changedAttributes.searchMinLength ||
|
||||
changedAttributes.searchClass ||
|
||||
changedAttributes.searchMode ||
|
||||
changedAttributes.searchCaseSensitive
|
||||
)) {
|
||||
this.applySearch();
|
||||
}
|
||||
if(isSnippetMode && (
|
||||
childrenDidRefresh ||
|
||||
hasResized ||
|
||||
changedAttributes.search ||
|
||||
changedAttributes.searchMinLength ||
|
||||
changedAttributes.searchClass ||
|
||||
changedAttributes.searchMode ||
|
||||
changedAttributes.searchCaseSensitive
|
||||
)) {
|
||||
this.applySnippets();
|
||||
}
|
||||
this.wrapperWidth = wrapperWidth;
|
||||
return childrenDidRefresh;
|
||||
};
|
||||
|
||||
exports.dynannotate = DynannotateWidget;
|
||||
|
||||
})();
|
116
plugins/tiddlywiki/dynannotate/modules/selection-tracker.js
Normal file
116
plugins/tiddlywiki/dynannotate/modules/selection-tracker.js
Normal file
@ -0,0 +1,116 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/dynannotate/selection-tracker.js
|
||||
type: application/javascript
|
||||
module-type: startup
|
||||
|
||||
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 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SelectionTracker.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;
|
||||
};
|
||||
|
||||
})();
|
177
plugins/tiddlywiki/dynannotate/modules/textmap.js
Normal file
177
plugins/tiddlywiki/dynannotate/modules/textmap.js
Normal file
@ -0,0 +1,177 @@
|
||||
/*\
|
||||
title: $:/plugins/tiddlywiki/dynannotate/textmap.js
|
||||
type: application/javascript
|
||||
module-type: library
|
||||
|
||||
Structure for modelling mapping between a string and its representation in the DOM
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var PREFIX_SUFFIX_LENGTH = 50;
|
||||
|
||||
/*
|
||||
Build a map of the text content of a dom node and its descendents:
|
||||
|
||||
string: concatenation of the text content of child nodes
|
||||
metadata: array of {start,end,domNode} where start and end identify position in the string
|
||||
*/
|
||||
exports.TextMap = function(domNode) {
|
||||
var self = this,
|
||||
stringChunks = [],
|
||||
p = 0;
|
||||
this.metadata = [];
|
||||
var processNode = function(domNode) {
|
||||
// Check for text nodes
|
||||
if(domNode.nodeType === 3) {
|
||||
var text = domNode.textContent;
|
||||
stringChunks.push(text);
|
||||
self.metadata.push({
|
||||
start: p,
|
||||
end: p + text.length,
|
||||
domNode: domNode
|
||||
});
|
||||
p += text.length;
|
||||
} else {
|
||||
// Otherwise look within the child nodes
|
||||
if(domNode.childNodes) {
|
||||
for(var t=0; t<domNode.childNodes.length; t++ ) {
|
||||
processNode(domNode.childNodes[t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Process our text nodes
|
||||
processNode(domNode);
|
||||
this.string = stringChunks.join("");
|
||||
};
|
||||
|
||||
/*
|
||||
Locate the metadata record corresponding to a given position in the string
|
||||
*/
|
||||
exports.TextMap.prototype.locateMetadata = function(position) {
|
||||
return this.metadata.find(function(metadata) {
|
||||
return position >= metadata.start && position < metadata.end;
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Search for the first occurance of a target string within the textmap of a dom node
|
||||
|
||||
Returns an object with the following properties:
|
||||
startNode: node containing the start of the text
|
||||
startOffset: offset of the start of the text within the node
|
||||
endNode: node containing the end of the text
|
||||
endOffset: offset of the end of the text within the node
|
||||
*/
|
||||
exports.TextMap.prototype.findText = function(targetString,targetPrefix,targetSuffix) {
|
||||
if(!targetString) {
|
||||
return null;
|
||||
}
|
||||
targetPrefix = targetPrefix || "";
|
||||
targetSuffix = targetSuffix || "";
|
||||
var startPos = this.string.indexOf(targetPrefix + targetString + targetSuffix);
|
||||
if(startPos !== -1) {
|
||||
startPos += targetPrefix.length;
|
||||
var startMetadata = this.locateMetadata(startPos),
|
||||
endMetadata = this.locateMetadata(startPos + targetString.length);
|
||||
if(startMetadata && endMetadata) {
|
||||
return {
|
||||
startNode: startMetadata.domNode,
|
||||
startOffset: startPos - startMetadata.start,
|
||||
endNode: endMetadata.domNode,
|
||||
endOffset: (startPos + targetString.length) - endMetadata.start
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/*
|
||||
Search for all occurances of a string within the textmap of a dom node
|
||||
|
||||
Options include:
|
||||
mode: "normal", "regexp" or "whitespace"
|
||||
caseSensitive: true if the search should be case sensitive
|
||||
|
||||
Returns an array of objects with the following properties:
|
||||
startPos: start position of the match within the string contained by this TextMap
|
||||
startNode: node containing the start of the text
|
||||
startOffset: offset of the start of the text within the node
|
||||
endPos: end position of the match within the string contained by this TextMap
|
||||
endNode: node containing the end of the text
|
||||
endOffset: offset of the end of the text within the node
|
||||
*/
|
||||
exports.TextMap.prototype.search = function(searchString,options) {
|
||||
if(!searchString) {
|
||||
return [];
|
||||
}
|
||||
options = options || {};
|
||||
// Compose the regexp
|
||||
var regExpString,
|
||||
flags = options.caseSensitive ? "g" : "gi";
|
||||
if(options.mode === "regexp") {
|
||||
regExpString = "(" + searchString + ")";
|
||||
} else if(options.mode === "whitespace") {
|
||||
// Normalise whitespace
|
||||
regExpString = "(" + searchString.split(/\s+/g).filter(function(word) {
|
||||
return !!word
|
||||
}).map($tw.utils.escapeRegExp).join("\\s+") + ")";
|
||||
} else {
|
||||
// Normal search
|
||||
regExpString = "(" + $tw.utils.escapeRegExp(searchString) + ")";
|
||||
}
|
||||
// Compile the regular expression
|
||||
var regExp;
|
||||
try {
|
||||
regExp = RegExp(regExpString,flags);
|
||||
} catch(e) {
|
||||
}
|
||||
if(!regExp) {
|
||||
return [];
|
||||
}
|
||||
// Find each match
|
||||
var results = [],
|
||||
match;
|
||||
do {
|
||||
match = regExp.exec(this.string);
|
||||
if(match) {
|
||||
var metadataStart = this.locateMetadata(match.index),
|
||||
metadataEnd = this.locateMetadata(match.index + match[0].length);
|
||||
if(metadataStart && metadataEnd) {
|
||||
results.push({
|
||||
startPos: match.index,
|
||||
startNode: metadataStart.domNode,
|
||||
startOffset: match.index - metadataStart.start,
|
||||
endPos: match.index + match[0].length,
|
||||
endNode: metadataEnd.domNode,
|
||||
endOffset: match.index + match[0].length - metadataEnd.start
|
||||
});
|
||||
}
|
||||
}
|
||||
} while(match);
|
||||
return results;
|
||||
};
|
||||
|
||||
/*
|
||||
Given a start container and offset and a search string, return a prefix and suffix to disambiguate the text
|
||||
*/
|
||||
exports.TextMap.prototype.extractContext = function(startContainer,startOffset,text) {
|
||||
var startMetadata = this.metadata.find(function(metadata) {
|
||||
return metadata.domNode === startContainer
|
||||
});
|
||||
if(!startMetadata) {
|
||||
return null;
|
||||
}
|
||||
var startPos = startMetadata.start + startOffset;
|
||||
return {
|
||||
prefix: this.string.slice(Math.max(startPos - PREFIX_SUFFIX_LENGTH, 0), startPos),
|
||||
suffix: this.string.slice(startPos + text.length, Math.min(startPos + text.length + PREFIX_SUFFIX_LENGTH, this.string.length))
|
||||
};
|
||||
};
|
||||
|
||||
})();
|
9
plugins/tiddlywiki/dynannotate/plugin.info
Normal file
9
plugins/tiddlywiki/dynannotate/plugin.info
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"title": "$:/plugins/tiddlywiki/dynannotate",
|
||||
"description": "Dynamic content annotation",
|
||||
"author": "JeremyRuston",
|
||||
"core-version": ">=5.0.0",
|
||||
"version": "0.0.6-prerelease",
|
||||
"list": "readme examples history",
|
||||
"dependents": ["$:/plugins/tiddlywiki/dynaview"]
|
||||
}
|
44
plugins/tiddlywiki/dynannotate/styles.tid
Normal file
44
plugins/tiddlywiki/dynannotate/styles.tid
Normal file
@ -0,0 +1,44 @@
|
||||
title: $:/plugins/tiddlywiki/dynannotate/styles
|
||||
tags: [[$:/tags/Stylesheet]]
|
||||
|
||||
\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline
|
||||
|
||||
.tc-dynannotation-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tc-dynannotation-annotation-overlay {
|
||||
position: absolute;
|
||||
background: rgba(255,255,0,0.3);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.tc-dynannotation-search-overlay {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: rgba(255,0,0,0.3);
|
||||
}
|
||||
|
||||
.tc-dynannotation-search-overlay-blurred {
|
||||
background: rgba(255,0,0,0.3);
|
||||
mix-blend-mode: multiply;
|
||||
border-radius: 4px;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
@keyframes ta-dynannotation-search-overlay-animated { to { background-position: 100% 100% } }
|
||||
|
||||
.tc-dynannotation-search-overlay-animated {
|
||||
mix-blend-mode: multiply;
|
||||
background: repeating-linear-gradient(-45deg, #ff8 0, #dd8 25%, transparent 0, transparent 50%) 0 / .6em .6em;
|
||||
animation: ta-dynannotation-search-overlay-animated 12s linear infinite;
|
||||
}
|
||||
|
||||
.tc-dynannotate-snippet-highlight {
|
||||
background: #efef53;
|
||||
}
|
||||
|
||||
.tc-dynannotation-example-info {
|
||||
background: #ffa;
|
||||
padding: 1em;
|
||||
}
|
Loading…
Reference in New Issue
Block a user