Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/img modal #283

Merged
merged 12 commits into from
Aug 16, 2021
4 changes: 4 additions & 0 deletions custom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ The site name, logo, and most of the text on the website can be modified from th
placing a value for the same key in `custom/strings.yaml`, with a custom string,
Javascript function, or image path.

## Image Modal
Images can opened/expanded in a modal/dialog. On hover of an image, the cursor will turn into the pointer style, and an expand button will show. Once clicked, the image will center in the page, and a minimize icon will show. Styles for the modal can be found in
`/styles/partials/core/_furniture.scss` as well as one line in `/styles/partials/core/_base.scss` that adds a pointer cursor on hover of images. The image modal is added server side, code for which can be found [here](../server/formatter.js#L168). `img` elements are wrapped in an overlay that allows for an expand button to show on hover during, code for which can be found [here](../server/formatter.js#L104). All components of the image modal that are used on the server can be found in the `/server/ssrComponents/` directory. Finally, the javascript that enables the image modal is in `/layouts/partials/footer.ejs`.

## Middleware
Middleware can be added to the beginning or end of the request cycle by placing
files into `custom/middleware`. These files can export `preload` and `postload`
Expand Down
1 change: 0 additions & 1 deletion layouts/categories/default.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<%- include('partials/childrenList', {children, kicker: template('folder.childrenList.kicker', title)}) %>
<% } %>
</div>

<%- include('partials/footer', { pageType: 'document', topLevelFolder: url.split('/')[1] }) %>
</div>
</body>
Expand Down
34 changes: 34 additions & 0 deletions layouts/partials/footer.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,38 @@
}) // no callback on this.
})

window.onload = () => {
const imgs = document.querySelectorAll('.g-main-content img'); // get all images
imgs.forEach(img => {
img.addEventListener('click', (event) => {
expandImage(img.src);
});
});
}

var imgModal = document.querySelector('.image-modal')
var modalImg = document.querySelector('.image-modal .modal-image');


function expandImage(imgSrc) {
imgModal.style.display = 'block';
modalImg.src = imgSrc;

window.addEventListener('scroll', function onScroll() {
window.removeEventListener('scroll', onScroll);
imgModal.style.display='none';
});
};

var modalCloseBtn = document.querySelector('.image-modal .close');
modalCloseBtn.addEventListener('click', () => {
imgModal.style.display = 'none';
});

/* keydown listener so img modal can be closed with Esc key */
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && imgModal.style.display === 'block') {
imgModal.style.display = 'none';
}
});
</script>
30 changes: 30 additions & 0 deletions server/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const cheerio = require('cheerio')
const qs = require('querystring')
const unescape = require('unescape')
const hljs = require('highlight.js')
const fs = require('fs')
const list = require('./list')

/* Your one stop shop for all your document processing needs. */
Expand All @@ -18,6 +19,12 @@ function normalizeHtml(html) {
const $p = $('p')
const isClean = $('meta[name="library-html-doc"]').attr('content') === '1'

let svgString
// load svg that will be used on images. takes ~0.2ms
if (html.indexOf('img') !== -1) {
svgString = fs.readFileSync('server/ssrComponents/expandIcon.svg', 'utf8')
}

// Remove p tags in Table of Contents
$p.each((index, p) => {
if (p.children.length < 1) return // don't search any empty p tags
Expand Down Expand Up @@ -92,6 +99,13 @@ function normalizeHtml(html) {
$(el).attr('href', libraryDeepLink || decoded)
}

// Wrap images in a div, and add an expand button with an svg inside it
// to the wrapper
if (el.tagName === 'img') {
$(el).wrap('<div class="image-wrapper"></div>')
$(el).parent().append(`<button aria-label="expand this image" title="expand this image" class="expand-image-btn">${svgString}</button>`)
}

return el
})

Expand Down Expand Up @@ -151,6 +165,20 @@ function formatCodeContent(content) {
return content
}

function addImageModal(html) {
if (html.indexOf('img') === -1) {
return html
}

let imgModalHtml = fs.readFileSync('server/ssrComponents/imageModal.html', 'utf8')
const minimizeIconSvg = fs.readFileSync('server/ssrComponents/minimizeIcon.svg', 'utf8')
imgModalHtml = imgModalHtml.replace('<!-- svgPlaceholder -->', minimizeIconSvg)

const $ = cheerio.load(html)
$('body').append(imgModalHtml)
return $('head').html() + $('body').html() // include head for list style block
}

function checkForTableOfContents($, aTags) {
return aTags.length === 2 && // TOC links title and number
aTags[0].attribs.href.match('#h.') && // the links go to a heading in the doc
Expand Down Expand Up @@ -227,8 +255,10 @@ function convertYoutubeUrl(content) {
return content
}

// TODO: pass around cheerio instance vs html string to avoid loading multiple times?
function getProcessedHtml(src) {
let html = normalizeHtml(src)
html = addImageModal(html)
html = convertYoutubeUrl(html)
html = formatCode(html)
html = pretty(html)
Expand Down
1 change: 1 addition & 0 deletions server/ssrComponents/expandIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions server/ssrComponents/imageModal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div id="image-modal" class="image-modal">
<button type="button" class="close" title="Minimize this image" aria-label="Minimize this image">
<!-- svgPlaceholder -->
</button>
<div class="img-wrapper">
<img class="modal-image" id="img1" />
</div>
</div>
1 change: 1 addition & 0 deletions server/ssrComponents/minimizeIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions styles/partials/core/_categories.scss
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@
padding: 10px;
display: block;
margin: 20px auto;
cursor: pointer; // images can be opened in a modal

@include tablet{
max-width: 580px;
Expand Down
73 changes: 73 additions & 0 deletions styles/partials/core/_furniture.scss
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,76 @@
text-align: center;
}
}

// the image-wrapper class is added for the benefit of the
// Image Modal, so co-locating it here
.image-wrapper {
position: relative;

.expand-image-btn {
height: 60px;
width: 60px;
position: absolute;
bottom: 25px;
right: 25px;
opacity: 0;
transition: opacity 0.3s ease 0s;
background-color: transparent;
pointer-events: none;
border: none;
}

&:hover {
.expand-image-btn {
opacity: 1;
}
}
}

.image-modal {
display: none;
position: fixed;
z-index: 1000000090; /* search bar icon has z-index of 1*10 */
inset: 0px; /* shorthand for top,right,bottom,left at the same time */
overflow: hidden;
background-color: rgb(255, 255, 255); /* full white allows images to pop */
transition: display 0.2s ease 0s;
}

.image-modal .img-wrapper {
display: flex;
align-items: center;
align-content: center;
height: 100%;
padding: 30px;

.modal-image {
margin: auto;
display: block;
max-width: 100%;
cursor: default;
}
}

.image-modal .close {
display: flex;
align-items: center;
position: absolute;
top: 10px;
right: 10px;
background-color: transparent;
cursor: pointer;
border: 0.5px solid white;
border-radius: 50%;
width: 60px;
height: 60px;
transition: all 0.1s ease-in;
padding: 0px;

&:hover, &:focus {
background-color: #f9f9f9;
border-color: lightgray;
}
}


29 changes: 29 additions & 0 deletions test/unit/htmlProcessing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ describe('HTML processing', () => {
const widthMatch = imageWidth.attr('style').match('width')
assert.isNotNull(widthMatch)
})

it('wraps img elements in a div, and appends button containing svg to same div', () => {
const numImages = (testGlobal.rawHTML.match(/<img/g) || []).length

// There should be no divs with the image-wrapper class initially
assert.equal(testGlobal.rawHTML.indexOf('image-wrapper'), -1)

// expect there to be the same number of wrappers as images
assert.equal(testGlobal.output('.image-wrapper').length, numImages)

// assert each wrapper contains an image and an expand button
const $ = testGlobal.output
testGlobal.output('.image-wrapper').each((i, elem) => {
assert.equal($(elem).children().length, 2)
assert.isNotNull($(elem).find('.expand-image-btn'))
assert.isNotNull($(elem).find('img'))
})
});
})

describe('list handling', () => {
Expand Down Expand Up @@ -161,6 +179,17 @@ describe('HTML processing', () => {
})
})

describe('image modal handling', () => {
it('adds an image modal', () => {
assert.isNotNull(testGlobal.output('image-modal'))
})
it('replaces comment in imageModal.html with an actual svg', () => {
const $ = testGlobal.output
const imgModal = $('image-modal')
assert.isNotNull($(imgModal).find('svg'))
})
})

describe('comment handling', () => {
it('strips comments', () => {
assert.notMatch(testGlobal.processedHTML, /This comment text will not appear/)
Expand Down