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

Alternative approach to "fill in the blank" #1838

Closed
WildYellowfin opened this issue Feb 17, 2023 · 15 comments
Closed

Alternative approach to "fill in the blank" #1838

WildYellowfin opened this issue Feb 17, 2023 · 15 comments

Comments

@WildYellowfin
Copy link
Collaborator

I'd like to suggest an alternative approach to the "fill in the blank" feature. The current placeholder solution uses hacky getBoundingClientRect() stuff that doesn't seem stable. I also don't think putting nested math fields inside a read-only is a solid approach.

Instead I would make a new \prompt[id?][style?] command and a prompt mode. In prompt mode, the typical focus UI would be disabled, and selection would be controlled to give the illusion that the non-prompt parts are read only.

\prompts would basically be boxes, and inherit border styling from the math field. They would also inherit the :focus-within styles.

Prompts can be controlled by RegExing the global latex (I feel like there's a better solution, but don't know the library well).

I'm fairly new here, so I want to know how feasible this is. Given a few pointers I'd be happy to build this.

@arnog
Copy link
Owner

arnog commented Feb 17, 2023

Thanks for your interest and sharing this idea.

That's something interesting and worth thinking about a bit more.

Could you expand a bit on what issues you see with the current implementation of \placeholder{}? What problems would your approach solve?

It seems that you're arguing for a different implementation of \placeholder{} rather than a new command. I'm not sure how you would explain to a user of a math-field whether to use \placeholder{} or \prompt{}. Seems they would do the same thing but with a different implementation?

I don't think there's anything unsolvable, but there are some cases that are worth thinking about in advance. I would put them in three categories:

  • keyboard input: with this approach, the selection of a mathfield could span a read-only portion and an editable portion. For that matter the selection could start in a read-only portion, completely include an editable portion, span some more read-only portion, and end in the middle of an editable portion. In that case, what happens if the delete key is pressed? Or another key, for that matter. What happens when the right arrow key is pressed at the end of an editable portion? What does command-A do?
  • pointer input: you can drag a pointer to select a portion of mathfield. What happens when your cross an editable and non-editable portion? If you are currently in an editable portion, what happens if you click on a non-editable one?
  • API behavior: what would mf.value be when including a read-only and editable portion? Can you query the value of the portion that's editable? Can you get notified (event) when an editable portion is modified? What happens if you set the value of the mathfield, or perform an mf.insert()?

Prompts can be controlled by RegExing the global latex (I feel like there's a better solution, but don't know the library well).

I'm not sure what you mean by that.

Note that while being edited, an expression is represented as Atom objects. These objects are manipulated during edition (they are added, removed, modified as necessary). From these Atom objects, a LaTeX (or MathML, or...) value is serialized. There is no LaTeX expression that represents the expression while it is being edited.

I'm happy to answer any questions you may have if you want to explore this further.

@WildYellowfin
Copy link
Collaborator Author

If I understand correctly, the current implementation of \placeholder{} renders a MathfieldBox with dimensions equal to the editable mathfield. Speaking of, you can get rid of the height here, and instead put the \u200b 0 width space. Right now the height is too large, causing all sorts of alignment issues. This more or less fixes the current vertical alignment issue, but it still has the following issue:
image
The nested math fields are not aware that they are inside a fraction, and therefore the font size is incorrect. Despite my efforts I have not been able to align these up correctly. As far as I can tell, the fraction doesn't care about the height of the MathfieldBox span, since it's nested multiple layers within.

There is also a horizontal bug on first load that I can't figure out. Currently, a mathfield is delicately positioned on top of the box described above. What's strange is that the positioning logic seems to be running twice on load, and both of these give "incorrect" positions. On click anywhere in the broader math field, it runs again correctly.

I stepped through the positioning to see if maybe the underlying MathfieldBox was out of place while the DOM was loading, but is positioned correctly.

More broadly, I think that keeping this correspondence between the position of the box and the position of the math field is tricky.

As far as the points you raised, I would propose:

  • If the selection is not entirely within one of the editable regions, then insertion is disallowed
  • Arrow keys move between editable fields, and trigger move out events if moving out of first / last editable region
  • Pointer selection should just select as it normally would. The "fields" are really just styling in this model.
  • A click outside an editable region would blur the mathfield.
  • mf.value should return the entire value, including read only portions. mf.insert() should behave exactly as it usually does, and setting the value should overwrite all contents.
  • Seperate APIs could be made for querying and setting specific editable regions
    • Would also be nice to be able to set styles of individual regions (my project grades student answers, and I'd like to put a green border on correct regions etc.)

I brought up RegEx because I tried to hack this solution using the \enclose tag, but quickly discovered you could not navigate inside an \enclose anyways.

Maybe we can think about this more once you've had a chance to fix the current placeholder bug? Also, if you have any leads on the remaining alignment issues I can take another stab at it.

@WildYellowfin
Copy link
Collaborator Author

@arnog I went ahead and built a prototype of this. Basically I:

  • Added a promptMode option, and added it to GlobalContext
  • Made a prompt atom
    • Rounded box with focus ring in prompt mode
    • Greyed out box when not in prompt mode
  • I added handling for keyboard events:
    • move between prompts using arrow keys, including up/down in matrices
    • move-out event when no prompts in a direction
  • Block any insertions if selection is not contained to a prompt
  • Cursor can only be in prompts or at offset 0, in which case it is hidden with CSS
    • Had to add a ML__prompting class to the struts to do this

promptMode:

Screen.Recording.2023-02-20.at.16.24.04.mov
Screen.Recording.2023-02-20.at.16.20.58.mov

Things still not working / todo:

  • Copy/paste behaves strangely, as does history
    • I wonder if there's a data type I haven't encode correctly, the JSON and ASCII look wrong, while I made the mathML use the tag since it looks similar.
  • promptMode and readOnly should be exclusive
  • \prompt is not a real tex command, I could either move this feature under enclose, or add it under placeholder

History bug:

Screen.Recording.2023-02-20.at.16.23.22.mov

https://github.com/WildYellowfin/mathlive/tree/prompt

@arnog
Copy link
Owner

arnog commented Feb 21, 2023

That's looking good!

The issue with copy/paste and undo buffer would seem to indicate a problem with the JSON serialization. I had a quick look at your code, but I didn't see anything obviously wrong.

If this has the same functionality as \placeholder{}, I would rather replace \placeholder{} with this implementation. Looks like you don't support optional IDs for the prompts yet, though.

I don't know if promptMode is necessary. Perhaps the readonly mode can be used instead.

@WildYellowfin
Copy link
Collaborator Author

Yeah, I was intending to eventually merge this into the placeholder, but since this was my first time getting stuck into the source I wanted to build a prototype separately first - just to make sure the approach was sound.

I just had a look to figure out how readOnly works. If I understand correctly, the model doesn't change whatsoever, its just keyboard handlers and hiding the caret? If so I think I can scarp promptMode, and frankly the way I was hiding the caret outside of prompts is way sloppier than this.

I'll try and push something by Thursday, and take another look at the JSON + maybe make a few tests.

@arnog
Copy link
Owner

arnog commented Feb 22, 2023

@WildYellowfin yes, readOnly doesn't affect the model, only the interaction and rendering.

@WildYellowfin
Copy link
Collaborator Author

@arnog just following up for help with one of the last remaining bugs I'm having. As I described above, undo / redo destroys the prompts, as does copy and paste. I've also discovered that any inline shortcuts will also break a prompt. I tried to see if this behavior generalized, and found that it also affects \enclose atoms. I suspect it applies to any atom with an [argument] that does not put the argument in a branch (which is why surds are fine - the index has a branch).

I've traced the issue to here. This 'cursor.parent' does not have a placeholderId when it should.

This does not affect the bbox atom, and I am not sure why. Any help here would be appreciated. Also, should I make this a separate issue?

@arnog
Copy link
Owner

arnog commented Feb 24, 2023

Problems with both undo/redo and copy/paste point to an issue with the JSON serialization (which is used by both mechanism). This mechanism is also used by inline shortcut resolution.

When serializing to JSON, the content of any branch is automatically included by default in the serialization (see AtomClass.toJson()). Custom properties of atoms should be manually serialized in their respective toJson() function (and deserialized in their corresponding fromJson().

I suspect that placeholderId might not included in the serialization/deserialization of PromptAtom.

@WildYellowfin
Copy link
Collaborator Author

This is also happening to the EncloseAtom. Try \enclose{roundedbox}{x} in any mathfield, type something and then undo it.

@WildYellowfin
Copy link
Collaborator Author

Fixed! I didn't realize there was a global fromJson function I needed to add my atom to... my bad. Still can't explain the enclose behavior, should I open a separate issue?

@arnog
Copy link
Owner

arnog commented Feb 24, 2023

Oh, yes, sorry, I had forgotten that, but glad you solved it!

Yeah, open an issue for the enclose atom and I'll have a look at it.

@WildYellowfin
Copy link
Collaborator Author

No worries! I found the enclose issue also, in enclose.ts, all notations were serializing to downdiagonalstrike in some bizarre set of if statements. My fork has fixed it.

@WildYellowfin
Copy link
Collaborator Author

Hi @arnog. I've added an optional [correctness] argument to the prompt atom, with the idea of displaying correctness to a student and preventing further edits. So far I have just added classes with variable colors, but I'm now thinking about accessibility. Would it be difficult to post-fix correct / incorrect atoms with a rendered check / cross?

I don't really know how the actual drawing works in mathlive, so I wanted to check.

@arnog
Copy link
Owner

arnog commented Feb 24, 2023

@WildYellowfin for more complex rendering (i.e. one that cannot be handled with CSS) you would want to extend the render() function of the Atom class. The purpose of the render function is to generate Box objects that correspond to the rendering of the Atom. A Box eventually gets maps to HTML markup. It includes an optional text body, some CSS style, but it can also include some SVG which might work for what you're looking for. Boxes can be nested, so you would probably create a Box for the "main" content, and another for the "annotation" (i.e. the cross or check) in the form of a SVG element, then compose them by enclosing them in a wrapping box. Have a look at the render() function of the EncloseAtom for some inspiration.

@arnog
Copy link
Owner

arnog commented Mar 1, 2023

This has now been merged into the main branch.

Thanks for your contribution!

@arnog arnog closed this as completed Mar 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants