From 85cfe9373e94fd7a5eda5727ae8b2bbe5306d0c4 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 4 Dec 2025 11:52:54 -0600 Subject: [PATCH 1/2] Switch applets from storing state via `RECORD_FORM_LABEL` to using the `PERSISTENCE_HASH`. The applet state is not a "kept extra answer" which is what the `RECORD_FORM_LABEL` approach is for. The applet state isn't an answer at all. It is just extra problem data that needs to persist. So exactly what the `PERSISTENCE_HASH` is for. In addition, webwork2 stores the kept extra answers in the `last_answer` column which is type `TEXT` and thus limited to 64KB. The data in the `PERSISTENCE_HASH` is stored in the `problem_data` column which is of type `MEDIUMTEXT` and so can hold up to 16MB of data. The applet state can become larger than 64KB as is evidenced by the issue posted in the forums at https://forums.openwebwork.org/mod/forum/discuss.php?d=8785. There is also a litle clean up of the `insertAll` method of the `AppletObjects.pl` macro. It can take advantage of the `tag` method. --- htdocs/js/AppletSupport/ww_applet_support.js | 1 - lib/Applet.pm | 4 +- macros/graph/AppletObjects.pl | 75 +++++++------------- 3 files changed, 27 insertions(+), 53 deletions(-) diff --git a/htdocs/js/AppletSupport/ww_applet_support.js b/htdocs/js/AppletSupport/ww_applet_support.js index 395969bf3f..c9446cc30b 100644 --- a/htdocs/js/AppletSupport/ww_applet_support.js +++ b/htdocs/js/AppletSupport/ww_applet_support.js @@ -95,7 +95,6 @@ class ww_applet { if (typeof newState === 'undefined') newState = 'restart_applet'; const stateInput = ww_applet_list[this.appletName].stateInput; getQE(stateInput).value = newState; - getQE(`previous_${stateInput}`).value = newState; } // STATE: diff --git a/lib/Applet.pm b/lib/Applet.pm index 78f7e9789b..12b42a4678 100644 --- a/lib/Applet.pm +++ b/lib/Applet.pm @@ -270,8 +270,8 @@ JavaScript "submitAction()" which then asks each of the applets on the page to p submit action which consists of -- If the applet is to be reinitialized (appletName_state contains - restart_applet) then the HTML elements appletName_state and - previous_appletName_state are set to restart_applet to be interpreted by the + restart_applet) then the HTML element appletName_state + is set to restart_applet to be interpreted by the next setState command. -- Otherwise getState() from the applet and save it to the HTML input element appletName_state. diff --git a/macros/graph/AppletObjects.pl b/macros/graph/AppletObjects.pl index e8ad53a762..d85047f76a 100644 --- a/macros/graph/AppletObjects.pl +++ b/macros/graph/AppletObjects.pl @@ -69,54 +69,23 @@ =head2 insertAll # Inserts both header text and object text. sub insertAll { - my $self = shift; - my %options = @_; - - my $includeAnswerBox = (defined($options{includeAnswerBox}) && $options{includeAnswerBox} == 1) ? 1 : 0; - - my $reset_button = $options{reinitialize_button} || 0; - - # Get data to be interpolated into the HTML code defined in this subroutine. - # This consists of the name of the applet and the names of the routines to get and set State - # of the applet (which is done every time the question page is refreshed and to get and set - # Config which is the initial configuration the applet is placed in when the question is - # first viewed. It is also the state which is returned to when the reset button is pressed. + my ($self, %options) = @_; # Prepare html code for storing state. my $appletName = $self->appletName; # The name of the hidden "answer" blank storing state. $self->{stateInput} = "$main::PG->{QUIZ_PREFIX}${appletName}_state"; - my $appletStateName = $self->{stateInput}; - - # Names of routines for this applet - my $getState = $self->getStateAlias; - my $setState = $self->setStateAlias; - my $getConfig = $self->getConfigAlias; - my $setConfig = $self->setConfigAlias; - my $base64_initialState = $self->base64_encode($self->initialState); # This insures that the state will be saved from one invocation to the next. - # FIXME -- with PGcore the persistant data mechanism can be used instead - main::RECORD_FORM_LABEL($appletStateName); - my $answer_value = ''; - - # Implement the sticky answer mechanism for maintaining the applet state when the question - # page is refreshed This is important for guest users for whom no permanent record of - # answers is recorded. - if (defined(${$main::inputs_ref}{$appletStateName}) && ${$main::inputs_ref}{$appletStateName} =~ /\S/) { - $answer_value = ${$main::inputs_ref}{$appletStateName}; - } elsif (defined($main::rh_sticky_answers->{$appletStateName})) { - $answer_value = shift(@{ $main::rh_sticky_answers->{$appletStateName} }); + my $answer_value = ${$main::inputs_ref}{ $self->{stateInput} } // ''; + if ($answer_value !~ /\S/ && defined(my $persistent_data = main::persistent_data($self->{stateInput}))) { + $answer_value = $persistent_data; } $answer_value =~ tr/\\$@`//d; # Make sure student answers cannot be interpolated by e.g. EV3 $answer_value =~ s/\s+/ /g; # Remove excessive whitespace from student answer # Regularize the applet's state which could be in either XML format or in XML format encoded by base64. - # In rare cases it might be simple string. Protect against that by putting xml tags around the state. - # The result: - # $base_64_encoded_answer_value -- a base64 encoded xml string - # $decoded_answer_value -- an xml string - + # In rare cases it might be a simple string. Protect against that by putting xml tags around the state. my $base_64_encoded_answer_value; my $decoded_answer_value; if ($answer_value =~ /<\??xml/i) { @@ -125,10 +94,8 @@ sub insertAll { } else { $decoded_answer_value = $self->base64_decode($answer_value); if ($decoded_answer_value =~ /<\??xml/i) { - # Great, we've decoded the answer to obtain an xml string $base_64_encoded_answer_value = $answer_value; } else { - #WTF?? apparently we don't have XML tags $answer_value = "$answer_value"; $base_64_encoded_answer_value = $self->base64_encode($answer_value); $decoded_answer_value = $answer_value; @@ -136,25 +103,33 @@ sub insertAll { } $base_64_encoded_answer_value =~ s/\r|\n//g; # Get rid of line returns + main::persistent_data($self->{stateInput} => $base_64_encoded_answer_value); + # Construct the reset button string (this is blank if the button is not to be displayed). - my $reset_button_str = $reset_button - ? qq!
! + my $reset_button_str = $options{reinitialize_button} + ? main::tag( + 'button', + type => 'button', + class => 'btn btn-primary applet-reset-btn mt-3', + 'data-applet-name' => $appletName, + 'Return this question to its initial state' + ) : ''; - # Combine the state_input_button and the reset button into one string. - my $state_storage_html_code = qq!! - . qq!! - . $reset_button_str; + # Construct the state storage hidden input. + my $state_storage_html_code = main::tag( + 'input', + type => 'hidden', + name => $self->{stateInput}, + id => $self->{stateInput}, + value => $base_64_encoded_answer_value + ); # Construct the answerBox (if it is requested). This is a default input box for interacting # with the applet. It is separate from maintaining state but it often contains similar # data. Additional answer boxes or buttons can be defined but they must be explicitly # connected to the applet with additional JavaScript commands. - my $answerBox_code = $includeAnswerBox - ? $answerBox_code = main::NAMED_HIDDEN_ANS_RULE($self->{answerBoxAlias}, 50) - : ''; + my $answerBox_code = $options{includeAnswerBox} ? main::NAMED_HIDDEN_ANS_RULE($self->{answerBoxAlias}, 50) : ''; # Insert header material main::HEADER_TEXT($self->insertHeader()); @@ -162,7 +137,7 @@ sub insertAll { # Return HTML or TeX strings to be included in the body of the page return main::MODES( TeX => ' {\bf ' . $self->{type} . ' applet } ', - HTML => $self->insertObject . $main::BR . $state_storage_html_code . $answerBox_code, + HTML => $self->insertObject . $state_storage_html_code . $reset_button_str . $answerBox_code, PTX => ' applet ' ); } From 6e5d210d07fc5a68969e2e7e77b4f1820bc89ad3 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 4 Dec 2025 18:10:46 -0600 Subject: [PATCH 2/2] Switch ww_applet_support.js from using a submit handler to click handlers on the submit buttons. This makes applet problems work in the PG problem editor of webwork2. The PG problem editor uses click handlers on the submit buttons as well, but calls `preventDefault` on the event, and prevents the form submission from occuring. That prevents the current submit handler set in the ww_applet_support.js code from happening. Thus the answers for applets do not get submitted. By using the click handlers this gets in at the same point that the PG problem editor handlers are, and they still occur. All click handlers are executed even if one of them prevents default behavior. --- htdocs/js/AppletSupport/ww_applet_support.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/htdocs/js/AppletSupport/ww_applet_support.js b/htdocs/js/AppletSupport/ww_applet_support.js index c9446cc30b..e9d599d8c3 100644 --- a/htdocs/js/AppletSupport/ww_applet_support.js +++ b/htdocs/js/AppletSupport/ww_applet_support.js @@ -274,12 +274,14 @@ class ww_applet { if (form.submitHandlerInitialized) return; form.submitHandlerInitialized = true; - // Connect the submit action handler to the form. - form.addEventListener('submit', () => { - for (const appletName in ww_applet_list) { - ww_applet_list[appletName].submitAction(); - } - }); + // Connect the submit action handler to the form submit buttons. + for (const button of form.querySelectorAll('input[type="submit"]')) { + button.addEventListener('click', () => { + for (const appletName in ww_applet_list) { + ww_applet_list[appletName].submitAction(); + } + }); + } }; // Initialize applet support and the applets.