Skip to content

Commit d76f6c5

Browse files
authored
Merge pull request #1417 from drgrice1/mathquill-internal-toolbar
Update the mqeditor to use MathQuill's new internal toolbar.
2 parents af9274f + 4264918 commit d76f6c5

4 files changed

Lines changed: 50 additions & 386 deletions

File tree

htdocs/js/MathQuill/mqeditor.js

Lines changed: 11 additions & 304 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
// initialize MathQuill
1010
const MQ = MathQuill.getInterface();
1111

12-
let toolbarEnabled = (localStorage.getItem('MQEditorToolbarEnabled') ?? 'true') === 'true';
13-
1412
const setupMQInput = (mq_input) => {
1513
if (mq_input.dataset.mqEditorInitialized) return;
1614
mq_input.dataset.mqEditorInitialized = 'true';
@@ -50,30 +48,15 @@
5048
.join(' '),
5149
rootsAreExponents: true,
5250
logsChangeBase: true,
51+
useToolbar: true,
5352
maxDepth: 10
5453
};
5554

5655
// Merge options that are set by the problem.
5756
if (answerQuill.latexInput.dataset.mqOpts)
5857
Object.assign(cfgOptions, JSON.parse(answerQuill.latexInput.dataset.mqOpts));
5958

60-
// The handlers and blurWithCursor options are set after
61-
// the option merge to prevent them from being overridden.
62-
cfgOptions.handlers = {
63-
// Disable the toolbar when a text block is entered.
64-
textBlockEnter: () => {
65-
answerQuill.toolbar?.querySelectorAll('button').forEach((button) => (button.disabled = true));
66-
},
67-
// Re-enable the toolbar when a text block is exited.
68-
textBlockExit: () => {
69-
answerQuill.toolbar?.querySelectorAll('button').forEach((button) => (button.disabled = false));
70-
}
71-
};
72-
73-
cfgOptions.blurWithCursor = (e) =>
74-
toolbarEnabled &&
75-
answerQuill.toolbar &&
76-
(e.relatedTarget?.closest('.quill-toolbar') || e.relatedTarget?.classList.contains('symbol-button'));
59+
cfgOptions.handlers = {};
7760

7861
const latexEntryMode = input.classList.contains('latexentryfield');
7962

@@ -276,11 +259,6 @@
276259

277260
// Trigger a button press when the enter key is pressed in an answer box.
278261
cfgOptions.handlers.enter = () => {
279-
// Ensure that the toolbar and any open tooltips are removed.
280-
answerQuill.toolbar?.tooltips.forEach((tooltip) => tooltip.dispose());
281-
answerQuill.toolbar?.remove();
282-
delete answerQuill.toolbar;
283-
284262
// For ww2 homework if the enter_key_submit button is found, then use that.
285263
// This Depends on $pg{options}{enterKey}.
286264
const enterKeySubmit = document.getElementById('enter_key_submit');
@@ -291,12 +269,6 @@
291269
// If the enter_key_submit button is not found (it will not be present in tests),
292270
// then use the preview button.
293271
document.querySelector('input[name=previewAnswers]')?.click();
294-
295-
// For ww3
296-
const previewButtonId = answerQuill.textarea
297-
.closest('[name=problemMainForm]')
298-
?.id.replace('problemMainForm', 'previewAnswers');
299-
if (previewButtonId) document.getElementById(previewButtonId)?.click();
300272
};
301273

302274
input.after(answerQuill);
@@ -306,282 +278,17 @@
306278

307279
answerQuill.textarea = answerQuill.querySelector('textarea');
308280

309-
answerQuill.buttons = [
310-
{ id: 'frac', latex: '/', tooltip: 'fraction (/)', icon: '\\frac{\\text{ }}{\\text{ }}' },
311-
{ id: 'abs', latex: '|', tooltip: 'absolute value (|)', icon: '|\\text{ }|' },
312-
{ id: 'sqrt', latex: '\\sqrt', tooltip: 'square root (sqrt)', icon: '\\sqrt{\\text{ }}' },
313-
{ id: 'nthroot', latex: '\\root', tooltip: 'nth root (root)', icon: '\\sqrt[\\text{ }]{\\text{ }}' },
314-
{ id: 'exponent', latex: '^', tooltip: 'exponent (^)', icon: '\\text{ }^\\text{ }' },
315-
...(cfgOptions.logsChangeBase
316-
? []
317-
: [{ id: 'subscript', latex: '_', tooltip: 'subscript (_)', icon: '\\text{ }_\\text{ }' }]),
318-
{ id: 'infty', latex: '\\infty', tooltip: 'infinity (inf)', icon: '\\infty' },
319-
{ id: 'pi', latex: '\\pi', tooltip: 'pi (pi)', icon: '\\pi' },
320-
{ id: 'vert', latex: '\\vert', tooltip: 'such that (vert)', icon: '|' },
321-
{ id: 'cup', latex: '\\cup', tooltip: 'union (union)', icon: '\\cup' },
322-
// { id: 'leq', latex: '\\leq', tooltip: 'less than or equal (<=)', icon: '\\leq' },
323-
// { id: 'geq', latex: '\\geq', tooltip: 'greater than or equal (>=)', icon: '\\geq' },
324-
{ id: 'text', latex: '\\text', tooltip: 'text mode (")', icon: 'Tt' }
325-
];
326-
327-
const toolbarRemove = () => {
328-
if (answerQuill.toolbar) {
329-
const toolbar = answerQuill.toolbar;
330-
delete answerQuill.toolbar;
331-
toolbar.style.opacity = 0;
332-
window.removeEventListener('resize', toolbar.setPosition);
333-
window.removeEventListener('focus', toolbar.removeOnWindowRefocus);
334-
toolbar.tooltips.forEach((tooltip) => tooltip.dispose());
335-
toolbar.addEventListener('transitionend', () => toolbar.remove(), { once: true });
336-
toolbar.addEventListener('transitioncancel', () => toolbar.remove(), { once: true });
337-
if (toolbarEnabled && document.activeElement !== answerQuill.textarea) answerQuill.mathField.blur();
338-
}
339-
};
340-
341-
// Open the toolbar when the mathquill answer box gains focus.
342-
answerQuill.textarea.addEventListener('focusin', () => {
343-
if (!toolbarEnabled) return;
344-
if (answerQuill.toolbar) return;
345-
346-
answerQuill.toolbar = document.createElement('div');
347-
answerQuill.toolbar.tabIndex = -1;
348-
answerQuill.toolbar.classList.add('quill-toolbar');
349-
answerQuill.toolbar.style.opacity = 0;
350-
351-
answerQuill.toolbar.addEventListener('focusout', (e) => {
352-
if (
353-
!document.hasFocus() ||
354-
(e.relatedTarget &&
355-
(e.relatedTarget.closest('.quill-toolbar') ||
356-
e.relatedTarget.classList.contains('symbol-button') ||
357-
e.relatedTarget === answerQuill.textarea)) ||
358-
(answerQuill.clearButton && e.relatedTarget === answerQuill.clearButton)
359-
)
360-
return;
361-
362-
toolbarRemove();
363-
});
364-
365-
// If the window is refocused after a blur, and the focus is not on the toolbar
366-
// or the MathQuill input, then remove the toolbar.
367-
answerQuill.toolbar.removeOnWindowRefocus = () => {
368-
if (
369-
document.activeElement &&
370-
!document.activeElement.closest('.quill-toolbar') &&
371-
!document.activeElement.classList.contains('symbol-button') &&
372-
document.activeElement !== answerQuill.textarea
373-
)
374-
toolbarRemove();
375-
};
376-
window.addEventListener('focus', answerQuill.toolbar.removeOnWindowRefocus);
377-
378-
answerQuill.toolbar.tooltips = [];
379-
380-
for (const buttonData of answerQuill.buttons) {
381-
const button = document.createElement('button');
382-
button.type = 'button';
383-
button.id = `${buttonData.id}-${answerQuill.id}`;
384-
button.classList.add('symbol-button', 'btn', 'btn-dark');
385-
button.dataset.latex = buttonData.latex;
386-
button.dataset.bsToggle = 'tooltip';
387-
button.title = buttonData.tooltip;
388-
const icon = document.createElement('span');
389-
icon.id = `icon-${buttonData.id}-${answerQuill.id}`;
390-
icon.textContent = buttonData.icon;
391-
icon.setAttribute('aria-hidden', 'true');
392-
button.append(icon);
393-
answerQuill.toolbar.append(button);
394-
395-
MQ.StaticMath(icon, { mouseEvents: false, tabbable: false });
396-
397-
answerQuill.toolbar.tooltips.push(new bootstrap.Tooltip(button, { placement: 'left' }));
398-
399-
button.addEventListener('click', () => {
400-
answerQuill.textarea.focus();
401-
answerQuill.mathField.cmd(button.dataset.latex);
402-
});
403-
}
404-
405-
const getNextFocusableElement = (currentElement) => {
406-
const focusableElements = Array.from(
407-
document.querySelectorAll(
408-
'a[href]:not([tabindex="-1"]),' +
409-
'button:not([tabindex="-1"]),' +
410-
'input:not([tabindex="-1"]),' +
411-
'textarea:not([tabindex="-1"]),' +
412-
'select:not([tabindex="-1"]),' +
413-
'details:not([tabindex="-1"]),' +
414-
'[tabindex]:not([tabindex="-1"])'
415-
)
416-
);
417-
418-
let currentIndex = focusableElements.indexOf(currentElement);
419-
if (currentIndex === -1) return;
420-
421-
for (const focusableElement of focusableElements.slice(currentIndex + 1)) {
422-
if (!focusableElement.disabled && focusableElement.offsetParent !== null) return focusableElement;
423-
}
424-
};
425-
426-
answerQuill.toolbar.addEventListener('keydown', (e) => {
427-
if (e.key === 'Escape') {
428-
const nextFocusable = getNextFocusableElement(answerQuill.toolbar.lastElementChild);
429-
toolbarRemove();
430-
nextFocusable?.focus();
431-
}
432-
});
433-
434-
answerQuill.toolbar.setPosition = () => {
435-
// Note that this must be kept in sync with css. Currently each symbol button has a fixed height (due
436-
// to flex-shrink being 0) of 45px plus a 1px padding on the top and bottom plus a 1px margin on the top
437-
// and bottom, giving a 49px total height for each symbol button . Also, the toolbar itself has a 2px
438-
// border on the top and bottom, hence 4px is added to the end. These computations take into account
439-
// that box-sizing is border-box.
440-
const toolbarHeight = 49 * answerQuill.buttons.length + 4;
441-
442-
const pageHeight = (() => {
443-
const documentElHeight = document.documentElement.getBoundingClientRect().height;
444-
if (window.innerHeight > documentElHeight) return window.innerHeight;
445-
return documentElHeight;
446-
})();
447-
448-
// Different positioning is needed when contained in a relatively positioned parent.
449-
const relativeParent = (() => {
450-
let parent = answerQuill.parentElement;
451-
while (parent && parent !== document) {
452-
const positionType = window.getComputedStyle(parent).position;
453-
if (positionType === 'relative') return parent;
454-
// If a fixed parent is encountered before a relative parent is encountered,
455-
// that negates relative positioning.
456-
if (positionType === 'fixed') return;
457-
parent = parent.parentElement;
458-
}
459-
})();
460-
461-
if (relativeParent) {
462-
// If contained in a relatively positioned parent, the toolbar needs
463-
// to be positioned relative to that parent.
464-
const pageWidth = (() => {
465-
const documentElWidth = document.documentElement.getBoundingClientRect().width;
466-
if (window.innerWidth > documentElWidth) return window.innerWidth;
467-
return documentElWidth;
468-
})();
469-
470-
const parentRect = relativeParent.getBoundingClientRect();
471-
answerQuill.toolbar.style.right = `${window.scrollX + parentRect.right + 10 - pageWidth}px`;
472-
473-
const elRect = answerQuill.getBoundingClientRect();
474-
475-
if (window.scrollY + elRect.top + elRect.height / 2 < toolbarHeight / 2) {
476-
answerQuill.toolbar.style.top = `-${window.scrollY + parentRect.top}px`;
477-
answerQuill.toolbar.style.bottom =
478-
toolbarHeight > pageHeight ? `${window.scrollY + parentRect.bottom - pageHeight}px` : null;
479-
} else if (window.scrollY + elRect.top + elRect.height / 2 + toolbarHeight / 2 > pageHeight) {
480-
answerQuill.toolbar.style.top = null;
481-
answerQuill.toolbar.style.bottom = `${window.scrollY + parentRect.bottom - pageHeight}px`;
482-
} else {
483-
answerQuill.toolbar.style.top = `${
484-
elRect.top + elRect.height / 2 - toolbarHeight / 2 - parentRect.top
485-
}px`;
486-
answerQuill.toolbar.style.bottom = null;
487-
}
488-
} else {
489-
// If not in a relatively positioned parent, the toolbar is positioned absolutely on the page.
490-
if (toolbarHeight > pageHeight) {
491-
answerQuill.toolbar.style.top = 0;
492-
answerQuill.toolbar.style.height = '100%';
493-
} else {
494-
const elRect = answerQuill.getBoundingClientRect();
495-
const top = window.scrollY + elRect.bottom - elRect.height / 2 - toolbarHeight / 2;
496-
const bottom = top + toolbarHeight;
497-
answerQuill.toolbar.style.top = `${
498-
top < 0 ? 0 : bottom > pageHeight ? pageHeight - toolbarHeight : top
499-
}px`;
500-
answerQuill.toolbar.style.height = null;
501-
}
502-
}
503-
};
504-
505-
window.addEventListener('resize', answerQuill.toolbar.setPosition);
506-
answerQuill.toolbar.setPosition();
507-
508-
answerQuill.after(answerQuill.toolbar);
509-
setTimeout(() => {
510-
if (answerQuill.toolbar) answerQuill.toolbar.style.opacity = 1;
511-
}, 0);
512-
});
513-
514-
// Add a context menu to toggle whether the toolbar is enabled or not.
515-
answerQuill.addEventListener('contextmenu', (e) => {
516-
e.preventDefault();
517-
518-
const container = document.createElement('div');
519-
container.classList.add('dropdown', 'd-inline-block');
520-
answerQuill.after(container);
521-
522-
const hiddenLink = document.createElement('a');
523-
hiddenLink.classList.add('dropdown-toggle', 'd-none');
524-
hiddenLink.dataset.bsToggle = 'dropdown';
525-
hiddenLink.href = '#';
526-
container.append(hiddenLink);
527-
528-
const menuEl = document.createElement('ul');
529-
menuEl.classList.add('dropdown-menu');
530-
const li = document.createElement('li');
531-
menuEl.append(li);
532-
const action = document.createElement('a');
533-
action.classList.add('dropdown-item');
534-
action.href = '#';
535-
action.textContent = toolbarEnabled ? 'Disable Toolbar' : 'Enable Toolbar';
536-
li.append(action);
537-
container.append(menuEl);
538-
539-
const menu = new bootstrap.Dropdown(hiddenLink, {
540-
reference: answerQuill,
541-
offset: [answerQuill.offsetWidth, 0]
542-
});
543-
menu.show();
544-
545-
hiddenLink.addEventListener('hidden.bs.dropdown', () => {
546-
menu.dispose();
547-
menuEl.remove();
548-
container.remove();
549-
});
550-
551-
action.addEventListener(
552-
'click',
553-
(e) => {
554-
e.preventDefault();
555-
toolbarEnabled = !toolbarEnabled;
556-
localStorage.setItem('MQEditorToolbarEnabled', toolbarEnabled);
557-
if (!toolbarEnabled && answerQuill.toolbar) toolbarRemove();
558-
// Bootstrap tries to focus the triggering element after hiding the menu. However, the menu gets
559-
// disposed of and the hidden link which is the triggering element removed too quickly in the
560-
// hidden.bs.dropdown event, and that causes an exception. So ignore that exception so that the
561-
// answerQuill textarea is focused instead.
562-
try {
563-
menu.hide();
564-
} catch {
565-
/* ignore */
566-
}
567-
answerQuill.textarea.focus();
281+
if (!cfgOptions.logsChangeBase) {
282+
answerQuill.mathField.options.addToolbarButton(
283+
{
284+
id: 'subscript',
285+
latex: '_',
286+
tooltip: 'subscript (_)',
287+
icon: '\\text{ }_\\text{ }'
568288
},
569-
{ once: true }
289+
'exponent'
570290
);
571-
});
572-
573-
answerQuill.textarea.addEventListener('focusout', (e) => {
574-
if (
575-
!document.hasFocus() ||
576-
(e.relatedTarget &&
577-
(e.relatedTarget.closest('.quill-toolbar') ||
578-
e.relatedTarget.classList.contains('symbol-button') ||
579-
(answerQuill.clearButton && e.relatedTarget === answerQuill.clearButton)))
580-
)
581-
return;
582-
583-
toolbarRemove();
584-
});
291+
}
585292

586293
window.answerQuills[answerLabel] = answerQuill;
587294

0 commit comments

Comments
 (0)