Skip to content

Commit 2c1ecee

Browse files
authored
feat: improved Progress Bar
- Able to jump to different code parts Co-authored by: @tuankhainguy
2 parents 241baa6 + 8c4be63 commit 2c1ecee

File tree

5 files changed

+318
-78
lines changed

5 files changed

+318
-78
lines changed

src/components/mid-panel/ControlPanel.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ function ControlPanel() {
182182
<ProgressBar
183183
current={currentChunk}
184184
max={chunkerLength}
185+
state={algorithm}
186+
dispatch={dispatch}
185187
/>
186188
</div>
187189

Lines changed: 179 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,203 @@
1-
import React, { useEffect } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
3+
import { GlobalActions } from '../../context/actions';
34
import '../../styles/ProgressBar.scss';
45

5-
function ProgressBar({ current, max }) {
6-
const node = {
7-
rectPrimary: document.querySelector('.mux-lpi-rect--primary'),
8-
buffer: document.querySelector('.mux-lpi-buffer'),
9-
};
10-
11-
function setTransform(ref, value) {
12-
if (ref !== null) {
13-
const { style } = ref;
14-
style.transform = value;
15-
style.WebkitTransform = value;
16-
style.MozTransform = value;
17-
style.OTransform = value;
18-
style.MSTransform = value;
6+
class ProgressBar extends React.Component {
7+
constructor(props) {
8+
super(props);
9+
10+
this.handleMouseDown = this.handleMouseDown.bind(this);
11+
this.handleMouseMove = this.handleMouseMove.bind(this);
12+
this.handleMouseUp = this.handleMouseUp.bind(this);
13+
14+
this.lastX = null;
15+
16+
this.max;
17+
this.current;
18+
this.viewable;
19+
this.next;
20+
this.prev;
21+
22+
this.inner;
23+
}
24+
25+
handleMouseDown(e) {
26+
this.handleMouseMove(e);
27+
document.addEventListener('mousemove', this.handleMouseMove);
28+
document.addEventListener('mouseup', this.handleMouseUp);
29+
}
30+
31+
handleMouseMove(e) {
32+
e.preventDefault();
33+
let chunkNum;
34+
35+
// how far around mouse on X to look for viewable chunk
36+
let searchRadius = 10;
37+
let rect = this.inner.getBoundingClientRect();
38+
let width = rect.right - rect.left;
39+
40+
let x = e.clientX - rect.left;
41+
if (x <= 0) {
42+
chunkNum = 0;
43+
44+
} else if (x >= width) {
45+
chunkNum = this.max - 1;
46+
47+
} else {
48+
// translate to viewable chunk array index
49+
x = Math.round((x / width) * this.max);
50+
if (x === this.current) {
51+
return;
52+
}
53+
54+
// search for the closest viewable chunk in a certain radius
55+
for (let i = 0; i <= searchRadius; i++) {
56+
if (i === 0) {
57+
if (this.viewable[x]) {
58+
chunkNum = x;
59+
break;
60+
}
61+
continue;
62+
}
63+
64+
if (this.viewable[x + i]) {
65+
chunkNum = x + i;
66+
break;
67+
}
68+
69+
if (this.viewable[x - i]) {
70+
chunkNum = x - i;
71+
break;
72+
}
73+
}
74+
}
75+
76+
// move to chunk
77+
if (this.viewable[chunkNum]) {
78+
if (chunkNum > this.current) {
79+
this.next({stopAt: chunkNum, playing: false});
80+
}
81+
if (chunkNum < this.current && chunkNum !== this.max - 1) {
82+
this.prev({stopAt: chunkNum, playing: false});
83+
}
1984
}
2085
}
2186

22-
useEffect(() => {
23-
function setProgress(ref, val) {
87+
handleMouseUp(e) {
88+
document.removeEventListener('mousemove', this.handleMouseMove);
89+
document.removeEventListener('mouseup', this.handleMouseUp);
90+
}
91+
92+
refresh() {
93+
this.forceUpdate();
94+
}
95+
96+
render() {
97+
const { current, max, state, dispatch } = this.props;
98+
const node = {
99+
rectPrimary: document.querySelector('.mux-lpi-rect--primary'),
100+
buffer: document.querySelector('.mux-lpi-buffer'),
101+
inner: document.querySelector('.mux-lpi-inner'),
102+
thumb: document.querySelector('.progressThumb'),
103+
};
104+
this.inner = node.inner;
105+
106+
this.max = max;
107+
this.current = current;
108+
this.viewable = (state.chunker !== undefined) ?
109+
state.chunker.viewable :
110+
null;
111+
112+
this.prev = (playing) => {
113+
dispatch(GlobalActions.PREV_LINE, playing);
114+
};
115+
116+
this.next = (playing) => {
117+
dispatch(GlobalActions.NEXT_LINE, playing);
118+
};
119+
120+
function setTransform(ref, value) {
121+
if (ref !== null) {
122+
const { style } = ref;
123+
style.transform = value;
124+
style.WebkitTransform = value;
125+
style.MozTransform = value;
126+
style.OTransform = value;
127+
style.MSTransform = value;
128+
}
129+
}
130+
131+
// set the progress bar body
132+
const setProgress = (ref, val) => {
24133
setTransform(ref, `scaleX(${val})`);
25134
}
26135

27136
function setBuffer(ref, val) {
28137
setTransform(ref, `scaleX(${val})`);
29138
}
30139

140+
// set the slider thumb position
141+
const setThumb = (thumb, value) => {
142+
if (thumb !== null) {
143+
const { style } = thumb;
144+
let x = 0;
145+
146+
if (this !== undefined) {
147+
x = value * this.inner.offsetWidth;
148+
}
149+
let transform = `translateX(${x}px)`;
150+
151+
style.transform = transform;
152+
style.WebkitTransform = transform;
153+
style.MozTransform = transform;
154+
style.OTransform = transform;
155+
style.MSTransform = transform;
156+
}
157+
}
158+
31159
setProgress(node.rectPrimary, parseFloat(current / max, 10));
32160
setBuffer(node.buffer, 1);
33-
}, [node, current, max]);
34-
35-
36-
return (
37-
<div role="progressbar" className="mux-lpi">
38-
<div className="progressLable" id="progressLabel">
39-
<div className="innerText">
40-
{
41-
// if the user enters a valid input and clicks on LOAD
42-
// the progress bar displays the percentage of progress
43-
// convert the lines of code to percentge by multiplying the division by 100
44-
`Progress: ${Math.round((current / max) * 100, 2)} %`
45-
// if the user does not enter a valid input, initialise the progress bar as not loaded
46-
}
161+
setThumb(node.thumb, parseFloat(current / max, 10));
162+
163+
164+
return (
165+
<div
166+
role="progressbar"
167+
className="mux-lpi"
168+
tabIndex={0}
169+
onMouseDown={this.handleMouseDown}
170+
>
171+
<div className="mux-lpi-padding" />
172+
<div className="mux-lpi-inner">
173+
<div className="progressLable" id="progressLabel">
174+
<div className="innerText">
175+
{
176+
// if the user enters a valid input and clicks on LOAD
177+
// the progress bar displays the percentage of progress
178+
// convert the lines of code to percentge by multiplying the division by 100
179+
`Progress: ${Math.round((current / max) * 100, 2)} %`
180+
// if the user does not enter a valid input, initialise the progress bar as not loaded
181+
}
182+
</div>
183+
</div>
184+
<div className="progressThumb">
185+
<div className="innerThumb" />
186+
</div>
187+
<div className="mux-lpi-buffer" />
188+
<div className="mux-lpi-rect mux-lpi-rect--primary">
189+
<span className="mux-lpi-rect-inner" />
190+
</div>
47191
</div>
48192
</div>
49-
<div className="mux-lpi-buffer" />
50-
<div className="mux-lpi-rect mux-lpi-rect--primary">
51-
<span className="mux-lpi-rect-inner" />
52-
</div>
53-
</div>
54-
);
193+
);
194+
}
55195
}
56196

57197
export default ProgressBar;
58198
ProgressBar.propTypes = {
59199
current: PropTypes.number.isRequired,
60200
max: PropTypes.number.isRequired,
201+
state: PropTypes.object.isRequired,
202+
dispatch: PropTypes.func.isRequired,
61203
};

src/context/actions.js

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,25 @@ function addLineExplanation(procedurePseudocode) {
215215
}
216216
}
217217

218+
/**
219+
* Get the array of viewable state of chunks
220+
* @param {object} chunker: current chunker instance
221+
* @param {object} pseudocode: pseudocode of current algorithm
222+
* @param {object} collapse: collapse state of pseudocode
223+
*/
224+
function viewableChunks(chunker, pseudocode, collapse) {
225+
let currChunkNum = 0;
226+
let viewable = Array(chunker.chunks.length).fill(false);
227+
viewable[0] = true;
228+
229+
while (currChunkNum < chunker.chunks.length - 1) {
230+
currChunkNum = findNext(chunker.chunks, currChunkNum, pseudocode, collapse);
231+
viewable[currChunkNum] = true;
232+
}
233+
234+
chunker.viewable = viewable;
235+
}
236+
218237
// At any time the app may call dispatch(action, params), which will trigger one of
219238
// the following functions. Each comment shows the expected properties in the
220239
// params argument.
@@ -273,6 +292,10 @@ export const GlobalActions = {
273292
const bookmarkInfo = chunker.next();
274293
//const firstLineExplan = findBookmark(procedurePseudocode, bookmarkInfo.bookmark).explanation;
275294
const firstLineExplan = null;
295+
const collapse = state === undefined || state.collapse === undefined
296+
? getCollapseController(algorithms)
297+
: state.collapse;
298+
viewableChunks(chunker, procedurePseudocode, collapse[params.name][params.mode]);
276299

277300
return {
278301
...state,
@@ -286,10 +309,7 @@ export const GlobalActions = {
286309
...bookmarkInfo, // sets bookmark & finished fields
287310
chunker,
288311
visualisers: chunker.visualisers,
289-
collapse:
290-
state === undefined || state.collapse === undefined
291-
? getCollapseController(algorithms)
292-
: state.collapse,
312+
collapse: collapse,
293313
playing: false,
294314
lineExplanation: firstLineExplan,
295315
};
@@ -310,14 +330,23 @@ export const GlobalActions = {
310330
let result;
311331

312332
let triggerPauseInCollapse = false;
333+
let stopAt = undefined;
313334
if (typeof playing === 'object') {
314335
triggerPauseInCollapse = playing.triggerPauseInCollapse;
336+
stopAt = playing.stopAt;
315337
playing = playing.playing;
316338
}
317339

318340
// console.log(['NEXT_LINE', playing, triggerPauseInCollapse]);
319341
// figure out what chunk we need to stop at
320-
let stopAt = findNext(state.chunker.chunks, state.chunker.currentChunk, state.pseudocode, state.collapse[state.id.name][state.id.mode]);
342+
if (stopAt === undefined) {
343+
stopAt = state.chunker.currentChunk;
344+
if (stopAt < state.chunker.chunks.length - 1) {
345+
do {
346+
stopAt++;
347+
} while (!state.chunker.viewable[stopAt])
348+
}
349+
}
321350
// step forward until we are at stopAt, or last chunk, or some weird
322351
// pauseInCollapse stuff (for Warshall's?) I don't really understand:( XXX
323352
do {
@@ -364,10 +393,23 @@ export const GlobalActions = {
364393
// of range (perhaps should change this XXX); we need check for
365394
// that here
366395
// console.log(['PREV_LINE', state.chunker.currentChunk, state.chunker.chunks.length]);
367-
if (state.chunker.currentChunk > state.chunker.chunks.length) {
396+
if (state.chunker.currentChunk >= state.chunker.chunks.length) {
368397
state.chunker.currentChunk = state.chunker.chunks.length - 1;
369398
}
370-
let stopAt = findPrev(state.chunker.chunks, state.chunker.currentChunk, state.pseudocode, state.collapse[state.id.name][state.id.mode])
399+
400+
let stopAt = undefined;
401+
if (typeof playing === 'object') {
402+
stopAt = playing.stopAt;
403+
playing = playing.playing;
404+
}
405+
if (stopAt === undefined) {
406+
stopAt = state.chunker.currentChunk;
407+
if (stopAt > 0) {
408+
do {
409+
stopAt--;
410+
} while (!state.chunker.viewable[stopAt])
411+
}
412+
}
371413
let result1 = { bookmark: "", chunk: state.chunker.currentChunk };
372414
const result = state.chunker.goBackTo(stopAt); // changes state
373415

@@ -405,6 +447,13 @@ export const GlobalActions = {
405447
onCollapseStateChange(); // Transitive closure plugin
406448
unionFindToggleRank(state);
407449

450+
// update viewable chunks
451+
viewableChunks(
452+
state.chunker,
453+
state.pseudocode,
454+
state.collapse[state.id.name][state.id.mode]
455+
);
456+
408457
return {
409458
...state,
410459
collapse: result,

0 commit comments

Comments
 (0)