Skip to content

Commit a569881

Browse files
authored
Merge pull request #1463 from hackmdio/feature/image-lightbox
2 parents 557fda7 + e3a6669 commit a569881

File tree

4 files changed

+334
-0
lines changed

4 files changed

+334
-0
lines changed

public/js/extra.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
deserializeParamAttributeFromElement
2525
} from './lib/markdown/utils'
2626
import { renderFretBoard } from './lib/renderer/fretboard/fretboard'
27+
import './lib/renderer/lightbox'
2728

2829
import markdownit from 'markdown-it'
2930
import markdownitContainer from 'markdown-it-container'
@@ -1187,6 +1188,7 @@ md.use(markdownitContainer, 'spoiler', {
11871188
const defaultImageRender = md.renderer.rules.image
11881189
md.renderer.rules.image = function (tokens, idx, options, env, self) {
11891190
tokens[idx].attrJoin('class', 'raw')
1191+
tokens[idx].attrJoin('class', 'md-image')
11901192
return defaultImageRender(...arguments)
11911193
}
11921194
md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import './lightbox.css'
2+
3+
let images = []
4+
/** @type {HTMLImageElement} */
5+
let currentImage = null
6+
let currentIndexIndex = 0
7+
8+
let hideContainer
9+
10+
function findOrCreateLightboxContainer () {
11+
const lightboxContainerSelector = '.lightbox-container'
12+
13+
let lightBoxContainer = document.querySelector(lightboxContainerSelector)
14+
if (!lightBoxContainer) {
15+
lightBoxContainer = document.createElement('div')
16+
lightBoxContainer.className = 'lightbox-container'
17+
18+
lightBoxContainer.innerHTML = `
19+
<i class="fa fa-chevron-left lightbox-control-previous" aria-hidden="true"></i>
20+
<i class="fa fa-chevron-right lightbox-control-next" aria-hidden="true"></i>
21+
<i class="fa fa-close lightbox-control-close" aria-hidden="true"></i>
22+
23+
<div class="lightbox-inner">
24+
</div>
25+
`
26+
27+
addImageZoomListener(lightBoxContainer)
28+
29+
hideContainer = () => {
30+
lightBoxContainer.classList.remove('show')
31+
document.body.classList.remove('no-scroll')
32+
currentImage = null
33+
}
34+
35+
lightBoxContainer.querySelector('.lightbox-control-previous').addEventListener('click', (e) => {
36+
e.stopPropagation()
37+
switchImage(-1)
38+
})
39+
lightBoxContainer.querySelector('.lightbox-control-next').addEventListener('click', (e) => {
40+
e.stopPropagation()
41+
switchImage(1)
42+
})
43+
lightBoxContainer.querySelector('.lightbox-control-close').addEventListener('click', (e) => {
44+
e.stopPropagation()
45+
hideContainer()
46+
})
47+
lightBoxContainer.addEventListener('click', (e) => {
48+
e.stopPropagation()
49+
hideContainer()
50+
})
51+
52+
document.body.appendChild(lightBoxContainer)
53+
}
54+
55+
return lightBoxContainer
56+
}
57+
58+
function switchImage (dir) {
59+
const lightBoxContainer = findOrCreateLightboxContainer()
60+
61+
currentIndexIndex += dir
62+
if (currentIndexIndex >= images.length) {
63+
currentIndexIndex = 0
64+
} else if (currentIndexIndex < 0) {
65+
currentIndexIndex = images.length - 1
66+
}
67+
68+
const img = images[currentIndexIndex]
69+
70+
setImageInner(img, lightBoxContainer)
71+
}
72+
73+
function setImageInner (img, lightBoxContainer) {
74+
const src = img.getAttribute('src')
75+
const alt = img.getAttribute('alt')
76+
77+
lightBoxContainer.querySelector('.lightbox-inner').innerHTML = `<img src="${src}" alt="${alt}" draggable="false">`
78+
addImageDragListener(lightBoxContainer.querySelector('.lightbox-inner img'))
79+
}
80+
81+
function onClickImage (img) {
82+
const lightBoxContainer = findOrCreateLightboxContainer()
83+
84+
setImageInner(img, lightBoxContainer)
85+
86+
lightBoxContainer.classList.add('show')
87+
document.body.classList.add('no-scroll')
88+
89+
currentImage = img
90+
updateLightboxImages()
91+
}
92+
93+
function updateLightboxImages () {
94+
images = [...document.querySelectorAll('.markdown-body img.md-image')]
95+
96+
if (currentImage) {
97+
currentIndexIndex = images.findIndex(image => image === currentImage)
98+
}
99+
}
100+
101+
function addImageZoomListener (container) {
102+
container.addEventListener('wheel', function (e) {
103+
// normalize scroll position as percentage
104+
e.preventDefault()
105+
106+
/** @type {HTMLImageElement} */
107+
const image = container.querySelector('img')
108+
109+
if (!image) {
110+
return
111+
}
112+
113+
let scale = image.getBoundingClientRect().width / image.offsetWidth
114+
scale += e.deltaY * -0.01
115+
116+
// Restrict scale
117+
scale = Math.min(Math.max(0.125, scale), 4)
118+
119+
var transformValue = `scale(${scale})`
120+
121+
image.style.WebkitTransform = transformValue
122+
image.style.MozTransform = transformValue
123+
image.style.OTransform = transformValue
124+
image.style.transform = transformValue
125+
})
126+
}
127+
128+
/**
129+
* @param {HTMLImageElement} image
130+
*/
131+
function addImageDragListener (image) {
132+
let moved = false
133+
let pos = []
134+
135+
const container = findOrCreateLightboxContainer()
136+
const inner = container.querySelector('.lightbox-inner')
137+
138+
const onMouseDown = (evt) => {
139+
moved = true
140+
141+
const { left, top } = image.getBoundingClientRect()
142+
143+
pos = [
144+
evt.pageX - left,
145+
evt.pageY - top
146+
]
147+
}
148+
image.addEventListener('mousedown', onMouseDown)
149+
inner.addEventListener('mousedown', onMouseDown)
150+
151+
const onMouseMove = (evt) => {
152+
if (!moved) {
153+
return
154+
}
155+
156+
image.style.left = `${evt.pageX - pos[0]}px`
157+
image.style.top = `${evt.pageY - pos[1]}px`
158+
image.style.position = 'absolute'
159+
}
160+
image.addEventListener('mousemove', onMouseMove)
161+
inner.addEventListener('mousemove', onMouseMove)
162+
163+
const onMouseUp = () => {
164+
moved = false
165+
pos = []
166+
}
167+
image.addEventListener('mouseup', onMouseUp)
168+
inner.addEventListener('mouseup', onMouseUp)
169+
170+
inner.addEventListener('click', (e) => {
171+
e.stopPropagation()
172+
})
173+
image.addEventListener('click', (e) => {
174+
e.stopPropagation()
175+
})
176+
}
177+
178+
const init = () => {
179+
const markdownBody = document.querySelector('.markdown-body')
180+
if (!markdownBody) {
181+
return
182+
}
183+
184+
markdownBody.addEventListener('click', function (e) {
185+
const img = e.target
186+
if (img.nodeName === 'IMG' && img.classList.contains('md-image')) {
187+
onClickImage(img)
188+
e.stopPropagation()
189+
}
190+
})
191+
192+
window.addEventListener('keydown', function (e) {
193+
if (!currentImage) {
194+
return
195+
}
196+
197+
if (e.key === 'ArrowRight') {
198+
switchImage(1)
199+
e.stopPropagation()
200+
} else if (e.key === 'ArrowLeft') {
201+
switchImage(-1)
202+
e.stopPropagation()
203+
} else if (e.key === 'Escape') {
204+
if (hideContainer) {
205+
hideContainer()
206+
e.stopPropagation()
207+
}
208+
}
209+
})
210+
}
211+
212+
init()
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
.lightbox-container.show {
2+
display: flex !important;
3+
}
4+
5+
.lightbox-container {
6+
display: none;
7+
8+
position: fixed;
9+
z-index: 99999;
10+
background-color: rgba(255, 255, 255, 0.8);
11+
top: 0;
12+
left: 0;
13+
width: 100%;
14+
height: 100%;
15+
16+
justify-content: center;
17+
align-items: center;
18+
padding: 0px 40px;
19+
}
20+
21+
.night .lightbox-container {
22+
background-color: rgba(47, 47, 47, 0.8);;
23+
}
24+
25+
.lightbox-container .lightbox-control-previous,
26+
.lightbox-container .lightbox-control-next,
27+
.lightbox-container .lightbox-control-close {
28+
position: absolute;
29+
width: 40px;
30+
height: 40px;
31+
color: rgba(65, 65, 65, 0.8);
32+
text-align: center;
33+
cursor: pointer;
34+
user-select: none;
35+
font-size: 25px;
36+
z-index: 1;
37+
}
38+
39+
.night .lightbox-container .lightbox-control-previous,
40+
.night .lightbox-container .lightbox-control-next,
41+
.night .lightbox-container .lightbox-control-close {
42+
color: rgba(255, 255, 255, 0.5);
43+
}
44+
45+
.lightbox-container .lightbox-control-previous:hover,
46+
.lightbox-container .lightbox-control-next:hover,
47+
.lightbox-container .lightbox-control-close:hover {
48+
color: rgba(130, 130, 130, 0.78);
49+
}
50+
51+
.night .lightbox-container .lightbox-control-previous:hover,
52+
.night .lightbox-container .lightbox-control-next:hover,
53+
.night .lightbox-container .lightbox-control-close:hover {
54+
color: rgba(255, 255, 255, 0.8);
55+
}
56+
57+
.lightbox-container .lightbox-control-next,
58+
.lightbox-container .lightbox-control-previous {
59+
top: calc(50% - 10px);
60+
}
61+
62+
.lightbox-container .lightbox-control-previous {
63+
left: 0;
64+
}
65+
66+
.lightbox-container .lightbox-control-next {
67+
right: 0;
68+
}
69+
70+
.lightbox-container .lightbox-control-close {
71+
top: 10px;
72+
right: 10px;
73+
}
74+
75+
.lightbox-container .lightbox-inner {
76+
width: 100%;
77+
height: 100%;
78+
cursor: move;
79+
80+
display: -webkit-box;
81+
display: -webkit-flex;
82+
display: -moz-box;
83+
display: -ms-flexbox;
84+
85+
display: flex;
86+
-webkit-box-align: center;
87+
-webkit-align-items: center;
88+
-moz-box-align: center;
89+
-ms-flex-align: center;
90+
align-items: center;
91+
-webkit-box-pack: center;
92+
-webkit-justify-content: center;
93+
-moz-box-pack: center;
94+
-ms-flex-pack: center;
95+
justify-content: center;
96+
}
97+
98+
.lightbox-container .lightbox-inner img {
99+
max-width: 100%;
100+
cursor: move;
101+
102+
transform-origin: 0 0;
103+
-moz-transform-origin: 0 0;
104+
-webkit-transform-origin: 0 0;
105+
-ms-transform-origin: 0 0;
106+
-o-transform-origin: 0 0;
107+
}
108+
109+
.markdown-body img.md-image {
110+
cursor: zoom-in;
111+
}
112+
113+
body.no-scroll {
114+
overflow: hidden;
115+
}

public/js/lib/syncscroll.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
2727
addPart(tokens, idx)
2828
return self.renderToken(...arguments)
2929
}
30+
const defaultImageRender = md.renderer.rules.image
31+
md.renderer.rules.image = function (tokens, idx, options, env, self) {
32+
tokens[idx].attrJoin('class', 'md-image')
33+
return defaultImageRender(...arguments)
34+
}
3035
md.renderer.rules.bullet_list_open = function (tokens, idx, options, env, self) {
3136
addPart(tokens, idx)
3237
return self.renderToken(...arguments)

0 commit comments

Comments
 (0)