Skip to content

Commit

Permalink
Add an HTML/JavaScript drop down menu option to parserPopUp.pl.
Browse files Browse the repository at this point in the history
The point of this is to make a drop down menu that can contain math mode
content. A native select of course can not contain such content other
than in string form which is ugly at best.

The option is `useHTMLSelect`.  The default value for this option is 1
which means that a native HTML select element is used.  That means that
the current behavior of a drop down menu is used. If `useHTMLSelect` is
set to 0, then instead a Bootstrap drop down is used instead of a native
HTML select. In this case the choices must be provided that satisfy the
constraint that they will work directly in HTML and will also work if
placed in a `\text{...}` call in LaTeX.  In HTML of course `\(...\)` or
`\[...\]` will work and will be typeset by MathJax.  Those also work
inside `\text{...}` in LaXeX.  So the drop down could have choices like
`\(y < \frac{3}{4}\)` or `Choice 1: \(y^2 = e^x\)`.

The drop down menu is styled to appear much like the native select, and
JavaScript designed to make the drop down behave much the same. There
are some differences such as the down arrow for a Bootstrap drop down
menu looks a little different than that of a select element, and there
is a hover/focus background color change for a Bootstrap drop down.
  • Loading branch information
drgrice1 committed Dec 8, 2024
1 parent 3982cd9 commit 081f003
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 43 deletions.
63 changes: 63 additions & 0 deletions htdocs/js/DropDown/dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(() => {
const setupDropdown = (dropdown) => {
const input = dropdown?.querySelector(`input[name="${dropdown.dataset.feedbackInsertElement}"]`);
const dropdownBtn = dropdown?.querySelector('button.dropdown-toggle');
if (!dropdown || !input || !dropdownBtn) return;

// Give the dropdown button the correct/incorrect colors.
if (input.classList.contains('correct')) dropdownBtn.classList.add('correct');
if (input.classList.contains('incorrect')) dropdownBtn.classList.add('incorrect');
if (input.classList.contains('partially-correct')) dropdownBtn.classList.add('partially-correct');

const options = Array.from(dropdown.querySelectorAll('.dropdown-item:not(.disabled)'));

dropdown.addEventListener('shown.bs.dropdown', () => {
for (const option of options) {
if (option.classList.contains('active')) {
option.focus();
break;
}
}
});

for (const option of options) {
option.addEventListener('click', () => {
options.forEach((o) => o.classList.remove('active'));
option.classList.add('active');
input.value = option.dataset.value;
dropdownBtn.textContent = option.dataset.content;
dropdownBtn.focus();

if (window.MathJax)
MathJax.startup.promise = MathJax.startup.promise.then(() => MathJax.typesetPromise([dropdownBtn]));

// If any feedback popovers are open, then update their positions.
for (const popover of document.querySelectorAll('.ww-feedback-btn')) {
bootstrap.Popover.getInstance(popover)?.update();
}
});
}
};

// Set up dropdowns that are already in the page.
document.querySelectorAll('.pg-dropdown').forEach(setupDropdown);

// Observer that sets up MathQuill inputs.
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
if (node.classList.contains('pg-dropdown')) {
setupDropdown(node);
} else {
node.querySelectorAll('.pg-dropdown').forEach(setupDropdown);
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });

// Stop the mutation observer when the window is closed.
window.addEventListener('unload', () => observer.disconnect());
})();
33 changes: 33 additions & 0 deletions htdocs/js/DropDown/dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.pg-dropdown {
.btn.dropdown-toggle {
--bs-btn-color: #555;
--bs-btn-bg: white;
--bs-btn-padding-y: 0.2rem;
--bs-btn-padding-x: 0.45rem;
--bs-btn-font-size: 0.85rem;
--bs-btn-border-radius: 4px;
--bs-btn-border-color: #ccc;

&.show {
border-color: rgba(112, 154, 192, 0.8);
outline: 0;
box-shadow:
inset 0 1px 1px rgba(0, 0, 0, 0.25),
0 0 0 0.2rem rgba(136, 187, 221, 0.8);
}

&::after {
margin-left: 0.9em;
}
}

.dropdown-menu {
--bs-dropdown-min-width: 100%;
}

.dropdown-item {
--bs-dropdown-link-active-color: black;
--bs-dropdown-link-active-bg: lightgray;
--bs-dropdown-link-hover-bg: #d3d3d387;
}
}
2 changes: 2 additions & 0 deletions htdocs/js/Problem/problem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
input[type='radio'],
input[type='checkbox'],
span[id^='mq-answer'],
.pg-dropdown .btn.dropdown-toggle,
.graphtool-container {
&.correct:not(:focus):not(.mq-focused) {
border-color: rgba(81, 153, 81, 0.8); /* green */
Expand Down Expand Up @@ -118,6 +119,7 @@
input[type='radio'],
input[type='checkbox'],
textarea,
.pg-dropdown .btn.dropdown-toggle,
select {
&:focus {
border-color: rgba(112, 154, 192, 0.8);
Expand Down
153 changes: 110 additions & 43 deletions macros/parsers/parserPopUp.pl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ =head1 DESCRIPTION
Note that drop-down menus cannot contain mathematical notation, only
plain text. This is because the browser's native menus are used, and
these can contain only text, not mathematics or graphics.
these can contain only text, not mathematics or graphics. That is unless
the C<useHTMLSelect> option is set to 0. See more about that option
below.
The difference between C<PopUp()> and C<DropDown() >is that in HTML,
the latter will have an unselectable placeholder value. This value
Expand Down Expand Up @@ -137,6 +139,16 @@ =head1 DESCRIPTION
unnecessary in a static output format.) Default: 1, except 0 for
DropDownTF.
=item C<S<< useHTMLSelect => 0 or 1 >>>
If this is set to 1 (the default) then a native HTML select element will
be used for the dropdown menu. However, if this is set to 0 then a
Bootstrap Dropdown will be used instead. In this case, the answer labels
must work directly in HTML, and must also work inside C<\text{...}> in
LaTeX. Note that math mode (C<\(...\)> or C<\[...\]>) can be used in
these labels. In HTML those will be typeset by MathJax, and in hard copy
will be typeset by LaTeX.
=back
To insert the drop-down into the problem text when using PGML:
Expand Down Expand Up @@ -226,13 +238,14 @@ sub new {
$context->{parser}{String} = "parser::PopUp::String";
$context->update;
$self = bless {
data => [$value],
context => $context,
choices => $choices,
placeholder => $options{placeholder} // '',
showInStatic => $options{showInStatic} // 1,
values => $options{values} // [],
noindex => $options{noindex} // 0
data => [$value],
context => $context,
choices => $choices,
placeholder => $options{placeholder} // '',
showInStatic => $options{showInStatic} // 1,
values => $options{values} // [],
noindex => $options{noindex} // 0,
useHTMLSelect => $options{useHTMLSelect} // 1
}, $class;
$self->getChoiceOrder;
$self->addLabelsValues;
Expand Down Expand Up @@ -355,8 +368,19 @@ sub cmp_preprocess {
}
}

# Allow users to convert the value string into a label
sub quoteTeX {
my ($self, $s) = @_;
return "\\text{$s}" unless $self->{useHTMLSelect};
return $self->SUPER::quoteTeX($s);
}

sub quoteHTML {
my ($self, $s) = @_;
return $s unless $self->{useHTMLSelect};
return $self->SUPER::quoteHTML($s);
}

# Allow users to convert the value string into a label
sub answerLabel {
my ($self, $value) = @_;
my $index = $self->getIndexByValue($value);
Expand Down Expand Up @@ -388,43 +412,86 @@ sub MENU {
my $aria_label = main::generate_aria_label($name);

if ($main::displayMode =~ m/^HTML/) {
$menu = main::tag(
'div',
class => 'd-inline text-nowrap',
data_feedback_insert_element => $name,
data_feedback_insert_method => 'append_content',
main::tag(
'select',
class => 'pg-select',
name => $name,
id => $name,
aria_label => $aria_label,
size => 1,
(
$self->{placeholder}
? main::tag(
'option',
disabled => undef,
selected => undef,
value => '',
class => 'tex2jax_ignore',
if ($self->{useHTMLSelect}) {
$menu = main::tag(
'div',
class => 'd-inline text-nowrap',
data_feedback_insert_element => $name,
data_feedback_insert_method => 'append_content',
main::tag(
'select',
class => 'pg-select',
name => $name,
id => $name,
aria_label => $aria_label,
size => 1,
(
$self->{placeholder}
)
: ''
)
. join(
'',
map {
main::tag(
'option', $self->{values}[$_] eq $answer_value ? (selected => undef) : (),
value => $self->{values}[$_],
class => 'tex2jax_ignore',
$self->quoteHTML($self->{labels}[$_], 1)
? main::tag(
'option',
disabled => undef,
selected => undef,
value => '',
class => 'tex2jax_ignore',
$self->{placeholder}
)
} (0 .. $#list)
: ''
)
. join(
'',
map {
main::tag(
'option', $self->{values}[$_] eq $answer_value ? (selected => undef) : (),
value => $self->{values}[$_],
class => 'tex2jax_ignore',
$self->quoteHTML($self->{labels}[$_], 1)
)
} (0 .. $#list)
)
)
);
} else {
main::ADD_CSS_FILE('js/DropDown/dropdown.css');
main::ADD_JS_FILE('js/DropDown/dropdown.js', 0, { defer => undef });

$menu = main::tag(
'div',
class => 'dropdown-center pg-dropdown d-inline',
data_feedback_insert_element => $name,
data_feedback_insert_method => 'append_content',
join(
'',
main::tag('input', type => 'hidden', name => $name, value => $answer_value),
main::tag(
'button',
class => 'btn btn-outline-dark dropdown-toggle text-nowrap ',
type => 'button',
data_bs_toggle => 'dropdown',
aria_expanded => 'false',
$answer_value ne '' ? $self->answerLabel($answer_value) : defined $self->{placeholder}
&& $self->{placeholder} ne '' ? $self->{placeholder} : '?'
),
main::tag(
'ul',
class => 'dropdown-menu',
join(
'',
map {
main::tag(
'button',
class => 'dropdown-item'
. ($self->{values}[$_] eq $answer_value ? ' active' : ''),
type => 'button',
data_value => $self->{values}[$_],
data_content => $self->{labels}[$_],
$self->{labels}[$_]
)
} (0 .. $#list)
)
)
)
);
)
);
}
} elsif ($main::displayMode eq 'PTX') {
if ($self->{showInStatic}) {
$menu = main::tag(
Expand Down

0 comments on commit 081f003

Please sign in to comment.