Skip to content

Commit 3efce80

Browse files
committed
new showEval for use with interactives
1 parent 8d56fca commit 3efce80

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
ShowEval, a JS module for creating visualizations of expression evaluation. Mainly for programming tutorials.
3+
4+
Al Sweigart
5+
al@inventwithpython.com
6+
https://github.com/asweigart/
7+
*/
8+
9+
var SHOWEVAL = (function () {
10+
var thisModule = {};
11+
12+
13+
thisModule.ShowEval = function(container, steps, showTrace, addButtons) {
14+
if (typeof container === 'string') {
15+
container = document.querySelector(container);
16+
}
17+
this.container = container;
18+
this.container.classList.add('showEval');
19+
this.steps = steps.slice();
20+
this.currentStep = 0;
21+
this.createTrace = showTrace; // TODO - reset doesn't work for traces
22+
23+
if (addButtons == true) {
24+
let nextButton = document.createElement("button");
25+
nextButton.textContent = 'Next';
26+
nextButton.addEventListener("click", () => {
27+
this.evaluateStep();
28+
});
29+
this.container.appendChild(nextButton);
30+
31+
let resetButton = document.createElement("button");
32+
resetButton.textContent = 'Reset';
33+
resetButton.addEventListener("click", () => {
34+
this.evaluateStep();
35+
});
36+
this.container.appendChild(resetButton);
37+
38+
let codeDiv = document.createElement("div");
39+
this.container.appendChild(codeDiv);
40+
this.container = codeDiv;
41+
}
42+
43+
// create elements
44+
this.currentStepDiv = document.createElement('div');
45+
this.currentStepDiv.classList.add('currentStepDiv');
46+
this.container.appendChild(this.currentStepDiv);
47+
48+
let preSpan = document.createElement('span');
49+
preSpan.style.verticalAlign = 'text-top';
50+
preSpan.classList.add('pre');
51+
this.currentStepDiv.appendChild(preSpan);
52+
53+
let evalSpan = document.createElement('span');
54+
evalSpan.style.verticalAlign = 'text-top';
55+
evalSpan.classList.add('eval');
56+
this.currentStepDiv.appendChild(evalSpan);
57+
58+
let postSpan = document.createElement('span');
59+
postSpan.style.verticalAlign = 'text-top';
60+
postSpan.classList.add('post');
61+
this.currentStepDiv.appendChild(postSpan);
62+
63+
// parse steps and turn into a 4-string array: ['pre', 'before eval', 'after eval', 'post']
64+
for (var i = 0 ; i < this.steps.length; i++) {
65+
var s = this.steps[i];
66+
this.steps[i] = [s.substring(0, s.indexOf('{{')), // 'pre'
67+
s.substring(s.indexOf('{{') + 2, s.indexOf('}}{{')), // 'before eval'
68+
s.substring(s.indexOf('}}{{') + 4, s.indexOf('}}', s.indexOf('}}{{') + 4)), // 'after eval'
69+
s.substring(s.indexOf('}}', s.indexOf('}}{{') + 4) + 2)]; // 'post'
70+
}
71+
this.reset();
72+
};
73+
74+
thisModule.ShowEval.prototype.reset = function() {
75+
this.container.querySelectorAll('.previousStep').forEach(el => el.remove());
76+
this.setStep(0);
77+
};
78+
79+
thisModule.ShowEval.prototype.setStep = function(step) {
80+
this.currentStep = step;
81+
newWidth = this.getWidth(this.steps[this.currentStep][1]);
82+
this.currentStepDiv.querySelector('.eval').style.width = newWidth + 'px';
83+
this.currentStepDiv.querySelector('.pre').innerHTML = this.steps[step][0];
84+
this.currentStepDiv.querySelector('.eval').innerHTML = this.steps[step][1];
85+
this.currentStepDiv.querySelector('.post').innerHTML = this.steps[step][3];
86+
};
87+
88+
thisModule.ShowEval.prototype.getWidth = function(text) { // TODO - class style must match or else width will be off.
89+
var newElem = document.createElement('div');
90+
newElem.classList.add('showEval');
91+
newElem.style.display = 'none';
92+
newElem.innerHTML = text;
93+
document.body.appendChild(newElem);
94+
var newWidth = newElem.offsetWidth + 1; // +1 is a hack
95+
newElem.remove();
96+
97+
return newWidth;
98+
};
99+
100+
thisModule.ShowEval.prototype.createPreviousStepDiv = function(step) {
101+
let prevDiv = document.createElement('div');
102+
prevDiv.classList.add('previousStep');
103+
prevDiv.innerHTML = this.steps[step][0] + this.steps[step][1] + this.steps[step][3];
104+
this.currentStepDiv.parentNode.insertBefore(prevDiv, this.currentStepDiv);
105+
};
106+
107+
thisModule.ShowEval.prototype.evaluateStep = function(step) {
108+
if (step === undefined) {
109+
step = this.currentStep;
110+
}
111+
if (this.currentStep >= this.steps.length) {
112+
//this.currentStep = 0;
113+
//step = 0;
114+
return; // do nothing if on last step
115+
}
116+
this.setStep(step);
117+
118+
var fadeInSpeed = 0;
119+
if (this.createTrace) {
120+
this.createPreviousStepDiv(step);
121+
this.currentStepDiv.style.display = 'none';
122+
fadeInSpeed = 200;
123+
}
124+
125+
newWidth = this.getWidth(this.steps[step][2]);
126+
var evalElem = this.currentStepDiv.querySelector('.eval');
127+
128+
var thisShowEval = this;
129+
130+
evalElem.style.color = 'red';
131+
132+
// Fade in currentStepDiv
133+
this.fadeToOpacity(this.currentStepDiv, 1, fadeInSpeed, function() {
134+
window.setTimeout(function() {
135+
// Fade out evalElem
136+
thisShowEval.fadeToOpacity(evalElem, 0, 400, function() {
137+
//evalElem.style.overflow = 'hidden';
138+
// Animate width
139+
thisShowEval.animateWidth(evalElem, newWidth, 400, function() {
140+
evalElem.innerHTML = thisShowEval.steps[step][2];
141+
// Fade in evalElem
142+
thisShowEval.fadeToOpacity(evalElem, 1, 400, function() {
143+
window.setTimeout(function() {
144+
//evalElem.style.overflow = 'visible';
145+
evalElem.style.color = 'black';
146+
thisShowEval.currentStep += 1;
147+
if (thisShowEval.currentStep < thisShowEval.steps.length) {
148+
thisShowEval.setStep(thisShowEval.currentStep);
149+
}
150+
}, 600);
151+
});
152+
});
153+
});
154+
}, 600);
155+
});
156+
};
157+
158+
// Helper function to fade element to specific opacity
159+
thisModule.ShowEval.prototype.fadeToOpacity = function(element, targetOpacity, duration, callback) {
160+
if (duration === 0) {
161+
element.style.opacity = targetOpacity;
162+
if (targetOpacity === 1 && element.style.display === 'none') {
163+
element.style.display = '';
164+
}
165+
if (callback) callback();
166+
return;
167+
}
168+
169+
var startOpacity = parseFloat(window.getComputedStyle(element).opacity) || 0;
170+
var startTime = performance.now();
171+
172+
if (targetOpacity === 1 && element.style.display === 'none') {
173+
element.style.display = '';
174+
element.style.opacity = startOpacity;
175+
}
176+
177+
function animate(currentTime) {
178+
var elapsed = currentTime - startTime;
179+
var progress = Math.min(elapsed / duration, 1);
180+
181+
element.style.opacity = startOpacity + (targetOpacity - startOpacity) * progress;
182+
183+
if (progress < 1) {
184+
requestAnimationFrame(animate);
185+
} else {
186+
if (callback) callback();
187+
}
188+
}
189+
190+
requestAnimationFrame(animate);
191+
};
192+
193+
// Helper function to animate width
194+
thisModule.ShowEval.prototype.animateWidth = function(element, targetWidth, duration, callback) {
195+
var startWidth = element.offsetWidth;
196+
var startTime = performance.now();
197+
198+
function animate(currentTime) {
199+
var elapsed = currentTime - startTime;
200+
var progress = Math.min(elapsed / duration, 1);
201+
202+
element.style.width = (startWidth + (targetWidth - startWidth) * progress) + 'px';
203+
204+
if (progress < 1) {
205+
requestAnimationFrame(animate);
206+
} else {
207+
if (callback) callback();
208+
}
209+
}
210+
211+
requestAnimationFrame(animate);
212+
};
213+
214+
return thisModule;
215+
}());
216+
217+
var s;
218+
let next = document.querySelector("#nextStep");
219+
let reset = document.querySelector("#reset");
220+
reset.addEventListener("click", function () {
221+
console.log("Reset button clicked");
222+
s.reset(0);
223+
});
224+
let nextStep = document.querySelector("#nextStep");
225+
nextStep.addEventListener("click", function () {
226+
const frameid = window.frameElement.id;
227+
console.log(`Next Step button clicked in frame ${frameid}`);
228+
const message = {
229+
subject: "SPLICE.sendEvent",
230+
name: "nextStepClicked",
231+
activity_id: frameid,
232+
message_id: 10,
233+
};
234+
window.parent.postMessage(message, "*");
235+
s.evaluateStep();
236+
});
237+
238+

0 commit comments

Comments
 (0)