Skip to content

Commit 219bf83

Browse files
committed
Annotation code and documentation
1 parent 73ccff2 commit 219bf83

13 files changed

+2858
-1
lines changed

.DS_Store

6 KB
Binary file not shown.

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.DS_Store

README.md

+86-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,86 @@
1-
# annotate
1+
# Annotate
2+
3+
This is some basic HTML/CSS/JavaScript that can be used to publish annotated copies of text, such as ["The (Edited) Latecomer's Guide to Crypto"](https://www.mollywhite.net/annotations/latecomers-guide-to-crypto) for which it was created.
4+
5+
![](./screenshots/latecomers-wide.png)
6+
7+
## How to use
8+
Copy the `index.html`, `styles.css`, and `annotate.js` files to your project. You should only need to edit the `index.html` file, unless you want to change the styling or JavaScript behavior. This project does not *require* the JavaScript, so if you want to leave it out, just omit the `annotate.js` file and remove the `<script src="./annotate.js"></script>` tag from the HTML file. The `screenshots` folder has full-size screenshots of the index page in both desktop ([`index-desktop.png`](https://github.com/molly/annotate/blob/main/screenshots/index-desktop.png)) and mobile ([`index-mobile.png`](https://github.com/molly/annotate/blob/main/screenshots/index-mobile.png)) views, so you can see what the HTML produces.
9+
10+
The demo folder contains the HTML for the entire ["(Edited) Latecomer's Guide to Crypto"](https://www.mollywhite.net/annotations/latecomers-guide-to-crypto), if you're trying to replicate anything there.
11+
12+
Each section of the document follows this basic structure:
13+
14+
```html
15+
<div class="group">
16+
<div class="left">
17+
<div class="content">
18+
Text that's being <mark data-annotation-id="1" aria-details="unique-comment-id">annotated</mark>.
19+
</div>
20+
</div>
21+
<div class="right">
22+
<div class="content">
23+
<div class="annotation" role="comment" data-annotation-id="1" id="unique-comment-id">
24+
<div class="commenter">Commenter name</div>
25+
Comment text.
26+
</div>
27+
</div>
28+
</div>
29+
</div>
30+
```
31+
32+
and produces:
33+
34+
![](./screenshots/small-example.png)
35+
36+
## Details
37+
38+
Each section of text is captured in a row with left- and right-hand sections. The div with the `group` class represents this row. Each side then has a div with the `left` or `right` class, which has a child `content` div.
39+
40+
Each portion of highlighted text in the original source (left-hand side) is marked with `<mark>` tags. These must have a unique `aria-details` attribute that will correspond to the `id` of the annotation, which will enable visual focus highlighting on click. It can also optionally have a `data-annotation-id` to number the annotation, to help distinguish annotations when there are multiple in a section.
41+
42+
Corresponding to the `<mark>` tag will be a div with either the `annotation` or `annotation-group` class on the right-hand side (the former for single annotations, the latter for grouped annotations). These must have `role="comment"` and an `id` that exactly matches the unique `aria-details` value of the highlighted text to which it corresponds. As with the highlighted text, it can have a `data-annotation-id` to number the annotation.
43+
44+
### Grouped annotations
45+
Within an annotation group, there will be one or more divs with the `annotation` class. These can contain a div with the class `commenter` to identify the writer, if there are multiple annotators working on the document. These do *not* need `role="comment`, `data-annotation-id`, or `id` since they're nested within an `annotation-group` with those attributes.
46+
47+
In the case of multiple annotations within an annotation group, they can appear directly stacked, or threaded (rendering with increasing levels of indentation, to indicate that they are replies to one another). To thread comments, include the `thread` class on the second comment (the first reply). Any subsequent replies should be marked with the `thread-x` class, where `x` is the level of indentation from 2–10: `thread-2`, `thread-3`, ..., `thread-10`.
48+
49+
```html
50+
<div class="group">
51+
<div class="left">
52+
<div class="content">
53+
Text that's being <mark data-annotation-id="1" aria-details="unique-comment-id">annotated</mark>.
54+
</div>
55+
</div>
56+
<div class="right">
57+
<div class="content">
58+
<div class="annotation-group" role="comment" data-annotation-id="1" id="unique-comment-id">
59+
<div class="annotation">
60+
A comment with indented responses.
61+
</div>
62+
<div class="annotation thread">
63+
A reply
64+
</div>
65+
<div class="annotation thread-2">
66+
A second reply
67+
</div>
68+
</div>
69+
</div>
70+
</div>
71+
</div>
72+
```
73+
74+
![](./screenshots/small-example-thread.png)
75+
76+
## Other source formats
77+
78+
The original Latecomer's Guide project was created using [Pug](https://pugjs.org/) and [Sass](https://sass-lang.com). If you'd rather work with those, that source code lives over with my [website source](https://github.com/molly/website-v2):
79+
* [Pug](https://github.com/molly/website-v2/blob/master/src/pug/pages/annotations/latecomers-guide-to-crypto.pug) file
80+
* [Sass](https://github.com/molly/website-v2/blob/master/src/sass/reviews.sass) file
81+
82+
## Mobile display
83+
84+
This is how the annotations display on mobile:
85+
86+
![](./screenshots/latecomers-mobile.png | width=400)

annotate.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
function deselectAllExcept(selector) {
2+
var allSelected = document.getElementsByClassName('selected');
3+
for (var i = 0; i < allSelected.length; i++) {
4+
if (
5+
allSelected[i].id !== selector &&
6+
allSelected[i].getAttribute('aria-details') !== selector
7+
) {
8+
allSelected[i].classList.remove('selected');
9+
}
10+
}
11+
}
12+
13+
function makeClickHandler(isHighlight) {
14+
return function onClick(event) {
15+
var targetElement, selector, corresponding;
16+
if (isHighlight) {
17+
selector = event.target.getAttribute('aria-details');
18+
targetElement = event.target;
19+
} else {
20+
if (event.target.getAttribute('role') === 'comment') {
21+
selector = event.target.id;
22+
targetElement = event.target;
23+
} else {
24+
// Depending on where they click, they may have targeted a child element
25+
var annotation = event.target.closest('[role="comment"]');
26+
targetElement = annotation;
27+
selector = annotation.id;
28+
}
29+
}
30+
31+
if (isHighlight) {
32+
corresponding = document.getElementById(selector);
33+
} else {
34+
corresponding = document.querySelector(`[aria-details="${selector}"]`);
35+
}
36+
37+
// Highlight click target and corresponding element, and scroll to corresponding element
38+
// If target is already highlighted, dehilight (and don't scroll)
39+
const isSelected = targetElement.classList.toggle('selected');
40+
corresponding.classList.toggle('selected');
41+
if (isSelected) {
42+
var prefersReducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
43+
var prefersReducedMotion = !prefersReducedMotionQuery || prefersReducedMotionQuery.matches;
44+
corresponding.scrollIntoView({
45+
behavior: prefersReducedMotion ? 'auto' : 'smooth',
46+
block: 'nearest',
47+
});
48+
}
49+
50+
// Ensure this is the only highlighted pair
51+
deselectAllExcept(selector);
52+
53+
// Avoid bubbling through to the deselectAll function
54+
event.stopPropagation();
55+
};
56+
}
57+
58+
function deselectAll() {
59+
var selectedComments = document.querySelectorAll('.selected');
60+
for (var i = 0; i < selectedComments.length; i++) {
61+
selectedComments[i].classList.remove('selected');
62+
}
63+
}
64+
65+
function onInitialLoad() {
66+
document.documentElement.className = document.documentElement.className.replace(
67+
/\bno-js\b/,
68+
'js',
69+
);
70+
71+
var highlights = document.getElementsByTagName('mark');
72+
for (var i = 0; i < highlights.length; i++) {
73+
highlights[i].addEventListener('click', makeClickHandler(true));
74+
}
75+
var comments = document.getElementsByClassName('annotation');
76+
for (var j = 0; j < comments.length; j++) {
77+
comments[j].addEventListener('click', makeClickHandler(false));
78+
}
79+
80+
document.addEventListener('click', deselectAll);
81+
}
82+
83+
(function () {
84+
if (document.readyState != 'loading') {
85+
onInitialLoad();
86+
} else {
87+
document.addEventListener('DOMContentLoaded', onInitialLoad);
88+
}
89+
})();

0 commit comments

Comments
 (0)