Skip to content
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

Merged
merged 17 commits into from
Mar 22, 2024
Merged

Saving playground code state #1917

merged 17 commits into from
Mar 22, 2024

Conversation

mani-chand
Copy link
Contributor

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.

@djmitche djmitche self-requested a review March 11, 2024 17:15
@mani-chand mani-chand marked this pull request as draft March 11, 2024 17:17
@djmitche djmitche requested a review from fw-immunant March 11, 2024 19:44
@djmitche
Copy link
Collaborator

from @fw-immunant in #1476 (comment)

It seems like the text editor widget (ace_text) should have some API for getting/setting text contents, rather than digging around in its internal markup.

From the docs, it looks like these suffice: editors[0].getValue();/editors[0].setValue("fn main(){\n\n}", -1);.

While teaching I rely on being able to reset the code samples to their original contents (both while discussing a slide and before teaching the course a second time), so I'd like to make sure there's still a straightforward way to do so before this lands.

Copy link
Collaborator

@fw-immunant fw-immunant left a 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);
Copy link
Collaborator

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
Comment on lines 44 to 71
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);
}
}
Copy link
Collaborator

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);
Copy link
Collaborator

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
Comment on lines 77 to 102
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));
Copy link
Collaborator

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.

@mani-chand
Copy link
Contributor Author

I will work on it.

@mani-chand mani-chand marked this pull request as ready for review March 13, 2024 17:00
@mani-chand mani-chand requested a review from fw-immunant March 13, 2024 17:10
@mani-chand
Copy link
Contributor Author

Hello @djmitche , Its working fine. Just check it once and let me know the changes.

@mani-chand
Copy link
Contributor Author

mani-chand commented Mar 13, 2024

Hello @djmitche , Its working fine. Just check it once and let me know the changes.

Should I rename the file as addtional.js.
Because `redbox.js' doesn't sound perfect.

@djmitche
Copy link
Collaborator

Should I rename the file as addtional.js. Because `redbox.js' doesn't sound perfect.

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!

@mani-chand
Copy link
Contributor Author

Should I rename the file as addtional.js. Because `redbox.js' doesn't sound perfect.

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.

@mani-chand
Copy link
Contributor Author

Should I rename the file as addtional.js. Because `redbox.js' doesn't sound perfect.

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!

All the best for next version of Chromium .

Copy link
Collaborator

@djmitche djmitche left a 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"]
Copy link
Collaborator

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?

});
localStorage.setItem(window.location.href, JSON.stringify(codes));
}
setInterval(getCodeFromPlayground, 900);
Copy link
Collaborator

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.

});
}
}
setCodeToPlayground();
Copy link
Collaborator

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);
})()

@mani-chand
Copy link
Contributor Author

Hello @djmitche , I have did changes according to the changes required . Let me know any extra changes required. pagehide event is working fine once test it.

@mani-chand mani-chand requested a review from djmitche March 19, 2024 07:37
Copy link
Collaborator

@djmitche djmitche left a 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?

@mani-chand
Copy link
Contributor Author

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.

@djmitche
Copy link
Collaborator

I think it will be easiest to do it here, since this PR can't be merged until that's ready.

Copy link
Collaborator

@djmitche djmitche left a 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
Comment on lines 26 to 27
redBoxButton.innerHTML = "redbox";
playgroundStateButton.innerHTML = "reset code";
Copy link
Collaborator

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:

Suggested change
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";
Copy link
Collaborator

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) {
Copy link
Collaborator

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 =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
instructorMenu.innerHTML =
instructorMenu.title = "Utilities for course instructors";
instructorMenu.innerHTML =

theme/save-playgrounds.js Show resolved Hide resolved
@mani-chand mani-chand requested a review from djmitche March 21, 2024 05:36
@mani-chand
Copy link
Contributor Author

Hello @djmitche , Ignore the formatting and check it . Let me know any changes needed.

Copy link
Collaborator

@djmitche djmitche left a 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.

@@ -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"
Copy link
Collaborator

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() {
Copy link
Collaborator

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.

@mani-chand mani-chand requested a review from djmitche March 22, 2024 04:59
@mani-chand
Copy link
Contributor Author

Hello @djmitche , check and Let me know any changes needed.

Copy link
Collaborator

@djmitche djmitche left a 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?

@mani-chand
Copy link
Contributor Author

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.

@mani-chand
Copy link
Contributor Author

mani-chand commented Mar 22, 2024

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?

1st point : confirm works fine I think.

@mani-chand
Copy link
Contributor Author

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?

for 1st point confirm works fine I think.

@mani-chand mani-chand requested a review from djmitche March 22, 2024 13:02
@mani-chand
Copy link
Contributor Author

mani-chand commented Mar 22, 2024

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?

@djmitche , Once can you test it again.
It feels weird that the menu stays visible after clicking one of the items. Maybe hide it in the "click" handlers? - Completed/Solved.

@djmitche
Copy link
Collaborator

Nice! That might also address the feedback issue. I'm happy to merge this as-is, if others are OK.

@djmitche djmitche requested a review from mgeisler March 22, 2024 13:29
let code = editor.getValue();
codes.push(code);
});
localStorage.setItem(`${window.location.href}₹code`, JSON.stringify(codes));
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

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".

Copy link
Collaborator

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() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you discuss simply adding this to the theme instead? Since this will be a permanent thing on all pages, it should go into index.hbs or head.hbs.

That would simplify the code a lot since you can just write out the HTML directly.

Copy link
Collaborator

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.

@mgeisler
Copy link
Collaborator

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.

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:

image

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 editable attribute so that people can edit them inline again.

@djmitche djmitche dismissed fw-immunant’s stale review March 22, 2024 16:15

requested changes made

@djmitche djmitche merged commit b7a6e3b into google:main Mar 22, 2024
32 checks passed
@mani-chand mani-chand deleted the #1476_issue branch March 22, 2024 19:10
mgeisler added a commit that referenced this pull request Mar 24, 2024
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.
mgeisler added a commit that referenced this pull request Mar 24, 2024
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants