Commit 7d61ceb h
Changes: 3 files +63 -92
Changes (Side-by-Side)
share/jenny/layout/page.sh +23 -42
cat << _EOF_ | |||
| 890 | while their siblings were bold. Deeper headings stay regular -- they |
890 | while their siblings were bold. Deeper headings stay regular -- they |
| 891 | sit inside a section rather than opening one. */ |
891 | sit inside a section rather than opening one. */ |
| 892 | .contents :is(h1, h2) { font-weight: bold; } |
892 | .contents :is(h1, h2) { font-weight: bold; } |
| 893 | /* Deep-link mark. Hidden until the passage is hovered or the mark |
||
| 894 | itself is focused, so a page of prose is not littered with symbols; |
||
| 895 | keyboard users reach it by tabbing, which is why focus counts. */ |
||
| 893 | /* Deep-link mark, pinned to the top right of the block rather than |
||
| 894 | trailing the last word: at the end of a paragraph it sat wherever |
||
| 895 | the text happened to stop, which is a different place on every |
||
| 896 | block and reads as punctuation. A fixed corner is somewhere the eye |
||
| 897 | can learn. |
||
| 898 | |
||
| 899 | The block is the containing block, hence position: relative on it. |
||
| 900 | The mark sits outside the measure, in the margin the bleed already |
||
| 901 | uses, so it never reflows the text it belongs to. |
||
| 902 | |
||
| 903 | Hidden until the passage is hovered or the mark itself is focused, |
||
| 904 | so a page of prose is not littered with symbols; keyboard users |
||
| 905 | reach it by tabbing, which is why focus counts. */ |
||
| 906 | .contents :is(h1, h2, h3, h4)[id], |
||
| 907 | .contents p[id] { position: relative; } |
||
| 896 | .anchor-link { |
908 | .anchor-link { |
| 897 | margin-left: .4em; text-decoration: none; opacity: 0; |
||
| 898 | color: var(--text-muted); font-weight: normal; cursor: pointer; |
||
| 909 | position: absolute; top: 0; left: 100%; |
||
| 910 | margin-left: .5em; line-height: inherit; |
||
| 911 | text-decoration: none; opacity: 0; |
||
| 912 | color: var(--text-muted); font-weight: normal; |
||
| 899 | } |
913 | } |
| 900 | :is(h1, h2, h3, h4, p):hover > .anchor-link, |
914 | :is(h1, h2, h3, h4, p):hover > .anchor-link, |
| 901 | .anchor-link:focus { opacity: 1; } |
915 | .anchor-link:focus { opacity: 1; } |
cat << _EOF_ | |||
| 910 | box-shadow: 0 0 0 .4em var(--bg-secondary); |
924 | box-shadow: 0 0 0 .4em var(--bg-secondary); |
| 911 | border-radius: 2px; |
925 | border-radius: 2px; |
| 912 | } |
926 | } |
| 913 | .copy-toast { |
||
| 914 | position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em); |
||
| 915 | background-color: var(--bg-secondary); color: var(--text-primary); |
||
| 916 | border: 1px solid var(--border-light); border-radius: 3px; |
||
| 917 | padding: .5em 1em; font-size: .875em; z-index: 3000; |
||
| 918 | opacity: 0; pointer-events: none; |
||
| 919 | } |
||
| 920 | .copy-toast.show { opacity: 1; transform: translate(-50%, 0); } |
||
| 921 | /* Every heading is body size, which keeps the page even but leaves a |
927 | /* Every heading is body size, which keeps the page even but leaves a |
| 922 | section heading reading as a stray line of capitals: uppercase alone |
928 | section heading reading as a stray line of capitals: uppercase alone |
| 923 | does not say "new section". A rule above says it structurally rather |
929 | does not say "new section". A rule above says it structurally rather |
cat << _EOF_ | |||
| 1219 | var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); |
1225 | var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); |
| 1220 | if (!targets.length) return; |
1226 | if (!targets.length) return; |
| 1221 | |
1227 | |
| 1222 | var toast = null; |
||
| 1223 | function flash(msg) { |
||
| 1224 | if (!toast) { |
||
| 1225 | toast = document.createElement('div'); |
||
| 1226 | toast.className = 'copy-toast'; |
||
| 1227 | document.body.appendChild(toast); |
||
| 1228 | } |
||
| 1229 | toast.textContent = msg; |
||
| 1230 | toast.classList.add('show'); |
||
| 1231 | clearTimeout(toast._t); |
||
| 1232 | toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600); |
||
| 1233 | } |
||
| 1234 | |
||
| 1228 | // An ordinary link, not a copy button: following it puts the |
||
| 1229 | // address in the bar, and right-clicking offers copy-link the way |
||
| 1230 | // it does for every other link on the web. Nothing to learn. |
||
| 1235 | targets.forEach(function(el) { |
1231 | targets.forEach(function(el) { |
| 1236 | var a = document.createElement('a'); |
1232 | var a = document.createElement('a'); |
| 1237 | a.className = 'anchor-link'; |
1233 | a.className = 'anchor-link'; |
| 1238 | a.href = '#' + el.id; |
1234 | a.href = '#' + el.id; |
| 1239 | a.setAttribute('aria-label', 'Copy link to this passage'); |
||
| 1235 | a.setAttribute('aria-label', 'Link to this passage'); |
||
| 1240 | a.textContent = '\u00B6'; |
1236 | a.textContent = '\u00B6'; |
| 1241 | a.addEventListener('click', function(e) { |
||
| 1242 | e.preventDefault(); |
||
| 1243 | var url = location.origin + location.pathname + '#' + el.id; |
||
| 1244 | // The address bar is updated either way, so the link is |
||
| 1245 | // obtainable even where the clipboard is unavailable. |
||
| 1246 | history.replaceState(null, '', '#' + el.id); |
||
| 1247 | if (navigator.clipboard && navigator.clipboard.writeText) { |
||
| 1248 | navigator.clipboard.writeText(url).then( |
||
| 1249 | function() { flash('Link copied'); }, |
||
| 1250 | function() { flash('Press \u2318/Ctrl+C to copy'); } |
||
| 1251 | ); |
||
| 1252 | } else { |
||
| 1253 | flash('Press \u2318/Ctrl+C to copy'); |
||
| 1254 | } |
||
| 1255 | }); |
||
| 1256 | el.appendChild(a); |
1237 | el.appendChild(a); |
| 1257 | }); |
1238 | }); |
| 1258 | |
1239 | |
share/jenny/layout/post.sh +23 -42
cat << _EOF_ | |||
| 711 | while their siblings were bold. Deeper headings stay regular -- they |
711 | while their siblings were bold. Deeper headings stay regular -- they |
| 712 | sit inside a section rather than opening one. */ |
712 | sit inside a section rather than opening one. */ |
| 713 | .contents :is(h1, h2) { font-weight: bold; } |
713 | .contents :is(h1, h2) { font-weight: bold; } |
| 714 | /* Deep-link mark. Hidden until the passage is hovered or the mark |
||
| 715 | itself is focused, so a page of prose is not littered with symbols; |
||
| 716 | keyboard users reach it by tabbing, which is why focus counts. */ |
||
| 714 | /* Deep-link mark, pinned to the top right of the block rather than |
||
| 715 | trailing the last word: at the end of a paragraph it sat wherever |
||
| 716 | the text happened to stop, which is a different place on every |
||
| 717 | block and reads as punctuation. A fixed corner is somewhere the eye |
||
| 718 | can learn. |
||
| 719 | |
||
| 720 | The block is the containing block, hence position: relative on it. |
||
| 721 | The mark sits outside the measure, in the margin the bleed already |
||
| 722 | uses, so it never reflows the text it belongs to. |
||
| 723 | |
||
| 724 | Hidden until the passage is hovered or the mark itself is focused, |
||
| 725 | so a page of prose is not littered with symbols; keyboard users |
||
| 726 | reach it by tabbing, which is why focus counts. */ |
||
| 727 | .contents :is(h1, h2, h3, h4)[id], |
||
| 728 | .contents p[id] { position: relative; } |
||
| 717 | .anchor-link { |
729 | .anchor-link { |
| 718 | margin-left: .4em; text-decoration: none; opacity: 0; |
||
| 719 | color: var(--text-muted); font-weight: normal; cursor: pointer; |
||
| 730 | position: absolute; top: 0; left: 100%; |
||
| 731 | margin-left: .5em; line-height: inherit; |
||
| 732 | text-decoration: none; opacity: 0; |
||
| 733 | color: var(--text-muted); font-weight: normal; |
||
| 720 | } |
734 | } |
| 721 | :is(h1, h2, h3, h4, p):hover > .anchor-link, |
735 | :is(h1, h2, h3, h4, p):hover > .anchor-link, |
| 722 | .anchor-link:focus { opacity: 1; } |
736 | .anchor-link:focus { opacity: 1; } |
cat << _EOF_ | |||
| 731 | box-shadow: 0 0 0 .4em var(--bg-secondary); |
745 | box-shadow: 0 0 0 .4em var(--bg-secondary); |
| 732 | border-radius: 2px; |
746 | border-radius: 2px; |
| 733 | } |
747 | } |
| 734 | .copy-toast { |
||
| 735 | position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em); |
||
| 736 | background-color: var(--bg-secondary); color: var(--text-primary); |
||
| 737 | border: 1px solid var(--border-light); border-radius: 3px; |
||
| 738 | padding: .5em 1em; font-size: .875em; z-index: 3000; |
||
| 739 | opacity: 0; pointer-events: none; |
||
| 740 | } |
||
| 741 | .copy-toast.show { opacity: 1; transform: translate(-50%, 0); } |
||
| 742 | /* Every heading is body size, which keeps the page even but leaves a |
748 | /* Every heading is body size, which keeps the page even but leaves a |
| 743 | section heading reading as a stray line of capitals: uppercase alone |
749 | section heading reading as a stray line of capitals: uppercase alone |
| 744 | does not say "new section". A rule above says it structurally rather |
750 | does not say "new section". A rule above says it structurally rather |
cat << _EOF_ | |||
| 991 | var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); |
997 | var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]'); |
| 992 | if (!targets.length) return; |
998 | if (!targets.length) return; |
| 993 | |
999 | |
| 994 | var toast = null; |
||
| 995 | function flash(msg) { |
||
| 996 | if (!toast) { |
||
| 997 | toast = document.createElement('div'); |
||
| 998 | toast.className = 'copy-toast'; |
||
| 999 | document.body.appendChild(toast); |
||
| 1000 | } |
||
| 1001 | toast.textContent = msg; |
||
| 1002 | toast.classList.add('show'); |
||
| 1003 | clearTimeout(toast._t); |
||
| 1004 | toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600); |
||
| 1005 | } |
||
| 1006 | |
||
| 1000 | // An ordinary link, not a copy button: following it puts the |
||
| 1001 | // address in the bar, and right-clicking offers copy-link the way |
||
| 1002 | // it does for every other link on the web. Nothing to learn. |
||
| 1007 | targets.forEach(function(el) { |
1003 | targets.forEach(function(el) { |
| 1008 | var a = document.createElement('a'); |
1004 | var a = document.createElement('a'); |
| 1009 | a.className = 'anchor-link'; |
1005 | a.className = 'anchor-link'; |
| 1010 | a.href = '#' + el.id; |
1006 | a.href = '#' + el.id; |
| 1011 | a.setAttribute('aria-label', 'Copy link to this passage'); |
||
| 1007 | a.setAttribute('aria-label', 'Link to this passage'); |
||
| 1012 | a.textContent = '\u00B6'; |
1008 | a.textContent = '\u00B6'; |
| 1013 | a.addEventListener('click', function(e) { |
||
| 1014 | e.preventDefault(); |
||
| 1015 | var url = location.origin + location.pathname + '#' + el.id; |
||
| 1016 | // The address bar is updated either way, so the link is |
||
| 1017 | // obtainable even where the clipboard is unavailable. |
||
| 1018 | history.replaceState(null, '', '#' + el.id); |
||
| 1019 | if (navigator.clipboard && navigator.clipboard.writeText) { |
||
| 1020 | navigator.clipboard.writeText(url).then( |
||
| 1021 | function() { flash('Link copied'); }, |
||
| 1022 | function() { flash('Press \u2318/Ctrl+C to copy'); } |
||
| 1023 | ); |
||
| 1024 | } else { |
||
| 1025 | flash('Press \u2318/Ctrl+C to copy'); |
||
| 1026 | } |
||
| 1027 | }); |
||
| 1028 | el.appendChild(a); |
1009 | el.appendChild(a); |
| 1029 | }); |
1010 | }); |
| 1030 | |
1011 | |
test/regressions.sh +17 -8
assert_file_contains "$dg/.dist/post/charted.html" '.contents :is(h1, h2) {' | |||
| 1423 | assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;' |
1423 | assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;' |
| 1424 | assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {' |
1424 | assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {' |
| 1425 | |
1425 | |
| 1426 | it "a passage offers a mark that copies its link" |
||
| 1426 | it "a passage offers a mark linking to itself" |
||
| 1427 | # Ids alone are undiscoverable -- a reader would have to open the source to |
1427 | # Ids alone are undiscoverable -- a reader would have to open the source to |
| 1428 | # find one. Hovering a heading or paragraph reveals a mark that copies the |
||
| 1429 | # full URL to that place. |
||
| 1428 | # find one. Hovering a heading or paragraph reveals an ordinary link to it. |
||
| 1430 | assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';" |
1429 | assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';" |
| 1431 | assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';" |
1430 | assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';" |
| 1432 | |
1431 | |
| 1432 | it "the mark is a plain link, not a copy button" |
||
| 1433 | # Following it puts the address in the bar, and right-clicking offers |
||
| 1434 | # copy-link the way it does for every other link on the web. Nothing to learn, |
||
| 1435 | # and nothing to go wrong where the clipboard is unavailable. |
||
| 1436 | assert_file_not_contains "$dg/.dist/chart.html" "navigator.clipboard" |
||
| 1437 | assert_file_not_contains "$dg/.dist/chart.html" "copy-toast" |
||
| 1438 | |
||
| 1439 | it "the mark is pinned to the top right of its block" |
||
| 1440 | # Trailing the last word put it wherever the text happened to stop, which is a |
||
| 1441 | # different place on every block and reads as punctuation. A fixed corner is |
||
| 1442 | # somewhere the eye can learn. Measured in Chrome: flush with the block top, |
||
| 1443 | # 8px past its right edge, and no mark overflows the viewport at 700px. |
||
| 1444 | assert_file_contains "$dg/.dist/chart.html" 'position: absolute; top: 0; left: 100%;' |
||
| 1445 | assert_file_contains "$dg/.dist/chart.html" '.contents p[id] { position: relative; }' |
||
| 1446 | |
||
| 1433 | it "the mark is hidden until the passage is hovered or the mark focused" |
1447 | it "the mark is hidden until the passage is hovered or the mark focused" |
| 1434 | # Otherwise a page of prose is littered with symbols. Focus counts too, or a |
1448 | # Otherwise a page of prose is littered with symbols. Focus counts too, or a |
| 1435 | # keyboard user could never reach it. |
1449 | # keyboard user could never reach it. |
| 1436 | assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }' |
1450 | assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }' |
| 1437 | assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }' |
1451 | assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }' |
| 1438 | |
1452 | |
| 1439 | it "the link is obtainable even without the clipboard" |
||
| 1440 | # The address bar is updated whether or not the copy succeeds, so the reader |
||
| 1441 | # can always take the link from there. |
||
| 1442 | assert_file_contains "$dg/.dist/chart.html" "history.replaceState(null, '', '#' + el.id);" |
||
| 1443 | |
||
| 1444 | it "following a permalink marks what was linked" |
1453 | it "following a permalink marks what was linked" |
| 1445 | # A reader landing mid-document needs to see which passage was meant. |
1454 | # A reader landing mid-document needs to see which passage was meant. |
| 1446 | assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');" |
1455 | assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');" |
Raw
commit 7d61ceb33b32c6c0e4e0bbc26cbdaeb3143b4d58
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date: Tue Sep 1 09:14:52 2026 +0800
Make the deep-link mark an ordinary link in the corner
Two changes to a feature that was doing more than it needed to.
The mark no longer copies to the clipboard. It is a plain anchor, so
following it puts the address in the bar and right-clicking offers
copy-link the way it does for every other link on the web -- nothing to
learn, nothing to fail where the clipboard is unavailable, and no toast
to explain what just happened. The toast styles go with it; nothing
created the element any more.
And it is pinned to the top right of its block rather than trailing the
last word. At the end of a paragraph it sat wherever the text happened to
stop, which is a different place on every block and reads as punctuation
attached to the final sentence. In the corner the marks line up in a
column the eye can learn, and being absolutely positioned they cannot
reflow the text they belong to.
The block is the containing block, so it takes position: relative. The
mark sits in the margin the figure bleed already uses; measured at 1440
and at 700, no mark overflows the viewport and the page gains no
sideways scroll. Both layouts measured: flush with the block top, 8px
past its right edge.
diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 3c48745..a8ad0d9 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -890,12 +890,26 @@ cat << _EOF_
while their siblings were bold. Deeper headings stay regular -- they
sit inside a section rather than opening one. */
.contents :is(h1, h2) { font-weight: bold; }
- /* Deep-link mark. Hidden until the passage is hovered or the mark
- itself is focused, so a page of prose is not littered with symbols;
- keyboard users reach it by tabbing, which is why focus counts. */
+ /* Deep-link mark, pinned to the top right of the block rather than
+ trailing the last word: at the end of a paragraph it sat wherever
+ the text happened to stop, which is a different place on every
+ block and reads as punctuation. A fixed corner is somewhere the eye
+ can learn.
+
+ The block is the containing block, hence position: relative on it.
+ The mark sits outside the measure, in the margin the bleed already
+ uses, so it never reflows the text it belongs to.
+
+ Hidden until the passage is hovered or the mark itself is focused,
+ so a page of prose is not littered with symbols; keyboard users
+ reach it by tabbing, which is why focus counts. */
+ .contents :is(h1, h2, h3, h4)[id],
+ .contents p[id] { position: relative; }
.anchor-link {
- margin-left: .4em; text-decoration: none; opacity: 0;
- color: var(--text-muted); font-weight: normal; cursor: pointer;
+ position: absolute; top: 0; left: 100%;
+ margin-left: .5em; line-height: inherit;
+ text-decoration: none; opacity: 0;
+ color: var(--text-muted); font-weight: normal;
}
:is(h1, h2, h3, h4, p):hover > .anchor-link,
.anchor-link:focus { opacity: 1; }
@@ -910,14 +924,6 @@ cat << _EOF_
box-shadow: 0 0 0 .4em var(--bg-secondary);
border-radius: 2px;
}
- .copy-toast {
- position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
- background-color: var(--bg-secondary); color: var(--text-primary);
- border: 1px solid var(--border-light); border-radius: 3px;
- padding: .5em 1em; font-size: .875em; z-index: 3000;
- opacity: 0; pointer-events: none;
- }
- .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Every heading is body size, which keeps the page even but leaves a
section heading reading as a stray line of capitals: uppercase alone
does not say "new section". A rule above says it structurally rather
@@ -1219,40 +1225,15 @@ cat << _EOF_
var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
if (!targets.length) return;
- var toast = null;
- function flash(msg) {
- if (!toast) {
- toast = document.createElement('div');
- toast.className = 'copy-toast';
- document.body.appendChild(toast);
- }
- toast.textContent = msg;
- toast.classList.add('show');
- clearTimeout(toast._t);
- toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
- }
-
+ // An ordinary link, not a copy button: following it puts the
+ // address in the bar, and right-clicking offers copy-link the way
+ // it does for every other link on the web. Nothing to learn.
targets.forEach(function(el) {
var a = document.createElement('a');
a.className = 'anchor-link';
a.href = '#' + el.id;
- a.setAttribute('aria-label', 'Copy link to this passage');
+ a.setAttribute('aria-label', 'Link to this passage');
a.textContent = '\u00B6';
- a.addEventListener('click', function(e) {
- e.preventDefault();
- var url = location.origin + location.pathname + '#' + el.id;
- // The address bar is updated either way, so the link is
- // obtainable even where the clipboard is unavailable.
- history.replaceState(null, '', '#' + el.id);
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(url).then(
- function() { flash('Link copied'); },
- function() { flash('Press \u2318/Ctrl+C to copy'); }
- );
- } else {
- flash('Press \u2318/Ctrl+C to copy');
- }
- });
el.appendChild(a);
});
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index 1f05596..0785ce5 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -711,12 +711,26 @@ cat << _EOF_
while their siblings were bold. Deeper headings stay regular -- they
sit inside a section rather than opening one. */
.contents :is(h1, h2) { font-weight: bold; }
- /* Deep-link mark. Hidden until the passage is hovered or the mark
- itself is focused, so a page of prose is not littered with symbols;
- keyboard users reach it by tabbing, which is why focus counts. */
+ /* Deep-link mark, pinned to the top right of the block rather than
+ trailing the last word: at the end of a paragraph it sat wherever
+ the text happened to stop, which is a different place on every
+ block and reads as punctuation. A fixed corner is somewhere the eye
+ can learn.
+
+ The block is the containing block, hence position: relative on it.
+ The mark sits outside the measure, in the margin the bleed already
+ uses, so it never reflows the text it belongs to.
+
+ Hidden until the passage is hovered or the mark itself is focused,
+ so a page of prose is not littered with symbols; keyboard users
+ reach it by tabbing, which is why focus counts. */
+ .contents :is(h1, h2, h3, h4)[id],
+ .contents p[id] { position: relative; }
.anchor-link {
- margin-left: .4em; text-decoration: none; opacity: 0;
- color: var(--text-muted); font-weight: normal; cursor: pointer;
+ position: absolute; top: 0; left: 100%;
+ margin-left: .5em; line-height: inherit;
+ text-decoration: none; opacity: 0;
+ color: var(--text-muted); font-weight: normal;
}
:is(h1, h2, h3, h4, p):hover > .anchor-link,
.anchor-link:focus { opacity: 1; }
@@ -731,14 +745,6 @@ cat << _EOF_
box-shadow: 0 0 0 .4em var(--bg-secondary);
border-radius: 2px;
}
- .copy-toast {
- position: fixed; left: 50%; bottom: 2em; transform: translate(-50%, .5em);
- background-color: var(--bg-secondary); color: var(--text-primary);
- border: 1px solid var(--border-light); border-radius: 3px;
- padding: .5em 1em; font-size: .875em; z-index: 3000;
- opacity: 0; pointer-events: none;
- }
- .copy-toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Every heading is body size, which keeps the page even but leaves a
section heading reading as a stray line of capitals: uppercase alone
does not say "new section". A rule above says it structurally rather
@@ -991,40 +997,15 @@ cat << _EOF_
var targets = document.querySelectorAll('.contents :is(h1,h2,h3,h4)[id], .contents p[id]');
if (!targets.length) return;
- var toast = null;
- function flash(msg) {
- if (!toast) {
- toast = document.createElement('div');
- toast.className = 'copy-toast';
- document.body.appendChild(toast);
- }
- toast.textContent = msg;
- toast.classList.add('show');
- clearTimeout(toast._t);
- toast._t = setTimeout(function() { toast.classList.remove('show'); }, 1600);
- }
-
+ // An ordinary link, not a copy button: following it puts the
+ // address in the bar, and right-clicking offers copy-link the way
+ // it does for every other link on the web. Nothing to learn.
targets.forEach(function(el) {
var a = document.createElement('a');
a.className = 'anchor-link';
a.href = '#' + el.id;
- a.setAttribute('aria-label', 'Copy link to this passage');
+ a.setAttribute('aria-label', 'Link to this passage');
a.textContent = '\u00B6';
- a.addEventListener('click', function(e) {
- e.preventDefault();
- var url = location.origin + location.pathname + '#' + el.id;
- // The address bar is updated either way, so the link is
- // obtainable even where the clipboard is unavailable.
- history.replaceState(null, '', '#' + el.id);
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(url).then(
- function() { flash('Link copied'); },
- function() { flash('Press \u2318/Ctrl+C to copy'); }
- );
- } else {
- flash('Press \u2318/Ctrl+C to copy');
- }
- });
el.appendChild(a);
});
diff --git a/test/regressions.sh b/test/regressions.sh
index 6315aac..416e45e 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -1423,24 +1423,33 @@ assert_file_contains "$dg/.dist/post/charted.html" '.contents :is(h1, h2) {'
assert_file_contains "$dg/.dist/chart.html" 'width: 3ch;'
assert_file_contains "$dg/.dist/chart.html" '.contents :is(h1, h2)::before {'
-it "a passage offers a mark that copies its link"
+it "a passage offers a mark linking to itself"
# Ids alone are undiscoverable -- a reader would have to open the source to
-# find one. Hovering a heading or paragraph reveals a mark that copies the
-# full URL to that place.
+# find one. Hovering a heading or paragraph reveals an ordinary link to it.
assert_file_contains "$dg/.dist/chart.html" "a.className = 'anchor-link';"
assert_file_contains "$dg/.dist/post/charted.html" "a.className = 'anchor-link';"
+it "the mark is a plain link, not a copy button"
+# Following it puts the address in the bar, and right-clicking offers
+# copy-link the way it does for every other link on the web. Nothing to learn,
+# and nothing to go wrong where the clipboard is unavailable.
+assert_file_not_contains "$dg/.dist/chart.html" "navigator.clipboard"
+assert_file_not_contains "$dg/.dist/chart.html" "copy-toast"
+
+it "the mark is pinned to the top right of its block"
+# Trailing the last word put it wherever the text happened to stop, which is a
+# different place on every block and reads as punctuation. A fixed corner is
+# somewhere the eye can learn. Measured in Chrome: flush with the block top,
+# 8px past its right edge, and no mark overflows the viewport at 700px.
+assert_file_contains "$dg/.dist/chart.html" 'position: absolute; top: 0; left: 100%;'
+assert_file_contains "$dg/.dist/chart.html" '.contents p[id] { position: relative; }'
+
it "the mark is hidden until the passage is hovered or the mark focused"
# Otherwise a page of prose is littered with symbols. Focus counts too, or a
# keyboard user could never reach it.
assert_file_contains "$dg/.dist/chart.html" '.anchor-link:focus { opacity: 1; }'
assert_file_contains "$dg/.dist/chart.html" '@media (hover: none) { .anchor-link { display: none; } }'
-it "the link is obtainable even without the clipboard"
-# The address bar is updated whether or not the copy succeeds, so the reader
-# can always take the link from there.
-assert_file_contains "$dg/.dist/chart.html" "history.replaceState(null, '', '#' + el.id);"
-
it "following a permalink marks what was linked"
# A reader landing mid-document needs to see which passage was meant.
assert_file_contains "$dg/.dist/chart.html" "t.classList.add('target-flash');"