|
9 | 9 | // initialize MathQuill |
10 | 10 | const MQ = MathQuill.getInterface(); |
11 | 11 |
|
12 | | - let toolbarEnabled = (localStorage.getItem('MQEditorToolbarEnabled') ?? 'true') === 'true'; |
13 | | - |
14 | 12 | const setupMQInput = (mq_input) => { |
15 | 13 | if (mq_input.dataset.mqEditorInitialized) return; |
16 | 14 | mq_input.dataset.mqEditorInitialized = 'true'; |
|
50 | 48 | .join(' '), |
51 | 49 | rootsAreExponents: true, |
52 | 50 | logsChangeBase: true, |
| 51 | + useToolbar: true, |
53 | 52 | maxDepth: 10 |
54 | 53 | }; |
55 | 54 |
|
56 | 55 | // Merge options that are set by the problem. |
57 | 56 | if (answerQuill.latexInput.dataset.mqOpts) |
58 | 57 | Object.assign(cfgOptions, JSON.parse(answerQuill.latexInput.dataset.mqOpts)); |
59 | 58 |
|
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 = {}; |
77 | 60 |
|
78 | 61 | const latexEntryMode = input.classList.contains('latexentryfield'); |
79 | 62 |
|
|
276 | 259 |
|
277 | 260 | // Trigger a button press when the enter key is pressed in an answer box. |
278 | 261 | 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 | | - |
284 | 262 | // For ww2 homework if the enter_key_submit button is found, then use that. |
285 | 263 | // This Depends on $pg{options}{enterKey}. |
286 | 264 | const enterKeySubmit = document.getElementById('enter_key_submit'); |
|
291 | 269 | // If the enter_key_submit button is not found (it will not be present in tests), |
292 | 270 | // then use the preview button. |
293 | 271 | 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(); |
300 | 272 | }; |
301 | 273 |
|
302 | 274 | input.after(answerQuill); |
|
306 | 278 |
|
307 | 279 | answerQuill.textarea = answerQuill.querySelector('textarea'); |
308 | 280 |
|
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{ }' |
568 | 288 | }, |
569 | | - { once: true } |
| 289 | + 'exponent' |
570 | 290 | ); |
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 | + } |
585 | 292 |
|
586 | 293 | window.answerQuills[answerLabel] = answerQuill; |
587 | 294 |
|
|
0 commit comments