Update math equation rendering
This commit changes nothing in the RCE, but changes how canvas renders equations in the resulting page. Rather than adding mathml in a hidden span adjacent to the equation image for MathJax to process, this change replaces the image with a span containing the LaTex source for MathJax to format. This is better for a couple reasons. MathJax is not intended for formatting equations as they are being edited and dealing with MathJax processed equations in the RCE when you may want to edit an existing equation is never going to work well. The visible MathJax-ified equations in the resulting page provides the accessiblity we require. This approach will update all existing content with math images, so old content gets the benefit too. Contrary to what some believe, you will not be able to select, copy and paste parts of an equation. closes: LS-1401 flag=new_math_equation_handling test plan: - with new_math_equation_handling flag on - insert a math equation in the RCE > notice that when you click on the equation's image, you do not get the "Image Options" popup button - click on the equation > expect the "Edit Equation" popup button, not "Image Options" - edit the equation and save > expect it to be updated - save the page > expect the equation to fade into view after being processed by MathJax. > expect the MathJax menu when you right click in it > expect screenreaders read it nicely - edit the page again > expect the equation as an image again. > expect to be able to edit the equation - switch your user to a different language - open a page with an equation and right-click in the eq. > expect the mathjax context menu to be in the user's language (assuming mathjax supports it) Change-Id: Ieac6785d51c0cab475b1176712f46fc2c964ff71 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/247471 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Jackson Howe <jackson.howe@instructure.com> QA-Review: Daniel Sasaki <dsasaki@instructure.com> Product-Review: Peyton Craighill <pcraighill@instructure.com>
This commit is contained in:
parent
842daace63
commit
a992d05633
|
@ -214,7 +214,7 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
# put feature checks on Account.site_admin and @domain_root_account that we're loading for every page in here
|
# put feature checks on Account.site_admin and @domain_root_account that we're loading for every page in here
|
||||||
# so altogether we can get them faster the vast majority of the time
|
# so altogether we can get them faster the vast majority of the time
|
||||||
JS_ENV_SITE_ADMIN_FEATURES = [:cc_in_rce_video_tray, :featured_help_links, :rce_lti_favorites].freeze
|
JS_ENV_SITE_ADMIN_FEATURES = [:cc_in_rce_video_tray, :featured_help_links, :rce_lti_favorites, :new_math_equation_handling].freeze
|
||||||
JS_ENV_ROOT_ACCOUNT_FEATURES = [
|
JS_ENV_ROOT_ACCOUNT_FEATURES = [
|
||||||
:direct_share, :assignment_bulk_edit, :responsive_awareness, :recent_history,
|
:direct_share, :assignment_bulk_edit, :responsive_awareness, :recent_history,
|
||||||
:responsive_misc, :product_tours, :module_dnd, :files_dnd, :unpublished_courses, :bulk_delete_pages
|
:responsive_misc, :product_tours, :module_dnd, :files_dnd, :unpublished_courses, :bulk_delete_pages
|
||||||
|
@ -2138,7 +2138,7 @@ class ApplicationController < ActionController::Base
|
||||||
is_public: is_public
|
is_public: is_public
|
||||||
).processed_url
|
).processed_url
|
||||||
end
|
end
|
||||||
UserContent.escape(rewriter.translate_content(str), request.host_with_port)
|
UserContent.escape(rewriter.translate_content(str), request.host_with_port, Account.site_admin.feature_enabled?(:new_math_equation_handling))
|
||||||
end
|
end
|
||||||
helper_method :public_user_content
|
helper_method :public_user_content
|
||||||
|
|
||||||
|
|
|
@ -80,28 +80,63 @@
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.math_equation_latex {
|
||||||
|
visibility: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.fade-in-equation {
|
||||||
|
visibility: visible;
|
||||||
|
animation: fadein ease 0.3s;
|
||||||
|
}
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Visibility utilities
|
// Visibility utilities
|
||||||
|
|
||||||
// For desktops
|
// For desktops
|
||||||
.hidden-phone { }
|
.hidden-phone {
|
||||||
.hidden-tablet { }
|
}
|
||||||
.hidden-desktop { display: none !important; }
|
.hidden-tablet {
|
||||||
.visible-desktop { display: inherit !important; }
|
}
|
||||||
|
.hidden-desktop {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.visible-desktop {
|
||||||
|
display: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
// Tablets & small desktops only
|
// Tablets & small desktops only
|
||||||
@media (min-width: 768px) and (max-width: 979px) {
|
@media (min-width: 768px) and (max-width: 979px) {
|
||||||
// Hide everything else
|
// Hide everything else
|
||||||
.hidden-desktop { display: inherit !important; }
|
.hidden-desktop {
|
||||||
.visible-desktop { display: none !important ; }
|
display: inherit !important;
|
||||||
|
}
|
||||||
|
.visible-desktop {
|
||||||
|
display: none !important ;
|
||||||
|
}
|
||||||
// Hide
|
// Hide
|
||||||
.hidden-tablet { display: none !important; }
|
.hidden-tablet {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phones only
|
// Phones only
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
// Hide everything else
|
// Hide everything else
|
||||||
.hidden-desktop { display: inherit !important; }
|
.hidden-desktop {
|
||||||
.visible-desktop { display: none !important; }
|
display: inherit !important;
|
||||||
|
}
|
||||||
|
.visible-desktop {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
// Hide
|
// Hide
|
||||||
.hidden-phone { display: none !important; }
|
.hidden-phone {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,3 +56,11 @@ enable_fullstory:
|
||||||
description: |-
|
description: |-
|
||||||
Include FullStory recording of the user's session
|
Include FullStory recording of the user's session
|
||||||
applies_to: SiteAdmin
|
applies_to: SiteAdmin
|
||||||
|
new_math_equation_handling:
|
||||||
|
state: hidden
|
||||||
|
display_name: Updated math equation display
|
||||||
|
description: |-
|
||||||
|
Replaces the existing image of the math equation plus the hidden mathml
|
||||||
|
with a MathJax formatted version. This provides better accessibility
|
||||||
|
and many options for the reader to interact with the equation on the page.
|
||||||
|
applies_to: SiteAdmin
|
||||||
|
|
|
@ -20,7 +20,7 @@ require 'ritex'
|
||||||
require 'securerandom'
|
require 'securerandom'
|
||||||
|
|
||||||
module UserContent
|
module UserContent
|
||||||
def self.escape(str, current_host = nil)
|
def self.escape(str, current_host = nil, use_updated_math_rendering = false)
|
||||||
html = Nokogiri::HTML::DocumentFragment.parse(str)
|
html = Nokogiri::HTML::DocumentFragment.parse(str)
|
||||||
find_user_content(html) do |obj, uc|
|
find_user_content(html) do |obj, uc|
|
||||||
uuid = SecureRandom.uuid
|
uuid = SecureRandom.uuid
|
||||||
|
@ -55,13 +55,23 @@ module UserContent
|
||||||
|
|
||||||
find_equation_images(html) do |node|
|
find_equation_images(html) do |node|
|
||||||
equation = node['data-equation-content'] || node['alt']
|
equation = node['data-equation-content'] || node['alt']
|
||||||
mathml = UserContent.latex_to_mathml(equation)
|
next if equation.blank?
|
||||||
next if mathml.blank?
|
if use_updated_math_rendering
|
||||||
|
# replace the equation image with a span containing the
|
||||||
mathml_span = Nokogiri::HTML::DocumentFragment.parse(
|
# LaTex, which MathJAX will typeset once we're in the browser
|
||||||
"<span class=\"hidden-readable\">#{mathml}</span>"
|
latex_span = Nokogiri::HTML::DocumentFragment.parse(
|
||||||
)
|
"<span class=\"math_equation_latex\">\\(#{equation}\\)</span>"
|
||||||
node.add_next_sibling(mathml_span)
|
)
|
||||||
|
node.replace(latex_span)
|
||||||
|
else
|
||||||
|
mathml = UserContent.latex_to_mathml(equation)
|
||||||
|
next if mathml.blank?
|
||||||
|
|
||||||
|
mathml_span = Nokogiri::HTML::DocumentFragment.parse(
|
||||||
|
"<span class=\"hidden-readable\">#{mathml}</span>"
|
||||||
|
)
|
||||||
|
node.add_next_sibling(mathml_span)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
html.to_s.html_safe
|
html.to_s.html_safe
|
||||||
|
|
|
@ -53,6 +53,25 @@ tinymce.create('tinymce.plugins.InstructureEquation', {
|
||||||
return () => ed.off('NodeChange', toggleActive)
|
return () => ed.off('NodeChange', toggleActive)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isEquationImage(node) {
|
||||||
|
return node.tagName === 'IMG' && node.classList.contains('equation_image')
|
||||||
|
}
|
||||||
|
|
||||||
|
ed.ui.registry.addButton('instructure-equation-options', {
|
||||||
|
onAction(/* buttonApi */) {
|
||||||
|
ed.execCommand('instructureEquation')
|
||||||
|
},
|
||||||
|
|
||||||
|
text: formatMessage('Edit Equation')
|
||||||
|
})
|
||||||
|
|
||||||
|
ed.ui.registry.addContextToolbar('instructure-equation-toolbar', {
|
||||||
|
items: 'instructure-equation-options',
|
||||||
|
position: 'node',
|
||||||
|
predicate: isEquationImage,
|
||||||
|
scope: 'node'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,10 @@ tinymce.create('tinymce.plugins.InstructureImagePlugin', {
|
||||||
* Register the Image "Options" button that will open the Image Options
|
* Register the Image "Options" button that will open the Image Options
|
||||||
* tray.
|
* tray.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function canUpdateImageProps(node) {
|
||||||
|
return !node.classList.contains('equation_image') && isImageEmbed(node)
|
||||||
|
}
|
||||||
const buttonAriaLabel = formatMessage('Show image options')
|
const buttonAriaLabel = formatMessage('Show image options')
|
||||||
editor.ui.registry.addButton('instructure-image-options', {
|
editor.ui.registry.addButton('instructure-image-options', {
|
||||||
onAction(/* buttonApi */) {
|
onAction(/* buttonApi */) {
|
||||||
|
@ -134,7 +138,7 @@ tinymce.create('tinymce.plugins.InstructureImagePlugin', {
|
||||||
editor.ui.registry.addContextToolbar('instructure-image-toolbar', {
|
editor.ui.registry.addContextToolbar('instructure-image-toolbar', {
|
||||||
items: 'instructure-image-options',
|
items: 'instructure-image-options',
|
||||||
position: 'node',
|
position: 'node',
|
||||||
predicate: isImageEmbed,
|
predicate: canUpdateImageProps,
|
||||||
scope: 'node'
|
scope: 'node'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,24 +25,37 @@ const localConfig = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadMathJax(configFile, cb = null) {
|
export function loadMathJax(configFile = 'TeX-MML-AM_HTMLorMML', cb = null) {
|
||||||
if (!isMathJaxLoaded()) {
|
if (!isMathJaxLoaded()) {
|
||||||
|
const locale = ENV.locale || 'en'
|
||||||
// signal local config to mathjax as it loads
|
// signal local config to mathjax as it loads
|
||||||
window.MathJax = localConfig
|
window.MathJax = localConfig
|
||||||
$.getScript(
|
$.getScript(
|
||||||
`//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=${configFile}`,
|
`//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=${configFile}&locale=${locale}`,
|
||||||
cb
|
() => {
|
||||||
|
window.MathJax.Hub.Register.MessageHook('End Math', function(message) {
|
||||||
|
message[1]
|
||||||
|
.querySelectorAll('.math_equation_latex')
|
||||||
|
.forEach(m => m.classList.add('fade-in-equation'))
|
||||||
|
})
|
||||||
|
|
||||||
|
cb?.()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else if (typeof cb === 'function') {
|
} else {
|
||||||
// Make sure we always call the callback if it is loaded already and make sure we
|
// Make sure we always call the callback if it is loaded already and make sure we
|
||||||
// also reprocess the page since chances are if we are requesting MathJax again,
|
// also reprocess the page since chances are if we are requesting MathJax again,
|
||||||
// something has changed on the page and needs to get pulled into the MathJax ecosystem
|
// something has changed on the page and needs to get pulled into the MathJax ecosystem
|
||||||
window.MathJax.Hub.Reprocess()
|
window.MathJax.Hub.Reprocess()
|
||||||
cb()
|
cb?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMathMLOnPage() {
|
export function isMathMLOnPage() {
|
||||||
|
// handle the change from image + hidden mathml to mathjax formatted latex
|
||||||
|
if (document.querySelector('.math_equation_latex')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
const mathElements = document.getElementsByTagName('math')
|
const mathElements = document.getElementsByTagName('math')
|
||||||
for (let i = 0; i < mathElements.length; i++) {
|
for (let i = 0; i < mathElements.length; i++) {
|
||||||
const $el = $(mathElements[i])
|
const $el = $(mathElements[i])
|
||||||
|
@ -51,14 +64,14 @@ export function isMathMLOnPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMathJaxLoaded() {
|
export function isMathJaxLoaded() {
|
||||||
return !(typeof MathJax === 'undefined')
|
return !!window.MathJax?.Hub
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* elem: string with elementId or en elem object
|
* elem: string with elementId or en elem object
|
||||||
*/
|
*/
|
||||||
export function reloadElement(elem) {
|
export function reloadElement(elem) {
|
||||||
if (window.MathJax) {
|
if (isMathJaxLoaded()) {
|
||||||
window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, elem])
|
window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, elem])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ QUnit.module('MathML and MathJax test', {
|
||||||
const mathElem = document.createElement('math')
|
const mathElem = document.createElement('math')
|
||||||
mathElem.innerHTML = '<mi>π</mi> <msup> <mi>r</mi> <mn>2</mn> </msup>'
|
mathElem.innerHTML = '<mi>π</mi> <msup> <mi>r</mi> <mn>2</mn> </msup>'
|
||||||
$('body')[0].appendChild(mathElem)
|
$('body')[0].appendChild(mathElem)
|
||||||
|
window.ENV.locale = 'en'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -37,7 +38,11 @@ test('loadMathJax loads mathJax', () => {
|
||||||
|
|
||||||
test('loadMathJax does not load mathJax', () => {
|
test('loadMathJax does not load mathJax', () => {
|
||||||
sinon.stub($, 'getScript')
|
sinon.stub($, 'getScript')
|
||||||
window.MathJax = {}
|
window.MathJax = {
|
||||||
|
Hub: {
|
||||||
|
Reprocess: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
mathml.loadMathJax('bogus')
|
mathml.loadMathJax('bogus')
|
||||||
ok(!$.getScript.called)
|
ok(!$.getScript.called)
|
||||||
$.getScript.restore()
|
$.getScript.restore()
|
||||||
|
@ -48,7 +53,7 @@ test('isMathMLOnPage returns true', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('isMathJaxLoaded return true', () => {
|
test('isMathJaxLoaded return true', () => {
|
||||||
window.MathJax = {}
|
window.MathJax = {Hub: {}}
|
||||||
ok(mathml.isMathJaxLoaded())
|
ok(mathml.isMathJaxLoaded())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue