Skip to content

Commit

Permalink
Merge pull request #438 from CodeYourFuture/feature/solo-view
Browse files Browse the repository at this point in the history
Feature: solo view for prep
  • Loading branch information
SallyMcGrath authored Dec 15, 2023
2 parents 6378c9e + f0c63fd commit 3c4769c
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 21 deletions.
200 changes: 200 additions & 0 deletions assets/scripts/solo-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
class SoloView extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });

// State object
this.state = {
currentBlockIndex: 0,
blocks: [],
tocLinks: [],
navButtons: { back: null, next: null },
fragment: null,
touchStartX: 0,
touchEndX: 0,
};
// Adding window and doc event listeners
window.addEventListener("hashchange", this.handleFragment.bind(this));
document.addEventListener("keydown", this.handleKeydown.bind(this));
}

connectedCallback() {
this.render(); // Render the component
this.cacheDOM(); // Cache necessary DOM elements
this.addEventListeners(); // Setup event listeners
this.handleFragment(); // Check for a fragment in the URL and set to this view if present
this.updateView(); // Initial view update
}

disconnectedCallback() {
// Removing window and doc event listeners
window.removeEventListener("hashchange", this.handleFragment.bind(this));
document.removeEventListener("keydown", this.handleKeydown.bind(this));
}

// Cache DOM elements
cacheDOM() {
this.state.blocks = [...this.querySelectorAll('[slot="blocks"] .c-block')];
this.state.tocLinks = [
...this.querySelectorAll('[slot="header"] .c-toc li a'),
];
this.state.navButtons.back = this.querySelector(
'[slot="nav"] .c-solo-view__back'
);
this.state.navButtons.next = this.querySelector(
'[slot="nav"] .c-solo-view__next'
);
}

// Add event listeners
addEventListeners() {
this.state.tocLinks.forEach((link, index) => {
link.addEventListener("click", () => this.updateCurrentBlockIndex(index));
});

this.state.navButtons.back.addEventListener("click", this.navigateBack);
this.state.navButtons.next.addEventListener("click", this.navigateNext);

this.addEventListener(
"touchstart",
(event) => {
this.state.touchStartX = event.changedTouches[0].clientX;
},
{ passive: true }
);

this.addEventListener(
"touchend",
(event) => {
const touchEndX = event.changedTouches[0].clientX;
this.handleSwipeGesture(this.state.touchStartX, touchEndX);
},
{ passive: true }
);

this.addEventListener("keydown", this.handleKeydown);
}

// Update current block index
updateCurrentBlockIndex(index) {
this.state.currentBlockIndex = index;
this.updateView();
}

// Navigation logic
navigateBack = (event) => {
event.preventDefault();
const backIndex = this.state.currentBlockIndex - 1;
this.state.tocLinks[backIndex].click();
};

navigateNext = (event) => {
event.preventDefault();
const nextIndex = this.state.currentBlockIndex + 1;
if (nextIndex < this.state.blocks.length) {
this.state.tocLinks[nextIndex].click();
}
};

// Handle swipe gesture
handleSwipeGesture = (startX, endX) => {
const deltaX = endX - startX;
const swipeThreshold = 30;
if (deltaX > swipeThreshold) {
// Swipe right (previous)
this.navigateBack(new Event("swipe"));
} else if (deltaX < -swipeThreshold) {
// Swipe left (next)
this.navigateNext(new Event("swipe"));
}
};

handleKeydown = (event) => {
if (event.key === "ArrowLeft") {
this.navigateBack(event);
}
if (event.key === "ArrowRight") {
this.navigateNext(event);
}
};

// look for a fragment in the URL and navigate to it if one exists so we can link directly to a view
handleFragment = () => {
const fragment = window.location.hash.substring(1);

if (fragment === "toc") {
this.state.currentBlockIndex = 0;
this.updateView();
} else if (fragment) {
const matchingLinkIndex = this.state.tocLinks.findIndex(
(link) => link.getAttribute("href").substring(1) === fragment
);
if (matchingLinkIndex !== -1) {
this.state.currentBlockIndex = matchingLinkIndex;
this.updateView();
}
}
};

// Update view
updateView() {
this.state.blocks.forEach((block, index) => {
block.hidden = index !== this.state.currentBlockIndex;
});

this.state.tocLinks.forEach((link, index) => {
link.classList.toggle(
"is-active",
index === this.state.currentBlockIndex
);
});

// Hide unusable buttons
this.state.navButtons.back.style.display =
this.state.currentBlockIndex === 0 ? "none" : "inline-flex";
this.state.navButtons.next.style.display =
this.state.currentBlockIndex === this.state.blocks.length - 1
? "none"
: "inline-flex";
}

// Render method with slots and CSS
render() {
this.shadowRoot.innerHTML = `
<style>
@media (min-width: 768px) {
:host {
display: grid;
grid-template:
"sidebar blocks" auto
"sidebar nav" min-content
"sidebar . " 1fr / 1fr 4fr;
gap: var(--theme-spacing--gutter);
}
::slotted([slot="header"]) {
position: sticky;
top: 0;
grid-area: sidebar;
}
::slotted([slot="blocks"]) {
padding-top: var(--theme-spacing--6);
--theme-spacing--scrollmargin: 100vh !important;
grid-area: blocks;
}
::slotted([slot="nav"]) {
grid-area: nav;
margin: 0 0 auto auto;
}
}
</style>
<slot name="header"></slot>
<slot name="blocks"></slot>
<slot name="nav"></slot>
`;
}
}

customElements.define("solo-view", SoloView);
8 changes: 4 additions & 4 deletions assets/styles/02-variables/spacing.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
$c-min: 300px;
$c-max: 1200px; // this is here so you can reference in media queries if you want
$c-med: 768px;
$c-max: 1280px; // this is here so you can reference in media queries if you want

$spacing__levels: 1, 2, 3, 4, 5, 6;
:root {
Expand All @@ -19,11 +20,10 @@ $spacing__levels: 1, 2, 3, 4, 5, 6;

--theme-spacing--container-min: #{$c-min};
--theme-spacing--container-max: #{$c-max};
--theme-spacing--container-med: #{$c-med};
--theme-spacing--gutter: var(--theme-spacing--2);
--theme-spacing--offset: var(--theme-spacing--1); //set by eye is fine
--theme-spacing--linelength: 80ch; // readability maximum
--theme-spacing--touchtarget: 48px; // access minimum
--theme-spacing--menu: calc(
var(--theme-spacing--gutter) * 2
); // the big long menu trigger
--theme-spacing--scrollmargin: var(--theme-spacing--6);
}
1 change: 0 additions & 1 deletion assets/styles/03-elements/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ html {

html {
overflow-x: hidden;
scroll-behavior: smooth;
}

@keyframes fade-in {
Expand Down
4 changes: 4 additions & 0 deletions assets/styles/03-elements/misc-phrasing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ blockquote {
border-image: var(--theme-border-image);
}

iframe {
max-width: 100%; //never break out right even if rogue devs add giant iframes
}

code,
.code,
pre code .highlight pre,
Expand Down
1 change: 1 addition & 0 deletions assets/styles/04-components/block.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
> a:not([class]) {
text-decoration: none;
}
scroll-margin-top: var(--theme-spacing--scrollmargin);
}

&--youtube {
Expand Down
3 changes: 3 additions & 0 deletions assets/styles/04-components/copy.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
max-width: var(--theme-spacing--linelength);
}
margin-bottom: var(--theme-spacing--gutter);
li {
margin-bottom: reset;
}
h1 {
font-size: var(--theme-type-size--2);
}
Expand Down
23 changes: 23 additions & 0 deletions assets/styles/04-components/page-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@
/ 1fr var(--gap) minmax(0, 1fr);

.c-page-header--toc & {
grid-template:
". " var(--gap)
"breadcrumbs" min-content
"title " min-content
". " var(--gap)
"subtitle " min-content
"toc " min-content
". " var(--gap)
"description" 1fr
". " var(--gap)
/ 1fr;
}
.c-page-header--toc:not(.c-page-header--solo) & {
@media (min-width: $c-max) {
grid-template:
". . . " var(--gap)
Expand Down Expand Up @@ -81,5 +94,15 @@

&__toc {
scroll-margin-top: calc(2 * var(--header-height));

.c-toc {
max-height: 25vh;
}
.c-page-header--solo & .c-toc {
max-height: none;
@media (max-width: $c-med) {
max-height: 10vh;
}
}
}
}
6 changes: 5 additions & 1 deletion assets/styles/04-components/toc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
max-width: var(--theme-spacing--container);
border-image: var(--theme-border-image);
box-shadow: var(--theme-box-shadow);
max-height: 25vh;
padding: var(--theme-spacing--1) var(--theme-spacing--2)
var(--theme-spacing--4);
overflow-y: auto;
Expand Down Expand Up @@ -38,6 +37,11 @@
a,
a:link {
text-decoration: none;
position: relative;
&.is-active {
color: var(--theme-color--pop);
border-bottom: 1px solid var(--theme-color--pop);
}
}

li {
Expand Down
2 changes: 1 addition & 1 deletion content/en/js1/sprints/1/prep/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ name= "Critical Thinking"
src= "https://cyf-pd.netlify.app/blocks/prep-critical-thinking/readme/"
+++

# Interacting with computers.
## Interacting with computers.

Modern computers are complicated: it would be too difficult and time-consuming to list all the components that make up a modern computer. So to build our mental model, we will use this simple definition of a computer:

Expand Down
37 changes: 24 additions & 13 deletions layouts/_default/prep.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
{{ define "main" }}
<article>
<solo-view tabindex="0">
{{ .Scratch.Set "emoji" "🧑🏾‍💻" }}
{{ .Scratch.Set "headerClass"
"c-page-header--toc"
"c-page-header--solo"
}}
{{ partial "page-header.html" . }}
{{ with .Content }}
<section class="l-page__main c-block c-copy">{{ . }}</section>
{{ end }}
<div class="c-block__list">
<!-- Blocks will appear here in order listed in the blocks list in the sprint front matter -->
{{ range .Params.blocks }}
{{ partial "block/block.html" (dict "block" . "Page"
$.Page)
}}
<div slot="header">{{ partial "page-header.html" . }}</div>
<div slot="blocks">
{{ with .Content }}
<section class="l-page__main c-block c-copy">{{ . }}</section>
{{ end }}
<div class="c-block__list">
<!-- Blocks will appear here in order listed in the blocks list in the sprint front matter -->
{{ range .Params.blocks }}
{{ partial "block/block.html" (dict "block" . "Page"
$.Page)
}}
{{ end }}
</div>
</div>
</article>
{{ if gt .Params.blocks 1 }}
<nav slot="nav">
<a href="#" class="e-button c-solo-view__back">&larr; Back</a>
<a href="#" class="e-button c-solo-view__next">Next &rarr;</a>
</nav>
{{ end }}
</solo-view>

{{ $soloView := resources.Get "scripts/solo-view.js" | resources.Minify }}
<script src="{{ $soloView.Permalink }}" defer></script>
{{ end }}
3 changes: 2 additions & 1 deletion layouts/partials/page-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{{ end }}
{{ if (and (or (eq .Layout "prep") (eq .Layout "day-plan")) (gt .Params.blocks 1)) }}
{{ $hasTOC = true }}
{{ $mdTOC = true }}
{{ $blockTOC = true }}
{{ end }}
<header
Expand Down Expand Up @@ -53,7 +54,7 @@ <h2 class="c-page-header__description e-heading__3">
{{ end }}
<!--TOC-->
{{ if $hasTOC }}
<div class="c-page-header__toc is-none--lt-container" id="toc">
<div class="c-page-header__toc " id="toc">
<section class="c-toc">
<a id="top" class="c-toc__top e-button e-button--icon" href="#toc"
>🔖</a
Expand Down

0 comments on commit 3c4769c

Please sign in to comment.