Skip to content

Commit

Permalink
Adding initial version of carousel demo files
Browse files Browse the repository at this point in the history
  • Loading branch information
jonespm committed Aug 4, 2023
0 parents commit a516178
Show file tree
Hide file tree
Showing 7 changed files with 1,049 additions and 0 deletions.
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# canvas-carousel-demo

## Overview

This repository includes a collection of HTML, image, and JSON files that together form an announcement carousel
that can be installed or embedded on the Canvas home page (i.e. Dashboard).
The carousel makes use of Bootstrap (version 5), Bootstrap Icons, and native HTML and JavaScript functionality.

This carousel has been checked for accessibility and can be navigated with the keyboard and will announced the alt text if provided.

## Repository setup

This demo site can be forked on Github and either be left public or made private. Or you can just copy the files and host it where you like on S3 or any web host. At Michigan we just run the private repository as a Github Page the similar to the demo and make all our changes right in Github via the GUI. However we have an AWS CloudFront distribution setup as Github Pages has bandwidth limits. We typically have about 10 million requests and 500GB of traffic in a typical month and the costs total $10/month.

If you change the name of this repository you'll have to change the value of pathStem in the html file to something else. This may be useful to be configurable in the future.

`const pathStem = '/canvas-carousel-demo/';`

## Canvas installation

To enable the carousel, a Canvas admin must include the JavaScript below as part of a larger file uploaded in the Canvas custom theme editor.
Replace `{your domain}` with a domain that is publicly serving the `canvas_carousel_player.html` file
and the `content` and `data` directories under the `/canvas-carousel-demo/` path.
U-M ITS Teaching & Learning uses GitHub Pages to accomplish this, serving from [`tl-its-umich-edu.github.io/canvas-carousel-demo/](https://tl-its-umich-edu.github.io/canvas-carousel-demo/).

```
$(document).ready(function() {
// Add Carousel
$("#dashboard").before(`
<div style="margin-bottom: 25px;">
<iframe src='https://tl-its-umich-edu.github.io/canvas-carousel-demo/canvas_carousel_player.html' width="650" height="200" role="complementary" style="border: none;">
</iframe>
</div>
`);
});
```

Once added, the carousel will launch and begin cycling slides when the Canvas home page is loaded.

## Adding slide content

To add one or more new slides, perform the following steps in order:

1) Add one or more new image files to the [`/content/` directory](/content/) in this repository.
2) For each new file, add a new object to the existing array under the `"slides"` key in the JSON file
at [`data/slide-data.json`](/data/slide-data.json).
Each object must contain the following keys and appropriate string values:
- `imageFileName`: the name of an image file located in [`/content/`](/content/) in this repository
- `linkURL`: a URL to send the user to when they select the slide
- `altText`: alternative text for the slide image for use by assistive technologies

A template file can be found at [`data/slide-data-template.json`](/data/slide-data-template.json).

3) Before saving or committing changes to the JSON file, check that all key-value pairs are present and the JSON is valid.

To remove slides from the carousel, remove the corresponding objects from the array.
Slides will be presented in the order they are defined in the array.
While the carousel does not programmatically limit the number of slides,
a maximum of four slides is recommended to lessen the cognitive load on users.

## Settings

The carousel also supports a single setting currently that controls whether the carousel starts on a random slide.
To change this setting, modify the value for the `"startRandom"` key in the object under the `"settings"` key in the
[`data/slide-data.json`](/data/slide-data.json) file. A `true` value will make the starting slide random,
`false` will make the starting slide be the first slide in the array of objects defined under `"slides"` key.

## Local development

Since this application is a collection of basic static files, developers can test changes using a simple server
on `localhost`. There are a number of ways to accomplish; essentially, you serve the parent directory of the
`canvas-carousel-demo` directory.

For example, you can use the following steps in order to use a simple Python Web server.

1) Navigate to the project directory.
```
git clone https://github.com/tl-its-umich-edu/canvas-carousel-demo.git
cd canvas-carousel-demo
```

2) Run the server using `http`, targeting the parent directory.
```
python3 -m http.server --directory .
```

3) In your browser of choice, navigate to
[`http://localhost:8000/canvas-carousel-demo/canvas_carousel_player.html`](http://localhost:8000/canvas-carousel-demo/canvas_carousel_player.html).

## Resources

- [Canvas Theme Editor](https://community.canvaslms.com/t5/Admin-Guide/How-do-I-create-a-theme-for-an-account-using-the-Theme-Editor/ta-p/242)
- [Bootstrap Carousel](https://getbootstrap.com/docs/5.0/components/carousel/)
- [Boostrap Icons](https://icons.getbootstrap.com/)
- [Mozilla Developer Network - Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)
251 changes: 251 additions & 0 deletions canvas_carousel_player.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>carousel</title>

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">

<style type="text/css" media="screen">

/* Custom colors */
:root {
--mi-blue: #00274C;
--mi-blue-light: #0070D9;
}

/* Layout */
#canvas-carousel {
width: 650px;
height: 198.2px;
cursor: pointer;
}

.carousel-inner > .carousel-item > a {
margin-left: 50px;
}

.carousel-inner {
border: 1px solid #dddddd;
margin-block-end: 0px;
}

ul, menu, dir {
-webkit-padding-start: 0px;
}

ul, menu, dir {
-moz-padding-start: 0px;
}

/* Controls */

.btn-outline-primary {
--bs-btn-color: var(--mi-blue);
--bs-btn-border-color: var(--mi-blue);
--bs-btn-hover-color: white;
--bs-btn-hover-bg: var(--mi-blue-light) !important;
--bs-btn-hover-border-color: var(--mi-blue);
--bs-btn-active-color: white;
--bs-btn-active-bg: var(--mi-blue);
--bs-btn-active-border-color: var(--mi-blue);
}

/* Previous/Next */
.carousel-control-prev, .carousel-control-next {
width: 50px;
height: 170px;
z-index: 2;
opacity: 1.0 !important;
}

.carousel-control-prev .bi-chevron-left, .carousel-control-next .bi-chevron-right {
color: var(--mi-blue);
font-size: 32px;
}

.carousel-control-prev .bi-chevron-left:hover, .carousel-control-next .bi-chevron-right:hover {
color: var(--mi-blue-light);
}

/* visible focus rings */
.carousel-control-prev:focus-visible, .carousel-control-next:focus-visible, .carousel-item a:focus-visible {
outline: var(--bs-blue) auto 1px !important;
}

/* Play/Pause */
#play-pause {
position: absolute;
bottom: 35px;
right: 9px;
z-index: 999;
}

/* Indicators */
.carousel-indicators {
margin-bottom: 0 !important;
}

.carousel-indicators button {
height: 15px !important;
width: 15px !important;
border-radius: 100% !important;
opacity: 1 !important;
text-indent: 0 !important;
color: var(--mi-blue);
border: 1px solid var(--mi-blue) !important;
margin-left: 5px !important;
margin-right: 5px !important;
margin-bottom: 5px;
margin-top: 5px;
}

.carousel-indicators :is(button.active, button.active:hover, button.active:focus-visible) {
background-color: var(--mi-blue) !important;
color: white !important;
}

.carousel-indicators :is(button:hover, button:focus-visible) {
background-color: var(--mi-blue-light) !important;
color: white !important;
}

@media (prefers-reduced-motion) {
body {
display: none
}
}

</style>
</head>

<body class="d-none d-sm-block">
<div
id="canvas-carousel"
class="carousel slide carousel-fade"
role="complementary"
aria-label="Announcement Carousel"
>
<!-- Previous -->
<button class="carousel-control-prev" data-bs-target="#canvas-carousel" data-bs-slide="prev" title="Previous Slide" aria-label="Previous Slide">
<div class="bi bi-chevron-left"></div>
</button>
<!-- Wrapper for slides -->
<ul id="slide-list" class="carousel-inner" aria-live="polite">
</ul>
<!-- Next -->
<button class="carousel-control-next" data-bs-target="#canvas-carousel" data-bs-slide="next" title="Next Slide" aria-label="Next Slide">
<div class="bi bi-chevron-right"></div>
</button>
<!-- Wrapper for indicators -->
<div id="slide-indicators" class="carousel-indicators"></div>
<!-- Play/Pause -->
<div id="play-pause">
<button id="pause" data-bs-toggle="button" class="btn btn-sm btn-outline-primary" aria-label="Pause carousel">
<div id="play-pause-icon" class="bi bi-pause-fill"></div>
</button>
</div>
</div>
</body>
<script>
const pathStem = '/canvas-carousel-demo/';
const imagePathStem = pathStem + 'content/';
const dataPathStem = pathStem + 'data/';

/*
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
*/
function getRandomIndex (length) {
if (length < 1) return 0;
const max = Math.floor(length - 1);
return Math.floor(Math.random() * (max + 1));
}

function SlideEl (slide, active) {
const imgEl = document.createElement('img');
imgEl.setAttribute('src', imagePathStem + slide.imageFileName); // safe sink
imgEl.setAttribute('alt', slide.altText); // safe sink

const linkEl = document.createElement('a');
linkEl.setAttribute('href', slide.linkURL); // safe sink
linkEl.setAttribute('target', '_blank');
linkEl.append(imgEl);

const captionEl = document.createElement('p');
captionEl.classList.add('carousel-caption', 'visually-hidden');
captionEl.setAttribute('id', 'carousel-caption1');
captionEl.innerText = 'Select the link to learn more.';

const listItemEl = document.createElement('li');
listItemEl.classList.add('carousel-item');
if (active === true) listItemEl.classList.add('active');
listItemEl.append(linkEl, captionEl);
return listItemEl;
}

function IndicatorEl (index, active) {
const slideNumStr = String(index + 1);
const label = 'Slide ' + slideNumStr
const buttonEl = document.createElement('button');
buttonEl.setAttribute('data-bs-target', '#canvas-carousel');
buttonEl.setAttribute('data-bs-slide-to', String(index));
buttonEl.setAttribute('aria-label', label);
buttonEl.setAttribute('title', label);
buttonEl.classList.add('btn', 'btn-outline-primary');
if (active === true) {
buttonEl.classList.add('active');
buttonEl.setAttribute('aria-current', 'true');
}
return buttonEl;
}

async function addElements () {
const resp = await fetch(dataPathStem + 'slide-data.json');
const { slides, settings } = await resp.json();
const startingIndex = settings.startRandom === true ? getRandomIndex(slides.length) : 0;
const slideListEl = document.getElementById('slide-list');
const indicatorContainer = document.getElementById('slide-indicators');

slides.forEach((s, i) => {
slideListEl.append(SlideEl(s, i === startingIndex));
indicatorContainer.append(IndicatorEl(i, i === startingIndex));
});
}

async function main () {
await addElements();

const canvasCarouselEl = document.getElementById('canvas-carousel');
const pauseEl = document.getElementById('pause');
const playPauseIconEl = document.getElementById('play-pause-icon');

const canvasCarousel = new bootstrap.Carousel(canvasCarouselEl, {
interval: 11000, pause: false
});
canvasCarousel.cycle();

let paused = false;
pauseEl.addEventListener(
'click',
() => {
if (paused) {
canvasCarousel.cycle();
playPauseIconEl.classList.remove('bi-play-fill');
playPauseIconEl.classList.add('bi-pause-fill');
paused = false;
} else {
canvasCarousel.pause();
playPauseIconEl.classList.remove('bi-pause-fill');
playPauseIconEl.classList.add('bi-play-fill');
paused = true;
}
}
);
}

main().catch((r) => console.error(String(r)));
</script>
</html>
Binary file added content/sample_image_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/sample_image_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions data/slide-data-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"slides": [
{
"imageFileName": "",
"altText": "",
"linkURL": ""
}
],
"settings": {
"startRandom": true
}
}
17 changes: 17 additions & 0 deletions data/slide-data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"slides": [
{
"imageFileName": "sample_image_1.png",
"altText": "Sample Image 1",
"linkURL": "https://github.com/tl-its-umich-edu/"
},
{
"imageFileName": "sample_image_2.png",
"altText": "Sample Image 2",
"linkURL": "https://github.com/tl-its-umich-edu/"
}
],
"settings": {
"startRandom": true
}
}

0 comments on commit a516178

Please sign in to comment.