-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Saving playground code state #1917
Conversation
from @fw-immunant in #1476 (comment)
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See individual comments.
I think for usability reasons this feature also requires that there's a way to reset the code on an individual page to its original state, the way reloading the page did previously. In addition, after teaching the full course, instructors need a way to "erase the blackboards" and reset all code samples to their pristine state.
theme/redbox.js
Outdated
|
||
function setCodeToPlayground() { | ||
const code = JSON.parse(localStorage.getItem(window.location.href)); | ||
console.log(code); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove debugging printing.
theme/redbox.js
Outdated
const playground = document.getElementsByClassName("ace_text-layer")[0]; | ||
while (playground.firstElementChild) { | ||
console.log(playground.firstElementChild.innerText.replace(/\s+/g, " ")); | ||
playground.removeChild(playground.firstElementChild); | ||
} | ||
console.log(playground, "after removal"); | ||
for (let i = 0; i < code.length; i++) { | ||
let parentDiv = code[i][0]; | ||
let spanChild = code[i][1]; | ||
let div = document.createElement(parentDiv.tag); | ||
div.style.height = "17.5938px"; | ||
div.style.top = `${17.5938 * i}px`; | ||
for (let cls in parentDiv.classes) { | ||
div.classList.add(parentDiv.classes[cls]); | ||
} | ||
for (let j = 0; j < spanChild.length; j++) { | ||
//console.log(spanChild[j].styles,typeof(spanChild[j].styles)) | ||
let span = document.createElement(spanChild[j].tag); | ||
//span.classList = spanChild[j].classes | ||
for (let cls in spanChild[j].classes) { | ||
span.classList.add(spanChild[j].classes[cls]); | ||
} | ||
span.innerText = spanChild[j].text; | ||
div.insertBefore(span, div.lastChild); | ||
} | ||
playground.insertBefore(div, playground.lastChild); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the API of the editor component rather than poking into its portion of the DOM:
editors[0].setValue("fn main(){\n\n}", -1);
or similar.
theme/redbox.js
Outdated
localStorage.removeItem(window.location.href); | ||
} | ||
|
||
window.onunload = setTimeout(setCodeToPlayground, 5000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is a type error. Isn't window.onunload
supposed to be a callable? setTimeout
returns an integer timeout id.
What is this intended to do?
theme/redbox.js
Outdated
console.log("getCodeFromPlayground"); | ||
const playground = | ||
document.getElementsByClassName("ace_text-layer")[0].children; | ||
var code = []; | ||
for (let i = 0; i < playground.length; i++) { | ||
let parentCodeList = { | ||
tag: playground[i].tagName, | ||
classes: playground[i].classList, | ||
styles: playground[i].style, | ||
}; | ||
var line = []; | ||
for (let j = 0; j < playground[i].children.length; j++) { | ||
console.log(playground[i].innerHTML); | ||
let codeList = { | ||
tag: playground[i].children[j].tagName, | ||
text: playground[i].children[j].innerText, | ||
classes: playground[i].children[j].classList, | ||
styles: playground[i].children[j].style, | ||
}; | ||
line.push(codeList); | ||
} | ||
code.push([parentCodeList, line]); | ||
} | ||
console.log(code); | ||
//localStorage.removeItem(window.location.href) | ||
localStorage.setItem(window.location.href, JSON.stringify(code)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, remove debugging prints and use the public API of the editor rather than accessing the details of its implementation in the DOM.
I don't think I see a reason to store a serialized JSON object in localStorage if the object is just the code--it should be a single string if I understand correctly what information is being persisted.
I will work on it. |
Hello @djmitche , Its working fine. Just check it once and let me know the changes. |
Should I rename the file as |
This sounds like a good idea. I've got a lot of my "regular" job to do this week, before the next version of Chromium is released, so I'll be a little slow to review here. If @fw-immunant has time to take a look, that'd be great -- otherwise I'll get to it by-and-by. Sorry for the delay! |
Its ok with delay. Take your time and review the PR. |
All the best for next version of Chromium . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! I made a few suggestions, but the part I don't know is how best to either detect the changes or respond to navigation. I know that onunload
is deprecated, and onbeforeunload
is for displaying dialogs and not for saving data.
book.toml
Outdated
@@ -46,7 +46,7 @@ urlcolor = "red" | |||
|
|||
[output.html] | |||
curly-quotes = true | |||
additional-js = ["theme/speaker-notes.js", "theme/redbox.js"] | |||
additional-js = ["theme/speaker-notes.js", "theme/additional.js"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I thought you meant adding a new JS file for this functionality, rather than moving the existing red-box functionality too. Maybe keep theme/redbox.js
and move this new functionality into theme/save-playgrounds.js
?
theme/additional.js
Outdated
}); | ||
localStorage.setItem(window.location.href, JSON.stringify(codes)); | ||
} | ||
setInterval(getCodeFromPlayground, 900); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it will wake up the browser frequently even if nothing has changed. I wonder if we could either do this on some event indicating the browser is navigating away (maybe https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event?) or on a timer after the user has modified something in the editor.
theme/additional.js
Outdated
}); | ||
} | ||
} | ||
setCodeToPlayground(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor point: this is a bit hard to spot here. Please move the whole thing to an IIFE, like the red-box functionality, and put the immediately-evaluated statements at the bottom:
(function savePlaygrounds() {
function setCodeToPlayground() ..
function getCodeFromPlaground() ..
setCodeToPlayground();
setInterval(getCodeFromPlayground, 900);
})()
Hello @djmitche , I have did changes according to the changes required . Let me know any extra changes required. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work!! This works just fine for me, and I can see in the debugger that it's setting localStorage reliably, but only when necessary.
As @fw-immunant mentioned, though, we need a way to reset all playgrounds to their default state before this lands -- otherwise instructors will be stuck with modified content from the last time they taught the course.
Maybe we could convert the aspect-ratio icon into an "instructor menu" and put the aspect-ratio item and the reset-playgrounds item in that menu. What do you think?
Should I make another PR for that or finish it here itself. |
I think it will be easiest to do it here, since this PR can't be merged until that's ready. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to solve it, but I noticed that the theme menu (the paintbrush icon) disappears if you click outside the menu, while the instructor menu does not. It'd be great to fix that if it's an easy fix.
theme/redbox.js
Outdated
redBoxButton.innerHTML = "redbox"; | ||
playgroundStateButton.innerHTML = "reset code"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be more specific about what is reset, and also include a title:
redBoxButton.innerHTML = "redbox"; | |
playgroundStateButton.innerHTML = "reset code"; | |
redBoxButton.innerHTML = "aspect-ratio box"; | |
redBoxButton.title = "Outline the area that fits on one screen while teaching the course."; | |
playgroundStateButton.innerHTML = "reset all playgrounds"; | |
playgroundStateButton.title = "Reset code in all playgrounds to its original value."; |
theme/redbox.js
Outdated
|
||
redBoxButton.id = "redbox"; | ||
|
||
instructorMenuList.style.marginLeft = "55px"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor detail: I think this would be easier to read if each item was grouped together, and all its properties set in the same place.
theme/redbox.js
Outdated
hideShowButton.innerHTML = '<i class="fa fa-square-o"></i>'; | ||
} | ||
}); | ||
(function handleInstructor(params) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this function needs params
!
It probably makes sense to move this to yet another .js
file, maybe instructor-menu
?
For JS symbols that need to be reachable from other .js
files, I think attaching them to window
will work, even if it's a bit of a hack:
(function savePlaygrounds() {
...
window.resetAllPlaygrounds = function() { .. }
})()
(function instructorMenu() {
playgroundStateButton.addEventListener("click", () => {
...
window.resetAllPlaygrounds();
});
})()
theme/redbox.js
Outdated
playgroundStateItem.lastChild | ||
); | ||
|
||
instructorMenu.innerHTML = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instructorMenu.innerHTML = | |
instructorMenu.title = "Utilities for course instructors"; | |
instructorMenu.innerHTML = |
Hello @djmitche , Ignore the formatting and check it . Let me know any changes needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works great! My remaining comments are more about maintainability, and in particular separation of concerns. That is, the instructor-menu.js
file should just set up the instructor menu and call global functions when the menu items are clicked. Everything specific to how those menu items are implemented should be in their own file. For example, the ₹code
trick should only appear in save-playgrounds.js
.
.github/workflows/build.sh
Outdated
@@ -40,6 +40,7 @@ mdbook build -d "$dest_dir" | |||
|
|||
# Disable the redbox button in built versions of the course | |||
echo '// Disabled in published builds, see build.sh' > "${dest_dir}/html/theme/redbox.js" | |||
echo '// Disabled in published builds, see build.sh' > "${dest_dir}/html/theme/instructor-menu.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! I think we don't want this anymore, as we will need the "reset playgrounds" option in the published build. @mgeisler now that we have all of this in a sub-menu, what do you think?
theme/redbox.js
Outdated
@@ -1,4 +1,4 @@ | |||
(function redBoxButton() { | |||
function redBoxButton() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should still be an IIFE, but at the end should have something like
...
window.redboxButtonClicked = redboxButtonClicked;
})();
Similarly, save-playgrounds.js
would have
...
window.resetPlaygroundsClicked = resetPlaygroundsClicked;
})();
Then instructor-menu.js
can have
redboxButton.addEventListener("click", e => window.redboxButtonClicked(e));
playgroundStateButton.addEventListener("click", e => window.resetPlaygroundsClicked(e));
with all of the implementation of what to do when those buttons are clicked appearing in the appropriate file.
Hello @djmitche , check and Let me know any changes needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks good! This was a lot of work and a lot of rounds of review, but I think the result is going to make teaching the course quite a bit better.
I'd like to get some feedback from @mgeisler regarding keeping the menu in builds before merging it.
A few minor points that could be addressed in a later PR if you'd prefer:
- There's no feedback on clicking the "reset all playgrounds" to indicate that all playgrounds were reset. In fact, if one appears on the current slide, its contents aren't reset. I know
alert()
is lame, but maybe this is a good use-case for it? - It feels weird that the menu stays visible after clicking one of the items. Maybe hide it in the "click" handlers?
I can do 2nd point. If you say it should hide on clicking one option it is possible. I will do it and push it. |
1st point : |
|
@djmitche , Once can you test it again. |
Nice! That might also address the feedback issue. I'm happy to merge this as-is, if others are OK. |
let code = editor.getValue(); | ||
codes.push(code); | ||
}); | ||
localStorage.setItem(`${window.location.href}₹code`, JSON.stringify(codes)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has it already been discussed what the ₹
character is doing here? It looks a bit strange to me 😄
Have you considered using just the pathname
instead of the full URL? Since local storage is different for different hosts, I don't think we need the full URL here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, if the ₹
is necessary somehow, then I would suggest creating a new function to construct the key. You can then document the use there — and also call it from both places that need this key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, if the
₹
is necessary somehow, then I would suggest creating a new function to construct the key. You can then document the use there — and also call it from both places that need this key.
Sorry, I didn't understand creating of "another function for key creating".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mani-chand, I'm suggesting creating a one-line function which returns the correct key for the current page. Something like
function localStorageKey() {
// The '₹' here is used for ...
return `${window.location.href}₹code`
}
You should then explain what ₹
does here 🙂
@@ -0,0 +1,71 @@ | |||
(function handleInstructor() { | |||
function handleInstructorMenu() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this not done for the red box, you might ask? Because that was not supposed to be rendered for people looking at the course in the published version.
My plans for writing a new theme are basically on pause right now, so I guess it's fine to take away some more horizontal space now. So feel free to merge this and then we can improve it further afterwards. One potential way of improving it would be to reset all playgrounds the same way as a single playground is reset: The button with the rotating arrow in the top-right resets the playground. Perhaps you could let Ctrl-click reset all playgrounds? Alternatively, we can create a page in the course which embeds the necessary JavaScript to reset all playgrounds. Call it an "Instructors toolbox" page. It could be under Running the Course since that segment is already mostly for the instructor. This can all be for later, I think it's more important to get this cool new functionality published 🎉 After merging it, we can update all the exercises to have the |
When browing around on https://google.github.io/comprehensive-rust/, I sooner or later end up in a state where the local storage has `[]` stored for a page with one or more playgrounds. The effect of this is that the code is removed from the page! I am not sure why this happens, but I’m afraid the code here needs more testing. I’m teaching a class Monday morning, so I’ll disable the code from #1917 for now.
When browing around on https://google.github.io/comprehensive-rust/, I sooner or later end up in a state where the local storage has `[]` stored for a page with one or more playgrounds. The effect of this is that the code is removed from the page! I am not sure why this happens, but I’m afraid the code here needs more testing. I’m teaching a class Monday morning, so I’ll disable the code from #1917 for now.
The embedded Playground's code reset when you navigate between slides. This has caused problems: if people navigate away from a slide to look something up, they've suddenly lost their work.