Skip to content

Commit f8bc4d1

Browse files
committed
Merge branch 'main' of github.com:quarto-dev/quarto-cli into main
2 parents f09f260 + 1faeab4 commit f8bc4d1

File tree

2 files changed

+142
-2
lines changed

2 files changed

+142
-2
lines changed

src/resources/formats/revealjs/plugins/line-highlight/plugin.js

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ window.QuartoLineHighlight = function () {
3333
// move attributes on code block
3434
code.setAttribute(kCodeLineNumbersAttr, codeLineAttr);
3535

36+
const scrollState = { currentBlock: code };
37+
3638
// Check if there are steps and duplicate code block accordingly
3739
const highlightSteps = splitLineNumbers(codeLineAttr);
3840
if (highlightSteps.length > 1) {
@@ -84,7 +86,23 @@ window.QuartoLineHighlight = function () {
8486
fragmentBlock.removeAttribute(kFragmentIndex);
8587
}
8688

87-
// TODO add scrolling animation
89+
// Scroll highlights into view as we step through them
90+
fragmentBlock.addEventListener(
91+
"visible",
92+
scrollHighlightedLineIntoView.bind(
93+
this,
94+
fragmentBlock,
95+
scrollState
96+
)
97+
);
98+
fragmentBlock.addEventListener(
99+
"hidden",
100+
scrollHighlightedLineIntoView.bind(
101+
this,
102+
fragmentBlock.previousSibling,
103+
scrollState
104+
)
105+
);
88106
}
89107
);
90108
code.removeAttribute(kFragmentIndex);
@@ -93,7 +111,22 @@ window.QuartoLineHighlight = function () {
93111
joinLineNumbers([highlightSteps[0]])
94112
);
95113
}
96-
// TODO add scrolling animation: scroll the first highlight into view when the slide
114+
115+
// Scroll the first highlight into view when the slide becomes visible.
116+
const slide =
117+
typeof code.closest === "function"
118+
? code.closest("section:not(.stack)")
119+
: null;
120+
if (slide) {
121+
const scrollFirstHighlightIntoView = function () {
122+
scrollHighlightedLineIntoView(code, scrollState, true);
123+
slide.removeEventListener(
124+
"visible",
125+
scrollFirstHighlightIntoView
126+
);
127+
};
128+
slide.addEventListener("visible", scrollFirstHighlightIntoView);
129+
}
97130

98131
highlightCodeBlock(code);
99132
});
@@ -143,6 +176,93 @@ window.QuartoLineHighlight = function () {
143176
}
144177
}
145178

179+
/**
180+
* Animates scrolling to the first highlighted line
181+
* in the given code block.
182+
*/
183+
function scrollHighlightedLineIntoView(block, scrollState, skipAnimation) {
184+
window.cancelAnimationFrame(scrollState.animationFrameID);
185+
186+
// Match the scroll position of the currently visible
187+
// code block
188+
if (scrollState.currentBlock) {
189+
block.scrollTop = scrollState.currentBlock.scrollTop;
190+
}
191+
192+
// Remember the current code block so that we can match
193+
// its scroll position when showing/hiding fragments
194+
scrollState.currentBlock = block;
195+
196+
const highlightBounds = getHighlightedLineBounds(block);
197+
let viewportHeight = block.offsetHeight;
198+
199+
// Subtract padding from the viewport height
200+
const blockStyles = window.getComputedStyle(block);
201+
viewportHeight -=
202+
parseInt(blockStyles.paddingTop) + parseInt(blockStyles.paddingBottom);
203+
204+
// Scroll position which centers all highlights
205+
const startTop = block.scrollTop;
206+
let targetTop =
207+
highlightBounds.top +
208+
(Math.min(highlightBounds.bottom - highlightBounds.top, viewportHeight) -
209+
viewportHeight) /
210+
2;
211+
212+
// Make sure the scroll target is within bounds
213+
targetTop = Math.max(
214+
Math.min(targetTop, block.scrollHeight - viewportHeight),
215+
0
216+
);
217+
218+
if (skipAnimation === true || startTop === targetTop) {
219+
block.scrollTop = targetTop;
220+
} else {
221+
// Don't attempt to scroll if there is no overflow
222+
if (block.scrollHeight <= viewportHeight) return;
223+
224+
let time = 0;
225+
226+
const animate = function () {
227+
time = Math.min(time + 0.02, 1);
228+
229+
// Update our eased scroll position
230+
block.scrollTop =
231+
startTop + (targetTop - startTop) * easeInOutQuart(time);
232+
233+
// Keep animating unless we've reached the end
234+
if (time < 1) {
235+
scrollState.animationFrameID = requestAnimationFrame(animate);
236+
}
237+
};
238+
239+
animate();
240+
}
241+
}
242+
243+
function getHighlightedLineBounds(block) {
244+
const highlightedLines = block.querySelectorAll(".highlight-line");
245+
if (highlightedLines.length === 0) {
246+
return { top: 0, bottom: 0 };
247+
} else {
248+
const firstHighlight = highlightedLines[0];
249+
const lastHighlight = highlightedLines[highlightedLines.length - 1];
250+
251+
return {
252+
top: firstHighlight.offsetTop,
253+
bottom: lastHighlight.offsetTop + lastHighlight.offsetHeight,
254+
};
255+
}
256+
}
257+
258+
/**
259+
* The easing function used when scrolling.
260+
*/
261+
function easeInOutQuart(t) {
262+
// easeInOutQuart
263+
return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;
264+
}
265+
146266
function splitLineNumbers(lineNumbersAttr) {
147267
// remove space
148268
lineNumbersAttr = lineNumbersAttr.replace("/s/g", "");
@@ -196,6 +316,22 @@ window.QuartoLineHighlight = function () {
196316
id: "quarto-line-highlight",
197317
init: function (deck) {
198318
initQuartoLineHighlight(deck);
319+
320+
// If we're printing to PDF, scroll the code highlights of
321+
// all blocks in the deck into view at once
322+
deck.on("pdf-ready", function () {
323+
[].slice
324+
.call(
325+
deck
326+
.getRevealElement()
327+
.querySelectorAll(
328+
"pre code[data-code-line-numbers].current-fragment"
329+
)
330+
)
331+
.forEach(function (block) {
332+
scrollHighlightedLineIntoView(block, {}, true);
333+
});
334+
});
199335
},
200336
};
201337
};

src/resources/formats/revealjs/plugins/line-highlight/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ pre.numberSource code > span > a:first-child::before {
2525
width: 100%;
2626
box-sizing: border-box;
2727
}
28+
29+
.reveal div.sourceCode pre code {
30+
min-height: 100%;
31+
}

0 commit comments

Comments
 (0)