Skip to content

Commit 8b53f1b

Browse files
authored
Merge pull request #3127 from GuillaumeGomez/zoom-in
Add zoomable images feature
2 parents 3ec0546 + beab2fa commit 8b53f1b

8 files changed

Lines changed: 204 additions & 7 deletions

File tree

crates/mdbook-html/front-end/css/general.css

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,56 @@ dd > p {
409409
/* Add some space between the icon and the text. */
410410
margin-right: 8px;
411411
}
412+
413+
/* All this code is to handle the "zoomable" images feature. */
414+
415+
/* This one makes the checkbox invisible while keeping it selectable with the keyboard (with the
416+
"tab" key). */
417+
.content .checkbox-img {
418+
position: absolute;
419+
width: 1px;
420+
height: 1px;
421+
padding: 0;
422+
margin: -1px;
423+
overflow: hidden;
424+
clip: rect(0, 0, 0, 0);
425+
white-space: nowrap;
426+
border: 0;
427+
}
428+
/* When the input is focused, we make it appear to the user by adding an outline to the img
429+
instead */
430+
.content .checkbox-img:focus + img {
431+
outline: auto;
432+
}
433+
/* The "zoomed-in" image is not displayed until the checkbox is "ticked". */
434+
.content .checkbox-img:not(:checked) ~ .img-wrapper {
435+
display: none;
436+
}
437+
.content .checkbox-label {
438+
display: block;
439+
max-width: 100%;
440+
}
441+
.content .checkbox-label img {
442+
cursor: zoom-in;
443+
}
444+
.content .checkbox-img:checked ~ .img-wrapper {
445+
position: fixed;
446+
top: 0;
447+
left: 0;
448+
width: 100vw;
449+
height: 100vh;
450+
background-color: rgba(0, 0, 0, 0.4);
451+
z-index: 1001;
452+
display: flex;
453+
align-items: center;
454+
justify-content: center;
455+
cursor: zoom-out;
456+
}
457+
.content .checkbox-img:checked ~ .img-wrapper > img {
458+
--img-padding: 5px;
459+
padding: var(--img-padding);
460+
background: #999;
461+
cursor: zoom-out;
462+
max-width: calc(100% - (var(--img-padding) * 2));
463+
max-height: calc(100% - (var(--img-padding) * 2));
464+
}

crates/mdbook-html/front-end/js/book.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ function mdbook_something_else_has_focus(e) {
2727
// Check composedPath in case the event happened from something generated
2828
// from the shadowDOM.
2929
const target = e.composedPath()[0] || e.target;
30+
// If this is the `checkbox-img` input which has the focus, we want to handle it here.
31+
if (target.classList.contains('checkbox-img')) {
32+
return false;
33+
}
3034
return /^(?:input|select|textarea)$/i.test(target.nodeName);
3135
}
3236

@@ -658,6 +662,12 @@ aria-label="Show hidden lines"></button>';
658662
})();
659663

660664
(function chapterNavigation() {
665+
function zoomOutImages() {
666+
for (const elem of Array.from(document.querySelectorAll('input.checkbox-img'))) {
667+
elem.checked = false;
668+
}
669+
}
670+
661671
document.addEventListener('keydown', function(e) {
662672
if (e.altKey ||
663673
e.ctrlKey ||
@@ -724,6 +734,9 @@ aria-label="Show hidden lines"></button>';
724734
e.preventDefault();
725735
showHelp();
726736
break;
737+
case 'Escape':
738+
zoomOutImages();
739+
break;
727740
}
728741

729742
// Rest of the keys are only active when the Shift key is not pressed

crates/mdbook-html/src/html/tree.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use super::tokenizer::parse_html;
99
use super::{HtmlRenderOptions, hide_lines, wrap_rust_main};
1010
use crate::utils::{id_from_content, unique_id};
1111
use ego_tree::{NodeId, NodeRef, Tree};
12-
use html5ever::tendril::StrTendril;
12+
use html5ever::tendril::{SliceExt, StrTendril};
1313
use html5ever::tokenizer::{TagKind, Token};
1414
use html5ever::{LocalName, QualName};
1515
use indexmap::IndexMap;
@@ -69,7 +69,7 @@ impl Node {
6969
}
7070

7171
/// An HTML element.
72-
#[derive(Debug)]
72+
#[derive(Debug, Clone)]
7373
pub(crate) struct Element {
7474
/// The tag name.
7575
pub(crate) name: QualName,
@@ -565,8 +565,42 @@ where
565565
}
566566
// This will eat TagEnd::Image
567567
let alt = self.text_for_img_alt();
568-
img.insert_attr("alt", alt.into());
569-
self.append(Node::Element(img));
568+
img.insert_attr("alt", alt.to_tendril());
569+
570+
// If the image is not being rendered inside a link, we can enable the "zoom-in"
571+
// feature.
572+
if !self
573+
.tag_stack
574+
.iter()
575+
.filter_map(|node_id| {
576+
self.tree
577+
.get(*node_id)
578+
.and_then(|el| el.value().as_element())
579+
})
580+
.any(|el| *el.name.local == *"a")
581+
{
582+
let mut label = Element::new("label");
583+
label.insert_attr("class", "checkbox-label".to_tendril());
584+
self.push(Node::Element(label));
585+
586+
let mut input = Element::new("input");
587+
input.insert_attr("class", "checkbox-img".to_tendril());
588+
input.insert_attr("type", "checkbox".to_tendril());
589+
self.append(Node::Element(input));
590+
591+
self.append(Node::Element(img.clone()));
592+
593+
let mut wrapper = Element::new("span");
594+
wrapper.insert_attr("class", "img-wrapper".to_tendril());
595+
self.push_no_stack(Node::Element(wrapper));
596+
597+
self.append(Node::Element(img));
598+
599+
// We exit the `label` and the `span` (which used push_no_stack).
600+
self.pop();
601+
} else {
602+
self.append(Node::Element(img));
603+
}
570604
return;
571605
}
572606
Tag::MetadataBlock(_) => {

guide/src/format/markdown.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,7 @@ This feature is enabled by default.
318318
To disable it, see the [`output.html.admonitions`] config option.
319319

320320
[`output.html.admonitions`]: configuration/renderers.md#html-renderer-options
321+
322+
## Zoom-in
323+
324+
All images in the chapters content have a "zoom-in" feature: you can click on it to make it bigger, and click it again to zoom out. You can focus the image with the keyboard as well, and press the spacebar to zoom in and out as well.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# Chapter 1
22

33
Side by side [![the logo](rust-logo.svg)](link) [![the logo](rust-logo.svg)](link).
4+
5+
Some text [blob](a).
6+
7+
![rust logo](rust-logo.svg)
8+
9+
[![the logo](rust-logo.svg)](https://rust-lang.org/)

tests/gui/image-zoom.goml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// This test ensures that the image zoom-in/zoom-out works as expected.
2+
3+
define-function: (
4+
"check-image-not-zoomed-in",
5+
[],
6+
block {
7+
// The "zoomed in" image should not be displayed.
8+
assert-css: (".checkbox-label > .img-wrapper", {"display": "none"})
9+
// The cursor for the image should be "zoom-in".
10+
assert-css: (".checkbox-label > img", {"cursor": "zoom-in"})
11+
},
12+
)
13+
14+
define-function: (
15+
"check-image-zoomed-in",
16+
[],
17+
block {
18+
// The image wrapper should now be displayed and have a "zoom-out" cursor.
19+
assert-css: (".checkbox-label > .img-wrapper", {"display": "flex", "cursor": "zoom-out"})
20+
// Same for the image it contains.
21+
assert-css: (
22+
".checkbox-label > .img-wrapper img",
23+
{"display": "block", "cursor": "zoom-out"},
24+
)
25+
},
26+
)
27+
28+
go-to: |DOC_PATH| + "basic/index.html"
29+
show-text: true
30+
call-function: ("check-image-not-zoomed-in", {})
31+
32+
// We click on the image to "zoom in".
33+
click: ".checkbox-label > img"
34+
call-function: ("check-image-zoomed-in", {})
35+
36+
// We click on the wrapper to "zoom out".
37+
click: ".checkbox-label > img"
38+
// We check that everything is back to the previous state.
39+
call-function: ("check-image-not-zoomed-in", {})
40+
41+
// We test that the "escape" key also hides the "zoomed in" image.
42+
// We click on the image to "zoom in".
43+
click: ".checkbox-label > img"
44+
call-function: ("check-image-zoomed-in", {})
45+
press-key: "Escape"
46+
// We check that everything is back to the previous state.
47+
call-function: ("check-image-not-zoomed-in", {})
48+
49+
// We test that we can zoom in and out using only the keyboard.
50+
// First we focus on the link just before the image we want to zoom in.
51+
focus: "a[href='a']"
52+
assert: "a[href='a']:focus"
53+
press-key: "Tab"
54+
// We check that the checkbox to "zoom in" is focused.
55+
assert: ".checkbox-img:focus"
56+
// We check that the image about to be zoomed in has the outline.
57+
store-css: (".checkbox-img:focus", {"outline": outline})
58+
// Because we're using "auto" to match the system outline, we cannot check the whole value directly
59+
// so instead we check if "auto" is present.
60+
assert-variable: (outline, "auto", CONTAINS)
61+
// We zoom in.
62+
press-key: "Space"
63+
call-function: ("check-image-zoomed-in", {})
64+
// We press the key again to zoom out.
65+
press-key: "Space"
66+
call-function: ("check-image-not-zoomed-in", {})
67+
// We now check it works with the "Escape" key as well.
68+
press-key: "Space"
69+
call-function: ("check-image-zoomed-in", {})
70+
press-key: "Escape"
71+
call-function: ("check-image-not-zoomed-in", {})
72+
73+
// Now we check that images in links (in `<a>` elements) don't get the "zoom-in" feature enabled.
74+
// There is only one zoomable image.
75+
assert-count: ("main .checkbox-img", 1)
76+
// We should have 5 images: 4 "normal" ones visible in the rendered book and one hidden which is
77+
// used only for the "zoom-in" feature.
78+
assert-count: ("main img", 5)
79+
// We check exact paths to ensure the 5 `<img>` are different.
80+
assert: ("main > p > label.checkbox-label > img:nth-of-type(1)")
81+
assert: ("main > p > label.checkbox-label > span.img-wrapper > img")
82+
assert: ("main > p:nth-of-type(1) > a:nth-of-type(1) > img")
83+
assert: ("main > p:nth-of-type(1) > a:nth-of-type(2) > img")
84+
assert: ("main > p:not(:nth-of-type(2)) > a > img")
85+
86+
// The cursor for the image in link should not have "zoom-in".
87+
assert-css: ("main > p > a > img", {"cursor": "pointer"})
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<h1 id="images"><a class="header" href="#images">Images</a></h1>
2-
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "></p>
3-
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"></p>
2+
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "><span class="img-wrapper"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break "></span></label></p>
3+
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"><span class="img-wrapper"><img src="https://rust-lang.org/logos/rust-logo-256x256.png" title="Some title" alt="Image with title"></span></label></p>

tests/testsuite/print/relative_links/expected/print.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ <h1 id="testing-relative-links-for-the-print-page"><a class="header" href="#test
1313
Link <a href="first/alpha/beta.html#anchor">inside but doesn’t exist with anchor</a>.
1414
Link <a href="first/alpha/gamma.html">inside to html</a>.
1515
Link <a href="first/alpha/gamma.html#anchor">inside to html with anchor</a>.</p>
16-
<p><img src="images/picture.png" alt="Some image"></p>
16+
<p><label class="checkbox-label"><input class="checkbox-img" type="checkbox"><img src="images/picture.png" alt="Some image"><span class="img-wrapper"><img src="images/picture.png" alt="Some image"></span></label></p>
1717
<p><a href="#first-nested">HTML Link</a></p>
1818
<img src="images/picture.png" alt="raw html">
1919
<h2 id="some-section"><a class="header" href="#some-section">Some section</a></h2>

0 commit comments

Comments
 (0)