Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toggle not working after re-filling the parent div's innerHTML #2594

Closed
parhizkari opened this issue Dec 28, 2020 · 5 comments
Closed

toggle not working after re-filling the parent div's innerHTML #2594

parhizkari opened this issue Dec 28, 2020 · 5 comments
Labels
Expected Behavior This is how MathJax works v3

Comments

@parhizkari
Copy link

I have a multi-page markdown editor which benefits the usage of Mathjax. For it being multi-page, the cross referencing of equations failed to work as expected automatically until I modified the code for \eqref{}, after being processed by Mathjax, so that in action it will first set the correct value to the target page, divert to the new page, then the reference works as expected. This works very well, but just recently I have discovered that the mouse pointer over the term to be toggled is changed as on a clickable area, whereas, clicking over it will cause nothing to happen.

this is a minimal example to reproduce it:

<!DOCTYPE html>
<html class="htmlMain">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	
	<script type="text/javascript" src="tex-svg-full.js" async></script>
	<!-- <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg-full.js"></script> -->
</head>
<body>
<textarea id="source" style="width:400px; height:100px;"></textarea>
<input type="button" value="Click!" onclick="compile();">
<div id="result" style="display:inline-block;"></div>
<script>
var source=document.getElementById("source"),
	result=document.getElementById("result");

function compile(){
	result.innerHTML=source.value;
	
	MathJax.texReset();
	MathJax.typesetClear();
	MathJax.typeset();
	
	result.innerHTML=result.innerHTML;
}
</script>
</body>
</html>

The problem arises because of the line: result.innerHTML=result.innerHTML; . It is now a trivial statement left for the sake of bug reproducibility, while in the real code I have something non-trivial of the kind: result.innerHTML=result.innerHTML.replace(); . I assumed it be a bug as everything else seemingly work as far as I have checked, except for the \toggle command.

Thank you

@dpvc
Copy link
Member

dpvc commented Dec 28, 2020

The \toggle command works by adding event listeners to the DOM node for the toggle element, which causes the expression to update which sub-expression is to be displayed, and re-typesetting the expression. Your expression result.innerHTML = result.innerHTML first serializes the result tree as an HTML string then re-parses it to set the contents of result. Since event listeners are not part of the serialization, they are lost when you do this, so the toggle expression will stop working (as will the MathJax menus, pop-ups, and other features that rely on event listeners). Even if you had the event listeners, the re-typesetting when a toggle is toggled would mean that you would lose your modified URLs in the re-typeset expression (if any).

It would be better to implement a version of \eqref (and \ref) that would properly handle the external links originally. This can be done by overriding their definitions to call your own implementation that checks the reference label to see if it is one that refers to an external document, and then sets up the data needed to make the link to that document. For example, you could make the convention that \eqref{file.html#1.1} would refer to equation 1.1 in file.html (or any URL that you wanted to use). The updated \eqref could look for labels in that form and set up the data so that the original code would link to it properly. We can use the tagformat extension's url(id) method to check if the id is already a full URL and return it if it is, otherwise create the proper URL as normal.

Here is one such implementation:

<script>
MathJax = {
  loader: {load: ['[tex]/tagformat']},
  tex: {
    packages: {'[+]': ['tagformat', 'external-eqref']},
    tagformat: {
      //
      //  If the ID is already a URL, use it, otherwise construct the url as usual
      //
      url: (id, base) => (id.indexOf('#') >= 0 ? id : base + '#' + encodeURIComponent(id))
    }
  },
  startup: {
    ready() {
      //
      //  These would be replaced by import commands if you wanted to make
      //  a proper extension.
      //
      const Configuration = MathJax._.input.tex.Configuration.Configuration;
      const CommandMap = MathJax._.input.tex.SymbolMap.CommandMap;
      const Label = MathJax._.input.tex.Tags.Label;
      const BaseMethods = MathJax._.input.tex.base.BaseMethods.default;

      //
      //  Create a command map to override \ref and \eqref
      //
      new CommandMap('external-eqref', {
        ref:   ['HandleRef', false],
        eqref: ['HandleRef', true]
      }, {
        HandleRef(parser, name, eqref) {
          //
          //  Get the label parameter (keeping parse position as it is)
          //
          const i = parser.i;
          const label = parser.GetArgument(name);
          parser.i = i;
          //
          //  If the label is of the form url#tag and the label doesn't already exist
          //    split the url and tag
          //    create a label using the tag and a proper URL for the link
          //
          if (label.indexOf('#') >= 0 && !(parser.tags.allLabels[label] || parser.tags.labels[label])) {
            const [url, tag] = label.split(/#/);
            const id = parser.tags.formatId(tag);
            parser.tags.labels[label] = new Label(tag, parser.tags.formatUrl(id, url));
          }
          //
          //  Call the original function to perform the reference
          //
          BaseMethods.HandleRef(parser, name, eqref);
        }
      });
      //
      //  Create the package for the overridden macros
      //
      Configuration.create('external-eqref', {
        handler: {macro: ['external-eqref']}
      });
      
      MathJax.startup.defaultReady();
    }
  }
}
</script>

This could be made into a proper TeX extension that lives in its own file, if you wish.

@dpvc dpvc added Expected Behavior This is how MathJax works v3 labels Dec 28, 2020
@parhizkari
Copy link
Author

parhizkari commented Dec 30, 2020

@dvpc, thanks a lot,
so it was my bad implementation ... thank you for the thorough explanation. Unfortunately, I couldn't use the code you provided, as my pagination is different, I don't use external links, different Divs stand for different pages in my case, all but one being hidden, so that I should only recognize the cited equation resides on which div so that I can un-hide it first. I was previously accustomed to the code

	var eqsLabelsArrayPerPage=[];
	for(pageNum=0; pageNum<totalPageNumber; pageNum++){
		var jax = MathJax.getAllJax("resultPage-"+pageNum);
		var neWLabelsInPage=[];
		for (var i=0, l=jax.length; i<l; i++) {
			//alert(jax[i].math);
			jax[i].math.replace(/\\label\{([^\}]+)\}/g, function(x,y){
				neWLabelsInPage.push(y);
				return false;
			});
		}
		eqsLabelsArrayPerPage.push(neWLabelsInPage);
	}
	var eqOnWPVar;
	result.innerHTML=result.innerHTML.replace(/(href\=\"\#mjx\-eqn\-([^\"]*)\")/g,  function(x,y1,y2){
		eqOnWPVar=eqsLabelsArrayPerPage.findIndex(function(x) {
			return x.indexOf(y2) !== -1;
		});
		return y1+" onclick=\"currentPage="+eqOnWPVar+"; changePage();\"";
	});

the second part of which is now replaced by

	var eqCitations=[];
	result.innerHTML.replace(/(href\=\"\#mjx\-eqn\-([^\"]*)\")/g,  function(x,y1,y2){
		eqCitations=document.querySelectorAll("mrow[href='#mjx-eqn-"+y2+"']");
		
		for(var i=0, len=eqCitations.length; i<len; i++){
			eqCitations[i].parentNode.parentNode.parentNode.onclick=function(){
				currentPage=eqsLabelsArrayPerPage.findIndex(function(subArray){
					return subArray.indexOf(y2) !== -1;
				});
				changePage();
			};
		}
		return false;
	});

It is not efficient to re-evaluate the currentPage for every citation to even a single equation, but with onclick not working synchronously it seems I have no better way.

Thanks again, I very much appreciate the time you put in such conversations

@dpvc
Copy link
Member

dpvc commented Jan 2, 2021

Well, here is a possible approach that might do what you are looking for with minimal overhead. It is all done within MathJax, so doesn't need to have extra code like what you have given above, but it does take a little bit of setup to make it work.

There are three main components: a subclass of the standard tagging class that keeps track of the page that each labeled equation is on, a TeX configuration that overrides the \ref and \eqref macros in order to mark the references that point to other pages, and finally an output post-filter that adds an event listener onto those links that open another page (and closes the current one) before making the link.

I think that should do what you want (or at least give a framework for it). Here is a complete file that has several labeled equations with links to them in different pages.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<title>Page switching \eqref links</title>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script>
let currentPage = 1;
MathJax = {
  startup: {
    ready() {
      //
      //  These would be replaced by import commands if you wanted to make
      //  a proper extension.
      //
      const TagsFactory = MathJax._.input.tex.Tags.TagsFactory;
      const AmsTags = MathJax._.input.tex.ams.AmsConfiguration.AmsTags;
      
      //
      // Subclass the AmsTags object to track page information
      //
      class PageTags extends AmsTags {
        
        constructor() {
          super();
          this.page = null;        // the div for the last page where we found an equation
          this.pageNo = "";        // the number of that page
          this.math = null;        // the MathItem for the current equation
        }
        
        //
        //  Save the MathItem and do the usual startup.
        //
        startEquation(math) {
          this.math = math;
          super.startEquation(math);
        }
        
        //
        //  Check if there are labels for this equation.
        //  If so, find the page for this MathItem
        //    and save the page with the label information.
        //  The do the usual finishing up.
        //
        finishEquation(math) {
          const labels = Object.keys(this.labels);
          if (labels.length) {
            const page = this.getPage();
            labels.map((label) => {this.labels[label].page = page});
          }
          super.finishEquation(math);
        }
        
        //
        //  If there is a cached page div and this MathItem is in it, return its page number.
        //  Otherwise, look through the parents of the math until you find its page.
        //  If you found a page, cache it and get its number.
        //  Return the page number.
        //
        getPage() {
          let node = this.math.start.node;
          if (this.page && this.page.contains(node)) return this.pageNo;
          while (node && (!node.id || node.id.substr(0,11) !== 'resultPage-')) {
            node = node.parentNode;
          }
          if (node) {
            this.page = node;
            this.pageNo = node.id.substr(11);
          }
          return this.pageNo;
        }

      }
      
      //
      //  These would be replaced by import commands if you wanted to make
      //  a proper extension.
      //
      const Configuration = MathJax._.input.tex.Configuration.Configuration;
      const CommandMap = MathJax._.input.tex.SymbolMap.CommandMap;
      const Label = MathJax._.input.tex.Tags.Label;
      const ParseUtil = MathJax._.input.tex.ParseUtil.default;

      //
      //  Create a command map to override \ref and \eqref to handle page changes.
      //
      new CommandMap('page-eqref', {
        ref:   ['HandleRef', false],
        eqref: ['HandleRef', true]
      }, {
        //
        //  Copied from BaseMethods.HandleRef, and modified to add page information.
        //  
        HandleRef(parser, name, eqref) {
          //
          //  Get the label name and look up its data.
          //
          const label = parser.GetArgument(name);
          let ref = parser.tags.allLabels[label] || parser.tags.labels[label];
          //
          //  If none, then reprocess this item, if we are on the first pass,
          //  and use a blank label.
          //
          if (!ref) {
            if (!parser.tags.refUpdate) {
              parser.tags.redo = true;
            }
            ref = new Label();
          }
          //
          //  Get the tag string.
          //
          const tag = (eqref ? parser.tags.formatTag(ref.tag) : ref.tag);
          //
          //  Create an mrow with the parsed tags contents and href link.
          //
          const node = parser.create('node', 'mrow', ParseUtil.internalMath(parser, tag), {
            href: parser.tags.formatUrl(ref.id, parser.options.baseURL), 'class': 'MathJax_ref'
          });
          //
          //  If the page of the linked quation is not the same as our own equation,
          //    Add a page attribute to the mrow.
          //
          if (ref.page !== parser.tags.getPage()) {
            node.attributes.set('data-page', ref.page);
          }
          parser.Push(node);
        }
      });
      //
      //  Create the package for the overridden macros
      //
      Configuration.create('page-eqref', {
        handler: {macro: ['page-eqref']},
        tags: {page: PageTags}
      });
      
      //
      //  Do the normal setup (create the input and output jax and other objects)
      //
      MathJax.startup.defaultReady();
      
      //
      //  The listener function for when a link goes to a different page:
      //     Unhide the linked page and hide the current one.
      //     Save the current page as the linked one.
      //
      const listener = function (event) {
        const n = this.getAttribute('data-page');
        document.getElementById('resultPage-' + n).classList.remove('hidden');
        document.getElementById('resultPage-' + currentPage).classList.add('hidden');
        currentPage = n;
      };
      
      //
      //  Add a post-filter to the output jax to look for links that are to different
      //    pages, add the event listener to the <a> element for the link, and
      //    propagate the page to the anchor where the listener can find it.
      //
      MathJax.startup.output.postFilters.add(({data}) => {
        for (const link of data.querySelectorAll('[data-page]')) {
          link.parentNode.addEventListener('click', listener, true);
          link.parentNode.setAttribute('data-page', link.getAttribute('data-page'));
        }
      });
    }
  },
  tex: {
    packages: {'[+]': ['page-eqref']},
    tags: 'page'
  }
}
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<style>
div {
  border: 1px solid grey;
  padding: .5em;
}
h1 {
  font-size: 130%;
  margin-top: 0;
}
.hidden {
  display: none;
}
.spacer {
  height: 40em;
  width: 1em;
  background-color: red;
  margin: 1em 0;
  border: none;
}
</style>
</head>
<body>

<div class="spacer"></div>

<div id="resultPage-1">
<h1>Page 1</h1>
A math formula:
$$E = mc^2. \tag{1}\label{eq1}$$
That is the formula.  
We link to \eqref{eq3}.
</div>

<div id="resultPage-2" class="hidden">
<h1>Page 2</h1>
This page has a different formula:
$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}. \tag{2}\label{eq2}$$
And a second one:
$$\frac{x+1}{x-1}. \tag{2.1}\label{eq2.1}$$
We link to \eqref{eq1}.
</div>

<div id="resultPage-3" class="hidden">
<h1>Page 3</h1>
This one has yet another formula:
$$f(a) = \frac{1}{2\pi i} \oint\frac{f(z)}{z-a}dz. \tag{3}\label{eq3}$$
We link to \eqref{eq2}, and \eqref{eq3}.
</div>

<div class="spacer"></div>

</body>
</html>

The red bars above and below the math are just to make the page large enough to scroll so that you can see that the proper equation is being targeted. Note, however, that different browsers handle the scrolling to SVG positions slightly differently. I'm looking into improving the targeting so that it is more consistent, but that isn't yet ready. CommonHTML does better with that.

In any case, see if that gets you more like what you want.

@parhizkari
Copy link
Author

I cannot appreciate enough the time and energy you put in answering the questions. I just saw your answer, sounds so amazing. It would take me some times, though, before I can apply the code you provided into my app. Thank you very much.

@dpvc
Copy link
Member

dpvc commented Jan 14, 2021

No problem. It was an interesting project, and worth working out how it could be done, for future reference by others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Expected Behavior This is how MathJax works v3
Projects
None yet
Development

No branches or pull requests

2 participants