Skip to content

Commit

Permalink
feat: fixed #2052
Browse files Browse the repository at this point in the history
  • Loading branch information
arnog committed Aug 18, 2023
1 parent 5a1c363 commit d4d1cf2
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 68 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

### Improvements

- **#2052** When double-clicking then dragging, the selection is now extended
to the nearest boundary. This applies to math, text and LaTeX zones.
- Added `prompt` CSS part to the mathfield element. This allows styling of
prompts (placeholders) in a fill-in-the-blank mathfield.
- Added `w40` keycap class (4-wide)
Expand Down
18 changes: 9 additions & 9 deletions src/editor-mathfield/pointer-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,14 @@ export function onPointerDown(
const focus = offsetFromPoint(that, x, y, {
bias: x <= anchorX ? (x === anchorX ? 0 : -1) : +1,
});
if (trackingWords) {
// @todo: extend focus, actualAnchor to word boundary
}

if (actualAnchor >= 0 && focus >= 0) {
that.model.extendSelectionTo(actualAnchor, focus);
requestUpdate(mathfield);
}

if (trackingWords) selectGroup(that.model);

// Prevent synthetic mouseMove event when this is a touch event
evt.preventDefault();
evt.stopPropagation();
Expand Down Expand Up @@ -141,12 +140,6 @@ export function onPointerDown(
anchorY >= bounds.top &&
anchorY <= bounds.bottom
) {
// Focus the mathfield
if (!mathfield.hasFocus()) {
dirty = 'none'; // focus() will refresh
mathfield.focus({ preventScroll: true });
}

// Clicking or tapping the field resets the keystroke buffer
mathfield.flushInlineShortcutBuffer();
mathfield.adoptStyle = 'left';
Expand Down Expand Up @@ -219,6 +212,13 @@ export function onPointerDown(
}
}
}
// Focus the mathfield
// (do it after the selection has been set, since the
// logic on what to do on focus may depend on the selection)
if (!mathfield.hasFocus()) {
dirty = 'none'; // focus() will refresh
mathfield.focus({ preventScroll: true });
}
} else gLastTap = null;

mathfield.stopCoalescingUndo();
Expand Down
156 changes: 98 additions & 58 deletions src/editor-model/commands-select.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { register } from '../editor/commands';
import type { ModelPrivate } from './model-private';
import { LETTER_AND_DIGITS } from '../core-definitions/definitions-utils';
import { getMode } from './selection';
import { move, skip } from './commands';
import { range } from './selection-utils';

Expand All @@ -12,77 +11,118 @@ import { range } from './selection-utils';
* When the selection is in a text zone, the "group" is a word.
*/
export function selectGroup(model: ModelPrivate): boolean {
if (getMode(model, model.position) === 'text') {
let [start, end] = range(model.selection);
//
let done = false;
while (!done && start > 0) {
const atom = model.at(start);
if (atom.mode === 'text' && LETTER_AND_DIGITS.test(atom.value))
start -= 1;
else done = true;
}
let [start, end] = range(model.selection);
start = boundary(model, start, 'backward');
end = boundary(model, end, 'forward');

done = false;
while (!done && end <= model.lastOffset) {
const atom = model.at(end);
if (atom.mode === 'text' && LETTER_AND_DIGITS.test(atom.value)) end += 1;
else done = true;
}
model.setSelection(start, end);

return true;
}

if (done) end -= 1;
/** Extend the position to the next boundary */
function boundary(
model: ModelPrivate,
pos: number,
direction: 'forward' | 'backward'
): number {
let atom = model.at(pos);
if (!atom) return pos;

if (start >= end) {
// No word found. Select a single character
model.setSelection(end - 1, end);
return true;
const dir = direction === 'forward' ? 1 : -1;

//
// Text mode zone
//
if (atom.mode === 'text') {
while (atom) {
if (atom.mode !== 'text' || !LETTER_AND_DIGITS.test(atom.value)) break;
pos += dir;
atom = model.at(pos);
}
return direction === 'backward' ? pos - 1 : pos;
}

model.setSelection(start, end);
} else {
const atom = model.at(model.position);
// In a math zone, select all the sibling nodes
if (atom.isDigit()) {
// In a number, select all the digits
let [start, end] = range(model.selection);
//
while (model.at(start)?.isDigit()) start -= 1;
while (model.at(end)?.isDigit()) end += 1;
model.setSelection(start, end - 1);
} else {
if (atom.style.variant || atom.style.variantStyle) {
let [start, end] = range(model.selection);
let x = model.at(start)?.style;
//
// Latex mode zone
//
if (atom.mode === 'latex') {
if (/[a-zA-Z\*]/.test(atom.value)) {
// Possible command
if (direction === 'backward') {
// Look backward until we find a non-letter or a backslash
while (
x &&
x.variant === atom.style.variant &&
x.variantStyle === atom.style.variantStyle
atom &&
atom.mode === 'latex' &&
atom.value !== '\\' &&
/[a-zA-Z]/.test(atom.value)
) {
start -= 1;
x = model.at(start)?.style;
pos += dir;
atom = model.at(pos);
}

x = model.at(end)?.style;
while (
x &&
x.variant === atom.style.variant &&
x.variantStyle === atom.style.variantStyle
) {
end += 1;
x = model.at(end)?.style;
} else {
// Look backward until we find a non-letter or a star
while (atom && atom.mode === 'latex' && /[a-zA-Z\*]/.test(atom.value)) {
pos += dir;
atom = model.at(pos);
}
}
} else if (atom.value === '{') {
if (direction === 'forward') {
// Start of a group, select whole group
while (atom && atom.mode === 'latex' && atom.value !== '}') {
pos += dir;
atom = model.at(pos);
}
return pos;
}
return pos - 1;
} else if (atom.value === '}') {
if (direction === 'backward') {
while (atom && atom.mode === 'latex' && atom.value !== '{') {
pos += dir;
atom = model.at(pos);
}
return pos - 1;
}
return pos;
}

model.setSelection(start, end - 1);
} else {
model.setSelection(
model.offsetOf(atom.firstSibling),
model.offsetOf(atom.lastSibling)
);
return pos - 1;
}

//
// Math mode zone
//
if (atom.mode === 'math') {
//
// In a number, select all the digits
//
if (atom.isDigit()) {
while (model.at(pos + dir)?.isDigit()) pos += dir;
return direction === 'backward' ? pos - 1 : pos;
}

//
// In a styled area, select all the atoms with the same style
//
if (atom.style.variant || atom.style.variantStyle) {
let x = model.at(pos)?.style;
while (
x &&
x.variant === atom.style.variant &&
x.variantStyle === atom.style.variantStyle
) {
x = model.at(pos + dir)?.style;
pos += dir;
}
return direction === 'backward' ? pos - 1 : pos;
}

return pos;
}

return true;
return pos;
}

register(
Expand Down
32 changes: 31 additions & 1 deletion test/smoke/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,16 @@ <h1>Smoke Test</h1>
</ul>
</header>
<main>
<math-field id="mf">\operatorname*{arg~max}_{}</math-field>
<math-field id="mf"
><style>
.decor {
text-shadow: 0 0 2px cyan;
background: yellow;
}</style
>123+\operatorname*{arg~max}_{}+\frac{134}{x+3245}+
123+456+563=\sin(x)\class{decor}{\Alpha\Beta}\text{hello world, how are
you? Well, I hope!}</math-field
>
<!-- <math-field id="mf"
>\text{Hello
World}\int^{x+1}_{x-1}x^2+y^{1+a}_{b+2}+\frac{x+z}{y^{2+n}+2}+\left(a+3+\frac{2}{4}\right)+\sqrt[1+x]{\frac78}+\frac56</math-field
Expand Down Expand Up @@ -209,6 +218,22 @@ <h2>Latex to Speakable Text</h2>
{ capture: true }
);

document.addEventListener('focusin', (ev) => {
const mf = document.activeElement;
const showKeyboard =
mf?.tagName === 'MATH-FIELD' && mf.isSelectionEditable === true;
console.log(showKeyboard);
mathVirtualKeyboard.visible = showKeyboard;
});

mf.addEventListener('selection-change', (ev) => {
const mf = ev.target;
const showKeyboard =
mf?.tagName === 'MATH-FIELD' && mf.isSelectionEditable === true;
console.log(showKeyboard);
mathVirtualKeyboard.visible = showKeyboard;
});

mf.smartMode = false;

mf.locale = 'en-US';
Expand Down Expand Up @@ -256,6 +281,11 @@ <h2>Latex to Speakable Text</h2>
// },
// };

mf.macros = {
...mf.macros,
speed: '\\mathrm{speed}',
};

mf.addEventListener('input', (ev) => {
console.log('input event');
updateContent(mf);
Expand Down

0 comments on commit d4d1cf2

Please sign in to comment.