chansung commited on
Commit
992c838
·
verified ·
1 Parent(s): e72d90f

Create script.js

Browse files
Files changed (1) hide show
  1. script.js +365 -0
script.js ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const codeSnippet = `// HTML structure for this example:
3
+ // <button id="counterBtn">Click me: 0</button>
4
+ // <p id="message"></p>
5
+
6
+ const button = document.getElementById('counterBtn');
7
+ let count = 0;
8
+
9
+ button.addEventListener('click', () => {
10
+ count++;
11
+ button.textContent = \`Click me: \${count}\`;
12
+
13
+ if (count === 5) {
14
+ const messageEl = document.getElementById('message');
15
+ messageEl.textContent = "You've reached 5 clicks!";
16
+ } else if (count > 5) {
17
+ const msgEl = document.getElementById('message');
18
+ msgEl.textContent = "Keep clicking!";
19
+ }
20
+ });`;
21
+
22
+ const journeyStructure = [
23
+ {
24
+ groupTitle: "Setup & Initialization",
25
+ steps: [
26
+ { category: "Setup", title: "HTML Structure", lineRanges: [[0, 3]], explanation: "This JavaScript interacts with specific HTML elements. We're assuming a button with <code>id=\"counterBtn\"</code> and a paragraph with <code>id=\"message\"</code> exist on the page. The comments at the top outline this structure." },
27
+ { category: "DOM Query", title: "Access Button Element", lineRanges: [[4, 4]], explanation: "<code>const button = document.getElementById('counterBtn');</code><br>This line uses the DOM API to find the HTML button element with ID 'counterBtn' and stores it in the 'button' constant." },
28
+ { category: "Variable Declaration", title: "Initialize Click Counter", lineRanges: [[5, 5]], explanation: "<code>let count = 0;</code><br>A variable 'count' is declared and initialized to 0. This variable will track clicks." }
29
+ ]
30
+ },
31
+ {
32
+ groupTitle: "Core Interaction Logic",
33
+ steps: [
34
+ { category: "Event Handling", title: "Attach Click Listener", lineRanges: [[7, 7], [16,16]], explanation: "<code>button.addEventListener('click', () => { ... });</code><br>An event listener is attached to the 'button'. The arrow function <code>() => { ... }</code> executes on each click." },
35
+ { category: "State Update", title: "Increment Counter", lineRanges: [[8, 8]], explanation: "<code>count++;</code><br>Inside the click handler, 'count' is incremented by 1." },
36
+ { category: "UI Update", title: "Update Button Text", lineRanges: [[9, 9]], explanation: "<code>button.textContent = \`Click me: \${count}\`;</code><br>The button's text is updated to show the new 'count' using template literals." }
37
+ ]
38
+ },
39
+ {
40
+ groupTitle: "Conditional Feedback",
41
+ steps: [
42
+ { category: "Conditional Logic", title: "Check 5-Click Milestone", lineRanges: [[11, 11],[12,12],[13,13]], explanation: "<code>if (count === 5) { ... }</code><br>If 'count' reaches 5, a special message is displayed in the 'message' paragraph. Note that line 13 is the closing brace of this `if` block." },
43
+ { category: "Conditional Logic", title: "Handle Subsequent Clicks", lineRanges: [[13,13],[14,14],[15,15]], explanation: "<code>else if (count > 5) { ... }</code><br>If 'count' is over 5, a different message encourages continued clicking. Note that line 13 is also the start of this `else if`." }
44
+ ]
45
+ },
46
+ {
47
+ groupTitle: "Conclusion",
48
+ steps: [
49
+ { category: "Summary", title: "Tour Completed!", lineRanges: [], explanation: "You've completed the tour! This example showed DOM manipulation, events, variables, and conditional logic. Click 'Reset' to restart." }
50
+ ]
51
+ }
52
+ ];
53
+
54
+ const categoryStyles = {
55
+ "Setup": "bg-slate-200 text-slate-700",
56
+ "DOM Query": "bg-sky-100 text-sky-700",
57
+ "Variable Declaration": "bg-emerald-100 text-emerald-700",
58
+ "Event Handling": "bg-violet-100 text-violet-700",
59
+ "State Update": "bg-amber-100 text-amber-700",
60
+ "UI Update": "bg-indigo-100 text-indigo-700",
61
+ "Conditional Logic": "bg-rose-100 text-rose-700",
62
+ "Summary": "bg-cyan-100 text-cyan-700",
63
+ "Default": "bg-gray-200 text-gray-700" // Fallback
64
+ };
65
+
66
+ const codeLinesContainer = document.getElementById('codeLinesContainer');
67
+ const explanationStepTitle = document.getElementById('explanationStepTitle');
68
+ const explanationMeta = document.getElementById('explanationMeta');
69
+ const explanationText = document.getElementById('explanationText');
70
+ const explanationStepCategoryContainer = document.getElementById('explanationStepCategoryContainer');
71
+ const prevBtn = document.getElementById('prevBtn');
72
+ const nextBtn = document.getElementById('nextBtn');
73
+ const resetBtn = document.getElementById('resetBtn');
74
+ const progressIndicator = document.getElementById('progressIndicator');
75
+ const stepsOutlineUl = document.getElementById('stepsOutline');
76
+ const mobileCurrentGroupTextEl = document.getElementById('mobileCurrentGroupText');
77
+ const mobileCurrentStepTextEl = document.getElementById('mobileCurrentStepText'); // New element
78
+
79
+ let currentStepIndex = 0;
80
+ let lineElements = [];
81
+ let flatCodeSteps = [];
82
+
83
+ // Flatten the journeyStructure for sequential navigation
84
+ journeyStructure.forEach(group => {
85
+ group.steps.forEach(step => {
86
+ flatCodeSteps.push(step);
87
+ });
88
+ });
89
+
90
+ function handleCodeLineClick(event) {
91
+ const clickedLineDiv = event.currentTarget;
92
+ const clickedLineIndex = parseInt(clickedLineDiv.dataset.lineIndex, 10);
93
+
94
+ for (let i = 0; i < flatCodeSteps.length; i++) {
95
+ const step = flatCodeSteps[i];
96
+ if (step.lineRanges && step.lineRanges.length > 0) {
97
+ for (const range of step.lineRanges) {
98
+ if (clickedLineIndex >= range[0] && clickedLineIndex <= range[1]) {
99
+ currentStepIndex = i;
100
+ updateStep();
101
+ return;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function renderCode() {
109
+ codeLinesContainer.innerHTML = '';
110
+ lineElements = [];
111
+
112
+ const tempCodeElement = document.createElement('code');
113
+ tempCodeElement.className = 'language-javascript';
114
+ tempCodeElement.textContent = codeSnippet;
115
+
116
+ if (typeof Prism !== 'undefined' && Prism.highlightElement) {
117
+ Prism.highlightElement(tempCodeElement);
118
+ } else {
119
+ console.warn("Prism.js not available. Syntax highlighting will be plain.");
120
+ }
121
+
122
+ const highlightedHtml = tempCodeElement.innerHTML;
123
+ const linesHtml = highlightedHtml.split('\n');
124
+
125
+ linesHtml.forEach((lineHtml, index) => {
126
+ const lineDiv = document.createElement('div');
127
+ lineDiv.classList.add('code-line');
128
+ lineDiv.innerHTML = lineHtml || '&nbsp;';
129
+ lineDiv.dataset.lineIndex = index;
130
+ lineDiv.addEventListener('click', handleCodeLineClick);
131
+ codeLinesContainer.appendChild(lineDiv);
132
+ lineElements.push(lineDiv);
133
+ });
134
+ }
135
+
136
+ function renderOutline() {
137
+ stepsOutlineUl.innerHTML = '';
138
+ let globalStepIndex = 0;
139
+
140
+ journeyStructure.forEach((group, groupIndex) => {
141
+ const groupLi = document.createElement('li');
142
+ groupLi.className = `pt-3 pb-1 px-1 ${groupIndex > 0 ? 'mt-1' : ''}`;
143
+
144
+ const groupTitleEl = document.createElement('h4');
145
+ groupTitleEl.className = 'text-xs font-semibold text-slate-500 uppercase tracking-wider select-none';
146
+ groupTitleEl.textContent = group.groupTitle;
147
+ groupLi.appendChild(groupTitleEl);
148
+ stepsOutlineUl.appendChild(groupLi);
149
+
150
+ group.steps.forEach((step, stepIndexInGroup) => {
151
+ const li = document.createElement('li');
152
+ li.className = 'outline-step-item rounded-md ml-1';
153
+ li.dataset.stepIndex = globalStepIndex;
154
+
155
+ const button = document.createElement('button');
156
+ button.className = 'group flex items-start w-full text-left p-2 focus:outline-none focus:ring-2 focus:ring-sky-300 focus:z-10 rounded-md transition-colors hover:bg-slate-100';
157
+
158
+ const dotAndLineContainer = document.createElement('div');
159
+ dotAndLineContainer.className = 'flex flex-col items-center mr-3 mt-1 flex-shrink-0';
160
+
161
+ const dot = document.createElement('div');
162
+ dot.className = 'outline-dot w-3 h-3 rounded-full bg-slate-300 border-2 border-white group-hover:bg-sky-400 transition-all duration-150 ease-in-out';
163
+ dotAndLineContainer.appendChild(dot);
164
+
165
+ if (globalStepIndex < flatCodeSteps.length - 1) {
166
+ const isLastInGroup = stepIndexInGroup === group.steps.length - 1;
167
+ const isLastGroup = groupIndex === journeyStructure.length - 1;
168
+ if (isLastInGroup && !(isLastGroup && isLastInGroup) ) {
169
+ const line = document.createElement('div');
170
+ line.className = 'outline-connector w-0.5 h-2 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';
171
+ dotAndLineContainer.appendChild(line);
172
+ } else if (!(isLastGroup && isLastInGroup)){
173
+ const line = document.createElement('div');
174
+ line.className = 'outline-connector w-0.5 h-5 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';
175
+ dotAndLineContainer.appendChild(line);
176
+ } else {
177
+ const placeholder = document.createElement('div');
178
+ placeholder.className = 'h-5 mt-1';
179
+ dotAndLineContainer.appendChild(placeholder);
180
+ }
181
+ } else {
182
+ const placeholder = document.createElement('div');
183
+ placeholder.className = 'h-5 mt-1';
184
+ dotAndLineContainer.appendChild(placeholder);
185
+ }
186
+
187
+
188
+ const titleSpan = document.createElement('span');
189
+ titleSpan.className = 'outline-title text-sm text-slate-600 group-hover:text-sky-600 transition-colors duration-150 ease-in-out';
190
+ titleSpan.textContent = step.title;
191
+
192
+ button.appendChild(dotAndLineContainer);
193
+ button.appendChild(titleSpan);
194
+ li.appendChild(button);
195
+
196
+ button.addEventListener('click', () => {
197
+ currentStepIndex = parseInt(li.dataset.stepIndex, 10);
198
+ updateStep();
199
+ });
200
+ stepsOutlineUl.appendChild(li);
201
+ globalStepIndex++;
202
+ });
203
+ });
204
+ }
205
+
206
+ function updateOutlineActiveState() {
207
+ const outlineItems = stepsOutlineUl.querySelectorAll('.outline-step-item[data-step-index]');
208
+ const outlineNav = stepsOutlineUl.parentElement;
209
+
210
+ outlineItems.forEach((itemLi) => {
211
+ const idx = parseInt(itemLi.dataset.stepIndex, 10);
212
+ const button = itemLi.querySelector('button');
213
+ const dot = itemLi.querySelector('.outline-dot');
214
+ const title = itemLi.querySelector('.outline-title');
215
+ const connector = itemLi.querySelector('.outline-connector');
216
+
217
+ if (idx === currentStepIndex) {
218
+ button.classList.add('bg-sky-50');
219
+ dot.classList.remove('bg-slate-300');
220
+ dot.classList.add('bg-sky-500', 'scale-125');
221
+ if (connector) connector.classList.add('bg-sky-400');
222
+ title.classList.remove('text-slate-600');
223
+ title.classList.add('text-sky-700', 'font-semibold');
224
+
225
+ if (outlineNav.scrollHeight > outlineNav.clientHeight && !outlineNav.classList.contains('hidden')) { // Only scroll if scrollable and visible
226
+ const itemRect = itemLi.getBoundingClientRect();
227
+ const navRect = outlineNav.getBoundingClientRect();
228
+ if (itemRect.top < navRect.top || itemRect.bottom > navRect.bottom) {
229
+ itemLi.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
230
+ }
231
+ }
232
+ } else {
233
+ button.classList.remove('bg-sky-50');
234
+ dot.classList.add('bg-slate-300');
235
+ dot.classList.remove('bg-sky-500', 'scale-125');
236
+ if (connector) connector.classList.remove('bg-sky-400');
237
+ title.classList.add('text-slate-600');
238
+ title.classList.remove('text-sky-700', 'font-semibold');
239
+ }
240
+ });
241
+ }
242
+
243
+ function updateStep() {
244
+ if (currentStepIndex < 0 || currentStepIndex >= flatCodeSteps.length) return;
245
+
246
+ const step = flatCodeSteps[currentStepIndex];
247
+ const hasLineFocus = step.lineRanges && step.lineRanges.length > 0;
248
+
249
+ // Update mobile current group and step titles
250
+ if (mobileCurrentGroupTextEl && mobileCurrentStepTextEl) {
251
+ let currentGroupTitleForMobile = "Loading...";
252
+ let stepsProcessed = 0;
253
+ for (const group of journeyStructure) {
254
+ const stepsInGroup = group.steps.length;
255
+ if (currentStepIndex >= stepsProcessed && currentStepIndex < stepsProcessed + stepsInGroup) {
256
+ currentGroupTitleForMobile = group.groupTitle;
257
+ break;
258
+ }
259
+ stepsProcessed += stepsInGroup;
260
+ }
261
+ mobileCurrentGroupTextEl.textContent = currentGroupTitleForMobile;
262
+ mobileCurrentStepTextEl.textContent = step.title;
263
+ }
264
+
265
+
266
+ explanationStepCategoryContainer.innerHTML = '';
267
+ if (step.category) {
268
+ const badge = document.createElement('span');
269
+ const styleClasses = categoryStyles[step.category] || categoryStyles["Default"];
270
+ badge.className = `${styleClasses} text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full inline-block`;
271
+ badge.textContent = step.category;
272
+ explanationStepCategoryContainer.appendChild(badge);
273
+ explanationStepCategoryContainer.classList.remove('hidden');
274
+ } else {
275
+ explanationStepCategoryContainer.classList.add('hidden');
276
+ }
277
+
278
+ explanationStepTitle.textContent = step.title;
279
+ explanationText.innerHTML = step.explanation;
280
+
281
+ lineElements.forEach(el => {
282
+ el.classList.remove('highlighted', 'code-line-dimmed');
283
+ });
284
+
285
+ let firstHighlightedLineElement = null;
286
+ let highlightedLineNumbersForMeta = [];
287
+
288
+ if (hasLineFocus) {
289
+ const focusedIndices = new Set();
290
+ step.lineRanges.forEach(range => {
291
+ for (let i = range[0]; i <= range[1]; i++) {
292
+ focusedIndices.add(i);
293
+ if (lineElements[i]) {
294
+ if (!highlightedLineNumbersForMeta.includes(i + 1)) {
295
+ highlightedLineNumbersForMeta.push(i + 1);
296
+ }
297
+ if (!firstHighlightedLineElement) firstHighlightedLineElement = lineElements[i];
298
+ }
299
+ }
300
+ });
301
+
302
+ lineElements.forEach((el, idx) => {
303
+ if (focusedIndices.has(idx)) {
304
+ el.classList.add('highlighted');
305
+ } else {
306
+ el.classList.add('code-line-dimmed');
307
+ }
308
+ });
309
+ highlightedLineNumbersForMeta.sort((a,b) => a - b);
310
+ const rangesSummary = highlightedLineNumbersForMeta.length === 1 ? `Line ${highlightedLineNumbersForMeta[0]}` : `Lines ${highlightedLineNumbersForMeta.join(', ')}`;
311
+ explanationMeta.textContent = `Focus: ${rangesSummary}`;
312
+
313
+ } else {
314
+ explanationMeta.textContent = "General overview for this step.";
315
+ }
316
+
317
+ if (firstHighlightedLineElement) {
318
+ const codePane = codeLinesContainer.parentElement;
319
+ if (codePane.scrollHeight > codePane.clientHeight) {
320
+ const lineRect = firstHighlightedLineElement.getBoundingClientRect();
321
+ const paneRect = codePane.getBoundingClientRect();
322
+ if (lineRect.top < paneRect.top || lineRect.bottom > paneRect.bottom) {
323
+ firstHighlightedLineElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
324
+ }
325
+ }
326
+ }
327
+
328
+
329
+ prevBtn.disabled = currentStepIndex === 0;
330
+ nextBtn.disabled = currentStepIndex === flatCodeSteps.length - 1;
331
+ progressIndicator.textContent = `Step ${currentStepIndex + 1} / ${flatCodeSteps.length}`;
332
+ updateOutlineActiveState();
333
+ }
334
+
335
+ nextBtn.addEventListener('click', () => {
336
+ if (currentStepIndex < flatCodeSteps.length - 1) {
337
+ currentStepIndex++;
338
+ updateStep();
339
+ }
340
+ });
341
+
342
+ prevBtn.addEventListener('click', () => {
343
+ if (currentStepIndex > 0) {
344
+ currentStepIndex--;
345
+ updateStep();
346
+ }
347
+ });
348
+
349
+ resetBtn.addEventListener('click', () => {
350
+ currentStepIndex = 0;
351
+ updateStep();
352
+ const codePane = codeLinesContainer.parentElement;
353
+ if (codePane) {
354
+ codePane.scrollTop = 0;
355
+ }
356
+ const outlineNav = stepsOutlineUl.parentElement;
357
+ if (outlineNav && !outlineNav.classList.contains('hidden')) { // Check if outlineNav is visible
358
+ outlineNav.scrollTop = 0;
359
+ }
360
+ });
361
+
362
+ renderCode();
363
+ renderOutline();
364
+ updateStep();
365
+ });