Skip to content

Commit 5266451

Browse files
Merge pull request #321 from desmosinc/mike/allow-set-selection
Allow setting selection
2 parents 21dcf2c + 765dcae commit 5266451

File tree

5 files changed

+507
-17
lines changed

5 files changed

+507
-17
lines changed

docs/Api_Methods.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,33 @@ If the cursor is before the plus this method would return:
173173
{
174174
latex: 'a+b',
175175
startIndex: 1,
176-
endIndex: 1
176+
endIndex: 1,
177+
opaqueSnapshot: {...}
177178
}
178179
```
179180

181+
You can pass the result of `.selection()` back into `.selection()` to restore a cursor / selection. This works by taking a snapshot of the selection you currently have and recording
182+
enough information to restore it within `opaqueSnapshot`. You should not peek inside of `opaqueSnapshot` or permanently store it. This is valid only for this version of MathQuill. This selection is also only valid if the MQ's latex is identical. The MQ can go through changes, but when you try to restore the selection the current latex must match the latex when the selection snapshot was created.
183+
184+
```js
185+
// this would work
186+
mq.latex('abc');
187+
mq.select();
188+
const selection = mq.selection(); // takes a snapshot of the selection
189+
mq.latex('123');
190+
mq.latex('abc');
191+
mq.selection(selection); // will restore the selection
192+
```
193+
194+
```js
195+
// this would not work
196+
mq.latex('abc');
197+
mq.select();
198+
const selection = mq.selection(); // takes a snapshot of the selection
199+
mq.latex('123');
200+
mq.selection(selection); // will restore the selection
201+
```
202+
180203
# Editable MathField methods
181204

182205
Editable math fields have all of the [above](#mathquill-base-methods) methods in addition to the ones listed here.

src/mathquill.d.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ declare namespace MathQuill {
1919
handlers?: HandlerOptions;
2020
};
2121

22+
type ExportedLatexSelection = {
23+
latex: string;
24+
startIndex: number;
25+
endIndex: number;
26+
opaqueSnapshot: {
27+
uncleanedLatex: string;
28+
cursorInsertPath: string;
29+
signedSelectionSize: number;
30+
};
31+
};
32+
2233
interface BaseMathQuill {
2334
id: number;
2435
data: { [key: string]: any };
@@ -29,12 +40,8 @@ declare namespace MathQuill {
2940
html: () => string;
3041
mathspeak: () => string;
3142
text(): string;
32-
selection(): {
33-
latex: string;
34-
startIndex: number;
35-
endIndex: number;
36-
};
37-
43+
selection(selection: ExportedLatexSelection): this;
44+
selection(): ExportedLatexSelection;
3845
//chainable methods
3946
config(opts: Config): this;
4047
latex(latex: string): this;

src/publicapi.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,14 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API {
338338
return this.__controller.exportLatex();
339339
}
340340

341-
selection() {
341+
selection(selection: ExportedLatexSelection): this;
342+
selection(): ExportedLatexSelection;
343+
selection(selection?: ExportedLatexSelection) {
344+
if (selection) {
345+
this.__controller.restoreLatexSelection(selection);
346+
return this;
347+
}
348+
342349
return this.__controller.exportLatexSelection();
343350
}
344351
html() {

src/services/latex.ts

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ class TempSingleCharNode extends MQNode {
44
}
55
}
66

7+
type ExportedLatexSelection = {
8+
latex: string;
9+
startIndex: number;
10+
endIndex: number;
11+
opaqueSnapshot: {
12+
uncleanedLatex: string;
13+
cursorInsertPath: string;
14+
signedSelectionSize: number;
15+
};
16+
};
17+
718
// Parser MathBlock
819
var latexMathParser = (function () {
920
function commandToBlock(cmd: MQNode | Fragment): MathBlock {
@@ -116,18 +127,140 @@ class Controller_latex extends Controller_keystroke {
116127

117128
return this;
118129
}
119-
exportLatexSelection() {
130+
131+
// this traces up from the node to the root by first going left as much as possible
132+
// then going up one parent. Then going left as much as possible then going up on parent.
133+
// It continues this pattern until it finds the root. The "path" that this algorithm
134+
// constructs is from the root back down to this node. So it will output the path in
135+
// reverse traversal order and will replace lefts with rights and ups with downs.
136+
private findPathFromRootToNode(node: MQNode | Cursor | Anticursor): string {
137+
let path = '';
138+
do {
139+
while (node[L]) {
140+
path = 'R' + path;
141+
node = node[L];
142+
}
143+
144+
if (node.parent) {
145+
node = node.parent;
146+
path = 'D' + path;
147+
} else {
148+
return path;
149+
}
150+
} while (true);
151+
}
152+
153+
private insertCursorAtPath(path: string): boolean {
154+
if (!path) return false;
155+
156+
let node: MQNode = this.root;
157+
158+
// We generate the path starting from the node working up to the root.
159+
// So we need to work backwards when following the path. The very last instruction
160+
// we encounter does not point to a node. It points to a space where a cursor could
161+
// be inserted: either just to the right of the current node ("R") or just to the
162+
// left of the current node's children ("D")
163+
const lastInstructionI = path.length - 1;
164+
for (let i = 0; i < lastInstructionI; i++) {
165+
const instruction = path[i];
166+
167+
if (instruction === 'D') {
168+
const end = node.children().getEnd(L);
169+
if (!end) return false;
170+
node = end;
171+
} else if (instruction === 'R') {
172+
const end = node[R];
173+
if (!end) return false;
174+
node = end;
175+
} else {
176+
return false;
177+
}
178+
}
179+
180+
const lastInstruction = path[lastInstructionI];
181+
if (lastInstruction === 'D') {
182+
this.cursor.clearSelection().endSelection();
183+
this.cursor.insAtLeftEnd(node);
184+
return true;
185+
} else if (lastInstruction === 'R') {
186+
this.cursor.clearSelection().endSelection();
187+
this.cursor.insRightOf(node);
188+
return true;
189+
} else {
190+
return false;
191+
}
192+
}
193+
194+
restoreLatexSelection(data: ExportedLatexSelection) {
195+
const currentUncleanedLatex =
196+
this.exportLatexSelection().opaqueSnapshot.uncleanedLatex;
197+
const { cursorInsertPath, signedSelectionSize, uncleanedLatex } =
198+
data.opaqueSnapshot;
199+
200+
// verify the uncleanedLatex are identical. We need the trees to be identical so that the
201+
// path instructions are relative to an identical tree structure
202+
if (currentUncleanedLatex !== uncleanedLatex) return;
203+
204+
if (!this.insertCursorAtPath(cursorInsertPath)) return;
205+
206+
if (signedSelectionSize) {
207+
this.withIncrementalSelection((selectDir) => {
208+
const dir = signedSelectionSize < 0 ? L : R;
209+
const count = Math.abs(signedSelectionSize);
210+
for (let i = 0; i < count; i += 1) {
211+
selectDir(dir);
212+
}
213+
});
214+
}
215+
}
216+
217+
// any time there's a selection there is a cursor and anticursor. The
218+
// anticursor is the anchor, and the cursor is the head. It should be
219+
// true that these are siblings. If you trace right or left far enough
220+
// you will reach the other one. This returns the direction and magnitude
221+
// of how many hops it takes to find the cursor from the anticursor. Otherwise
222+
// returns 0. The idea is to try this both with L and R and use the one, if any,
223+
// that comes back with a non-zero answer.
224+
private findSignedSelectionSizeInDir(dir: L | R) {
225+
const cursor = this.cursor;
226+
const anticursor = cursor.anticursor;
227+
if (!anticursor) return 0;
228+
229+
let count = 0;
230+
let node = anticursor[dir];
231+
while (node !== cursor[dir]) {
232+
if (!node) return 0;
233+
234+
count += dir;
235+
node = node[dir];
236+
}
237+
238+
return count;
239+
}
240+
241+
exportLatexSelection(): ExportedLatexSelection {
120242
var ctx: LatexContext = {
121243
latex: '',
122244
startIndex: -1,
123245
endIndex: -1
124246
};
125247

248+
let cursorInsertPath: string = '';
249+
let signedSelectionSize: number = 0;
250+
126251
var selection = this.cursor.selection;
127-
if (selection) {
252+
if (selection && this.cursor.anticursor) {
253+
cursorInsertPath = this.findPathFromRootToNode(this.cursor.anticursor);
254+
128255
ctx.startSelectionBefore = selection.getEnd(L);
129256
ctx.endSelectionAfter = selection.getEnd(R);
257+
258+
signedSelectionSize =
259+
this.findSignedSelectionSizeInDir(L) ||
260+
this.findSignedSelectionSizeInDir(R);
130261
} else {
262+
cursorInsertPath = this.findPathFromRootToNode(this.cursor);
263+
131264
var cursorL = this.cursor[L];
132265
if (cursorL) {
133266
ctx.startSelectionAfter = cursorL;
@@ -146,26 +279,26 @@ class Controller_latex extends Controller_keystroke {
146279
this.root.latexRecursive(ctx);
147280

148281
// need to clean the latex
149-
var originalLatex = ctx.latex;
150-
var cleanLatex = this.cleanLatex(originalLatex);
282+
var uncleanedLatex = ctx.latex;
283+
var cleanLatex = this.cleanLatex(uncleanedLatex);
151284
var startIndex = ctx.startIndex;
152285
var endIndex = ctx.endIndex;
153286

154287
// assumes that the cleaning process will only remove characters. We
155-
// run through the originalLatex and cleanLatex to find differences.
288+
// run through the uncleanedLatex and cleanLatex to find differences.
156289
// when we find differences we see how many characters are dropped until
157290
// we sync back up. While detecting missing characters we decrement the
158291
// startIndex and endIndex if appropriate.
159292
var j = 0;
160293
for (var i = 0; i < ctx.endIndex; i++) {
161-
if (originalLatex[i] !== cleanLatex[j]) {
294+
if (uncleanedLatex[i] !== cleanLatex[j]) {
162295
if (i < ctx.startIndex) {
163296
startIndex -= 1;
164297
}
165298
endIndex -= 1;
166299

167300
// do not increment j. We wan to keep looking at this same
168-
// cleanLatex character until we find it in the originalLatex
301+
// cleanLatex character until we find it in the uncleanedLatex
169302
} else {
170303
j += 1; //move to next cleanLatex character
171304
}
@@ -174,7 +307,12 @@ class Controller_latex extends Controller_keystroke {
174307
return {
175308
latex: cleanLatex,
176309
startIndex: startIndex,
177-
endIndex: endIndex
310+
endIndex: endIndex,
311+
opaqueSnapshot: {
312+
uncleanedLatex,
313+
cursorInsertPath,
314+
signedSelectionSize
315+
}
178316
};
179317
}
180318

0 commit comments

Comments
 (0)