@pie-lib/math-input-mathlive 1.1.5-beta.10 → 1.1.5-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/mf/input.js CHANGED
@@ -19,6 +19,7 @@ var _styles = require("@mui/material/styles");
19
19
  var _debug = _interopRequireDefault(require("debug"));
20
20
  var _mathliveInstance = require("../mathlive-instance");
21
21
  var _latexBridge = require("../latex-bridge");
22
+ var _selection = require("../selection");
22
23
  function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2["default"])(o), (0, _possibleConstructorReturn2["default"])(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2["default"])(t).constructor) : o.apply(t, e)); }
23
24
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
24
25
  var log = (0, _debug["default"])('pie-lib:math-input-mathlive:input');
@@ -126,6 +127,9 @@ var Input = exports.Input = /*#__PURE__*/function (_React$Component) {
126
127
 
127
128
  // A selected `\placeholder{}` gets no MathLive caret; draw one.
128
129
  this.untrackCaret = (0, _mathliveInstance.trackPlaceholderCaret)(this.mathField);
130
+
131
+ // Shift+Arrow would otherwise select half a fraction; snap to whole atoms.
132
+ this.untrackSelection = (0, _selection.trackWholeAtomSelection)(this.mathField);
129
133
  this.mathField.addEventListener('input', this.onInputEdit);
130
134
 
131
135
  // Belt and braces: drop anything a previous mount may have left behind.
@@ -177,6 +181,10 @@ var Input = exports.Input = /*#__PURE__*/function (_React$Component) {
177
181
  this.untrackCaret();
178
182
  this.untrackCaret = undefined;
179
183
  }
184
+ if (this.untrackSelection) {
185
+ this.untrackSelection();
186
+ this.untrackSelection = undefined;
187
+ }
180
188
  if (this.mathField) {
181
189
  this.mathField.removeEventListener('input', this.onInputEdit);
182
190
  this.mathField.remove();
@@ -1 +1 @@
1
- {"version":3,"file":"input.js","names":["_react","_interopRequireDefault","require","_propTypes","_styles","_debug","_mathliveInstance","_latexBridge","_callSuper","t","o","e","_getPrototypeOf2","_possibleConstructorReturn2","_isNativeReflectConstruct","Reflect","construct","constructor","apply","Boolean","prototype","valueOf","call","log","debug","Holder","styled","display","width","Input","exports","_React$Component","props","_this","_classCallCheck2","_defineProperty2","onChange","mathField","latex","fromMathLive","getValue","lastEmitted","event","key","preventDefault","holderRef","React","createRef","undefined","_inherits2","_createClass2","value","_componentDidMount","_asyncToGenerator2","_regenerator","mark","_callee","generation","ml","wrap","_context","prev","next","mountGeneration","loadMathLive","sent","current","abrupt","MathfieldElement","macros","getMacros","toMathLive","readOnly","mathVirtualKeyboardPolicy","mathModeSpace","MATH_MODE_SPACE","smartSuperscript","applyShadowStyles","untrackCaret","trackPlaceholderCaret","addEventListener","onInputEdit","replaceChildren","innerRef","stop","componentDidMount","arguments","componentDidUpdate","prevProps","_this$props","componentWillUnmount","removeEventListener","remove","get","_this2","v","setLatex","el","focus","blur","clear","command","action","keyToAction","insert","keystroke","selector","KEYSTROKES","executeCommand","message","write","_this3","values","Array","isArray","forEach","vv","render","_this$props2","className","onFocus","onBlur","onClick","createElement","onKeyDown","ref","Component","PropTypes","string","func","bool","_default"],"sources":["../../src/mf/input.jsx"],"sourcesContent":["import React from 'react';\nimport PropTypes from 'prop-types';\nimport { styled } from '@mui/material/styles';\nimport debug from 'debug';\nimport {\n loadMathLive,\n getMacros,\n MATH_MODE_SPACE,\n applyShadowStyles,\n trackPlaceholderCaret,\n} from '../mathlive-instance';\nimport { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';\n\nconst log = debug('pie-lib:math-input-mathlive:input');\n\nconst Holder = styled('span')({\n display: 'inline-block',\n '& math-field': {\n display: 'block',\n width: '100%',\n },\n // MathLive ships its own virtual keyboard toggle; pie supplies its own keypad.\n '& math-field::part(virtual-keyboard-toggle)': {\n display: 'none',\n },\n '& math-field::part(menu-toggle)': {\n display: 'none',\n },\n});\n\n/**\n * Editable math field. Drop-in replacement for @pie-lib/math-input's `mq.Input`.\n *\n * Keeps the same imperative surface used by MathInput's keypad\n * (`write`, `command`, `keystroke`, `clear`, `focus`, `blur`) so callers do not\n * change, and the same `latex` / `onChange` props.\n */\nexport class Input extends React.Component {\n static propTypes = {\n className: PropTypes.string,\n latex: PropTypes.string,\n onChange: PropTypes.func,\n onClick: PropTypes.func,\n onFocus: PropTypes.func,\n onBlur: PropTypes.func,\n readOnly: PropTypes.bool,\n innerRef: PropTypes.func,\n };\n\n constructor(props) {\n super(props);\n this.holderRef = React.createRef();\n // Tracks the last latex we emitted, so prop echoes don't reset the cursor.\n this.lastEmitted = undefined;\n }\n\n async componentDidMount() {\n // MathLive loads asynchronously, so this method resumes after an await and\n // has to know whether it is still the current mount. React StrictMode calls\n // mount -> unmount -> mount on the SAME instance in dev, so a boolean\n // \"unmounted\" flag would latch and block the remount from ever creating a\n // field. A generation counter identifies the live mount instead: only the\n // newest one proceeds, and a real unmount invalidates all pending ones.\n const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);\n const ml = await loadMathLive();\n\n if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {\n return;\n }\n\n this.mathField = new ml.MathfieldElement({ macros: getMacros() });\n this.mathField.value = toMathLive(this.props.latex || '');\n\n if (this.props.readOnly) {\n this.mathField.readOnly = true;\n }\n\n // pie drives input through its own keypad, so suppress MathLive's.\n this.mathField.mathVirtualKeyboardPolicy = 'manual';\n // Without this the spacebar does nothing: MathLive's mathModeSpace\n // defaults to an empty string.\n this.mathField.mathModeSpace = MATH_MODE_SPACE;\n // After the exponent key the cursor must stay in the exponent until it is moved\n // MathLive's `smartSuperscript` (on by\n // default) jumps out of an empty superscript after a single digit, which breaks\n // typing x^23 or 10^6 and is inconsistent with non-digit exponents.\n this.mathField.smartSuperscript = false;\n\n // The mathfield renders in shadow DOM, so page CSS cannot reach it - the\n // stretchy-accent fix has to be injected there directly.\n applyShadowStyles(this.mathField);\n\n // A selected `\\placeholder{}` gets no MathLive caret; draw one.\n this.untrackCaret = trackPlaceholderCaret(this.mathField);\n\n this.mathField.addEventListener('input', this.onInputEdit);\n\n // Belt and braces: drop anything a previous mount may have left behind.\n this.holderRef.current.replaceChildren(this.mathField);\n\n if (this.props.innerRef) {\n this.props.innerRef(this);\n }\n }\n\n componentDidUpdate(prevProps) {\n if (!this.mathField) {\n return;\n }\n\n const { latex, readOnly } = this.props;\n\n if (readOnly !== prevProps.readOnly) {\n this.mathField.readOnly = !!readOnly;\n }\n\n // Only push the prop back into the field when it genuinely differs from\n // what the field already holds. This is the analogue of the old\n // shouldComponentUpdate latex-equality guard and prevents cursor thrash.\n if (latex !== undefined && latex !== null && latex !== this.lastEmitted) {\n const next = toMathLive(latex);\n\n if (next !== this.mathField.value) {\n this.mathField.value = next;\n }\n }\n }\n\n componentWillUnmount() {\n // Invalidate any mount still awaiting the MathLive load, so it does not\n // append a field into a holder that is going away.\n this.mountGeneration = (this.mountGeneration || 0) + 1;\n\n if (this.untrackCaret) {\n this.untrackCaret();\n this.untrackCaret = undefined;\n }\n\n if (this.mathField) {\n this.mathField.removeEventListener('input', this.onInputEdit);\n this.mathField.remove();\n this.mathField = undefined;\n }\n\n if (this.props.innerRef) {\n this.props.innerRef(null);\n }\n }\n\n onInputEdit = () => {\n const { onChange } = this.props;\n\n if (!this.mathField) {\n return;\n }\n\n const latex = fromMathLive(this.mathField.getValue('latex'));\n\n this.lastEmitted = latex;\n\n if (onChange) {\n onChange(latex);\n }\n };\n\n /** Current latex, in stored (MathQuill-compatible) form. */\n latex() {\n return this.mathField ? fromMathLive(this.mathField.getValue('latex')) : '';\n }\n\n /**\n * The underlying `<math-field>` element, or undefined until MathLive has\n * finished loading. Exposed for callers that need the real element.\n *\n * NOTE: this is NOT a MathQuill field. Code written against\n * `@pie-lib/math-input` often did `input.mathField.latex()`; that shape is\n * provided by `mqCompat` below, and `latex()` on this component is the\n * supported replacement.\n */\n get element() {\n return this.mathField;\n }\n\n /**\n * MathQuill-shaped facade, so existing `input.mathField.latex()` /\n * `.el()` call sites keep working instead of throwing. Only the members that\n * consumers actually used are provided; MathQuill internals such as\n * `__controller` have no MathLive equivalent and are deliberately absent.\n */\n get mqCompat() {\n return {\n latex: (v) => (v === undefined ? this.latex() : (this.setLatex(v), v)),\n el: () => this.mathField,\n focus: () => this.focus(),\n blur: () => this.blur(),\n };\n }\n\n /** Programmatically replace the content. */\n setLatex(v) {\n if (this.mathField) {\n this.mathField.value = toMathLive(v || '');\n }\n }\n\n clear() {\n if (this.mathField) {\n this.mathField.value = '';\n }\n\n return '';\n }\n\n blur() {\n log('blur mathfield');\n this.mathField && this.mathField.blur();\n }\n\n focus() {\n log('focus mathfield...');\n this.mathField && this.mathField.focus();\n }\n\n /**\n * MathQuill `cmd()` equivalent.\n *\n * Callers pass MathQuill command names (`'\\\\frac'`, `'\\\\sqrt'`, `'^'`, or an\n * array of them). Those must be expanded into latex with `#?` placeholders,\n * otherwise inserting a bare `\\frac` produces no editable slots.\n */\n command(v) {\n log('command: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const action = keyToAction({ command: v });\n\n if (action) {\n this.mathField.insert(action.value, { focus: true });\n }\n\n this.mathField.focus();\n\n return this.latex();\n }\n\n /**\n * MathQuill `keystroke()` equivalent.\n *\n * Accepts MathQuill keystroke names (`'Left'`, `'Right'`, `'Backspace'`) as\n * the keypad emits them, and passes anything else through as a MathLive\n * selector.\n */\n keystroke(v) {\n log('keystroke: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const selector = KEYSTROKES[v] || v;\n\n try {\n this.mathField.executeCommand(selector);\n } catch (e) {\n log('unknown keystroke/selector \"%s\": %s', v, e && e.message);\n }\n\n this.mathField.focus();\n\n return this.latex();\n }\n\n /** MathQuill `write()` equivalent. Accepts an array, as `cmd()` did. */\n write(v) {\n log('write: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const values = Array.isArray(v) ? v : [v];\n\n values.forEach((vv) => this.mathField.insert(vv, { focus: true }));\n this.mathField.focus();\n\n return this.latex();\n }\n\n /**\n * Enter must not insert a line break: math input is single-expression, and\n * the MathQuill version swallowed charCode 13 for the same reason. Newlines\n * are authored explicitly through the newLine embed, not the Enter key.\n */\n onKeyDown = (event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n }\n };\n\n render() {\n const { className, onFocus, onBlur, onClick } = this.props;\n\n return (\n <Holder\n className={className}\n onFocus={onFocus}\n onBlur={onBlur}\n onClick={onClick}\n onKeyDown={this.onKeyDown}\n ref={this.holderRef}\n />\n );\n }\n}\n\nexport default Input;\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"input.js","names":["_react","_interopRequireDefault","require","_propTypes","_styles","_debug","_mathliveInstance","_latexBridge","_selection","_callSuper","t","o","e","_getPrototypeOf2","_possibleConstructorReturn2","_isNativeReflectConstruct","Reflect","construct","constructor","apply","Boolean","prototype","valueOf","call","log","debug","Holder","styled","display","width","Input","exports","_React$Component","props","_this","_classCallCheck2","_defineProperty2","onChange","mathField","latex","fromMathLive","getValue","lastEmitted","event","key","preventDefault","holderRef","React","createRef","undefined","_inherits2","_createClass2","value","_componentDidMount","_asyncToGenerator2","_regenerator","mark","_callee","generation","ml","wrap","_context","prev","next","mountGeneration","loadMathLive","sent","current","abrupt","MathfieldElement","macros","getMacros","toMathLive","readOnly","mathVirtualKeyboardPolicy","mathModeSpace","MATH_MODE_SPACE","smartSuperscript","applyShadowStyles","untrackCaret","trackPlaceholderCaret","untrackSelection","trackWholeAtomSelection","addEventListener","onInputEdit","replaceChildren","innerRef","stop","componentDidMount","arguments","componentDidUpdate","prevProps","_this$props","componentWillUnmount","removeEventListener","remove","get","_this2","v","setLatex","el","focus","blur","clear","command","action","keyToAction","insert","keystroke","selector","KEYSTROKES","executeCommand","message","write","_this3","values","Array","isArray","forEach","vv","render","_this$props2","className","onFocus","onBlur","onClick","createElement","onKeyDown","ref","Component","PropTypes","string","func","bool","_default"],"sources":["../../src/mf/input.jsx"],"sourcesContent":["import React from 'react';\nimport PropTypes from 'prop-types';\nimport { styled } from '@mui/material/styles';\nimport debug from 'debug';\nimport {\n loadMathLive,\n getMacros,\n MATH_MODE_SPACE,\n applyShadowStyles,\n trackPlaceholderCaret,\n} from '../mathlive-instance';\nimport { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';\nimport { trackWholeAtomSelection } from '../selection';\n\nconst log = debug('pie-lib:math-input-mathlive:input');\n\nconst Holder = styled('span')({\n display: 'inline-block',\n '& math-field': {\n display: 'block',\n width: '100%',\n },\n // MathLive ships its own virtual keyboard toggle; pie supplies its own keypad.\n '& math-field::part(virtual-keyboard-toggle)': {\n display: 'none',\n },\n '& math-field::part(menu-toggle)': {\n display: 'none',\n },\n});\n\n/**\n * Editable math field. Drop-in replacement for @pie-lib/math-input's `mq.Input`.\n *\n * Keeps the same imperative surface used by MathInput's keypad\n * (`write`, `command`, `keystroke`, `clear`, `focus`, `blur`) so callers do not\n * change, and the same `latex` / `onChange` props.\n */\nexport class Input extends React.Component {\n static propTypes = {\n className: PropTypes.string,\n latex: PropTypes.string,\n onChange: PropTypes.func,\n onClick: PropTypes.func,\n onFocus: PropTypes.func,\n onBlur: PropTypes.func,\n readOnly: PropTypes.bool,\n innerRef: PropTypes.func,\n };\n\n constructor(props) {\n super(props);\n this.holderRef = React.createRef();\n // Tracks the last latex we emitted, so prop echoes don't reset the cursor.\n this.lastEmitted = undefined;\n }\n\n async componentDidMount() {\n // MathLive loads asynchronously, so this method resumes after an await and\n // has to know whether it is still the current mount. React StrictMode calls\n // mount -> unmount -> mount on the SAME instance in dev, so a boolean\n // \"unmounted\" flag would latch and block the remount from ever creating a\n // field. A generation counter identifies the live mount instead: only the\n // newest one proceeds, and a real unmount invalidates all pending ones.\n const generation = (this.mountGeneration = (this.mountGeneration || 0) + 1);\n const ml = await loadMathLive();\n\n if (generation !== this.mountGeneration || !ml || !this.holderRef.current || this.mathField) {\n return;\n }\n\n this.mathField = new ml.MathfieldElement({ macros: getMacros() });\n this.mathField.value = toMathLive(this.props.latex || '');\n\n if (this.props.readOnly) {\n this.mathField.readOnly = true;\n }\n\n // pie drives input through its own keypad, so suppress MathLive's.\n this.mathField.mathVirtualKeyboardPolicy = 'manual';\n // Without this the spacebar does nothing: MathLive's mathModeSpace\n // defaults to an empty string.\n this.mathField.mathModeSpace = MATH_MODE_SPACE;\n // After the exponent key the cursor must stay in the exponent until it is moved\n // MathLive's `smartSuperscript` (on by\n // default) jumps out of an empty superscript after a single digit, which breaks\n // typing x^23 or 10^6 and is inconsistent with non-digit exponents.\n this.mathField.smartSuperscript = false;\n\n // The mathfield renders in shadow DOM, so page CSS cannot reach it - the\n // stretchy-accent fix has to be injected there directly.\n applyShadowStyles(this.mathField);\n\n // A selected `\\placeholder{}` gets no MathLive caret; draw one.\n this.untrackCaret = trackPlaceholderCaret(this.mathField);\n\n // Shift+Arrow would otherwise select half a fraction; snap to whole atoms.\n this.untrackSelection = trackWholeAtomSelection(this.mathField);\n\n this.mathField.addEventListener('input', this.onInputEdit);\n\n // Belt and braces: drop anything a previous mount may have left behind.\n this.holderRef.current.replaceChildren(this.mathField);\n\n if (this.props.innerRef) {\n this.props.innerRef(this);\n }\n }\n\n componentDidUpdate(prevProps) {\n if (!this.mathField) {\n return;\n }\n\n const { latex, readOnly } = this.props;\n\n if (readOnly !== prevProps.readOnly) {\n this.mathField.readOnly = !!readOnly;\n }\n\n // Only push the prop back into the field when it genuinely differs from\n // what the field already holds. This is the analogue of the old\n // shouldComponentUpdate latex-equality guard and prevents cursor thrash.\n if (latex !== undefined && latex !== null && latex !== this.lastEmitted) {\n const next = toMathLive(latex);\n\n if (next !== this.mathField.value) {\n this.mathField.value = next;\n }\n }\n }\n\n componentWillUnmount() {\n // Invalidate any mount still awaiting the MathLive load, so it does not\n // append a field into a holder that is going away.\n this.mountGeneration = (this.mountGeneration || 0) + 1;\n\n if (this.untrackCaret) {\n this.untrackCaret();\n this.untrackCaret = undefined;\n }\n\n if (this.untrackSelection) {\n this.untrackSelection();\n this.untrackSelection = undefined;\n }\n\n if (this.mathField) {\n this.mathField.removeEventListener('input', this.onInputEdit);\n this.mathField.remove();\n this.mathField = undefined;\n }\n\n if (this.props.innerRef) {\n this.props.innerRef(null);\n }\n }\n\n onInputEdit = () => {\n const { onChange } = this.props;\n\n if (!this.mathField) {\n return;\n }\n\n const latex = fromMathLive(this.mathField.getValue('latex'));\n\n this.lastEmitted = latex;\n\n if (onChange) {\n onChange(latex);\n }\n };\n\n /** Current latex, in stored (MathQuill-compatible) form. */\n latex() {\n return this.mathField ? fromMathLive(this.mathField.getValue('latex')) : '';\n }\n\n /**\n * The underlying `<math-field>` element, or undefined until MathLive has\n * finished loading. Exposed for callers that need the real element.\n *\n * NOTE: this is NOT a MathQuill field. Code written against\n * `@pie-lib/math-input` often did `input.mathField.latex()`; that shape is\n * provided by `mqCompat` below, and `latex()` on this component is the\n * supported replacement.\n */\n get element() {\n return this.mathField;\n }\n\n /**\n * MathQuill-shaped facade, so existing `input.mathField.latex()` /\n * `.el()` call sites keep working instead of throwing. Only the members that\n * consumers actually used are provided; MathQuill internals such as\n * `__controller` have no MathLive equivalent and are deliberately absent.\n */\n get mqCompat() {\n return {\n latex: (v) => (v === undefined ? this.latex() : (this.setLatex(v), v)),\n el: () => this.mathField,\n focus: () => this.focus(),\n blur: () => this.blur(),\n };\n }\n\n /** Programmatically replace the content. */\n setLatex(v) {\n if (this.mathField) {\n this.mathField.value = toMathLive(v || '');\n }\n }\n\n clear() {\n if (this.mathField) {\n this.mathField.value = '';\n }\n\n return '';\n }\n\n blur() {\n log('blur mathfield');\n this.mathField && this.mathField.blur();\n }\n\n focus() {\n log('focus mathfield...');\n this.mathField && this.mathField.focus();\n }\n\n /**\n * MathQuill `cmd()` equivalent.\n *\n * Callers pass MathQuill command names (`'\\\\frac'`, `'\\\\sqrt'`, `'^'`, or an\n * array of them). Those must be expanded into latex with `#?` placeholders,\n * otherwise inserting a bare `\\frac` produces no editable slots.\n */\n command(v) {\n log('command: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const action = keyToAction({ command: v });\n\n if (action) {\n this.mathField.insert(action.value, { focus: true });\n }\n\n this.mathField.focus();\n\n return this.latex();\n }\n\n /**\n * MathQuill `keystroke()` equivalent.\n *\n * Accepts MathQuill keystroke names (`'Left'`, `'Right'`, `'Backspace'`) as\n * the keypad emits them, and passes anything else through as a MathLive\n * selector.\n */\n keystroke(v) {\n log('keystroke: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const selector = KEYSTROKES[v] || v;\n\n try {\n this.mathField.executeCommand(selector);\n } catch (e) {\n log('unknown keystroke/selector \"%s\": %s', v, e && e.message);\n }\n\n this.mathField.focus();\n\n return this.latex();\n }\n\n /** MathQuill `write()` equivalent. Accepts an array, as `cmd()` did. */\n write(v) {\n log('write: ', v);\n\n if (!this.mathField) {\n return '';\n }\n\n const values = Array.isArray(v) ? v : [v];\n\n values.forEach((vv) => this.mathField.insert(vv, { focus: true }));\n this.mathField.focus();\n\n return this.latex();\n }\n\n /**\n * Enter must not insert a line break: math input is single-expression, and\n * the MathQuill version swallowed charCode 13 for the same reason. Newlines\n * are authored explicitly through the newLine embed, not the Enter key.\n */\n onKeyDown = (event) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n }\n };\n\n render() {\n const { className, onFocus, onBlur, onClick } = this.props;\n\n return (\n <Holder\n className={className}\n onFocus={onFocus}\n onBlur={onBlur}\n onClick={onClick}\n onKeyDown={this.onKeyDown}\n ref={this.holderRef}\n />\n );\n }\n}\n\nexport default Input;\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.trackWholeAtomSelection = exports.expandRange = void 0;
8
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
+ var _debug = _interopRequireDefault(require("debug"));
10
+ var log = (0, _debug["default"])('pie-lib:math-input-mathlive:selection');
11
+
12
+ /**
13
+ * Widen a selection so it covers whole atoms of a single parent.
14
+ *
15
+ * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into
16
+ * a fraction instead of over it: from before `2x` in `5x^2+2x+\frac{\pi}{2}-4`,
17
+ * five presses select `2x+` plus the `\pi` of the numerator, and copying gives
18
+ * `2x+\pi`. MathQuill treated a fraction (root, superscript...) as one unit,
19
+ * which is what authors expect.
20
+ *
21
+ * The tree is recovered from MathLive's public `getElementInfo(offset)`, where
22
+ * offset `i` describes the atom ending at caret position `i`:
23
+ *
24
+ * - a composite atom (fraction, root, script) owns the offset right *after*
25
+ * its children; the children are one level deeper;
26
+ * - each branch (numerator, denominator, radicand) opens with an empty-latex
27
+ * "start" atom.
28
+ *
29
+ * The rule: find the shallowest depth the selection touches, counting the caret
30
+ * it starts from. If the range crosses into a sibling branch at that depth, the
31
+ * real common parent is one level up. Then push both ends out of anything
32
+ * deeper. A script atom (`^2`) is kept with its base, since MathLive stores
33
+ * `x^2` as `x` followed by a separate `^2` atom.
34
+ *
35
+ * Pure, so it can be tested without a browser.
36
+ *
37
+ * @param {number} start
38
+ * @param {number} end
39
+ * @param {function(number): {depth?: number, latex?: string}} info
40
+ * @param {number} lastOffset
41
+ * @returns {[number, number]} the widened range, start <= end
42
+ */
43
+ var expandRange = exports.expandRange = function expandRange(start, end, info, lastOffset) {
44
+ var at = function at(i) {
45
+ return info(i) || {};
46
+ };
47
+ var depth = function depth(i) {
48
+ return at(i).depth || 0;
49
+ };
50
+ var isBranchStart = function isBranchStart(i) {
51
+ return depth(i) > 0 && at(i).latex === '';
52
+ };
53
+ if (end < start) {
54
+ var _ref = [end, start];
55
+ start = _ref[0];
56
+ end = _ref[1];
57
+ }
58
+ if (end - start < 1) {
59
+ return [start, end];
60
+ }
61
+ var target = Infinity;
62
+ for (var i = start; i <= end; i++) {
63
+ target = Math.min(target, depth(i));
64
+ }
65
+ for (var _i = start + 1; _i <= end; _i++) {
66
+ if (depth(_i) === target && isBranchStart(_i)) {
67
+ target -= 1;
68
+ break;
69
+ }
70
+ }
71
+ target = Math.max(target, 0);
72
+ while (start > 0 && depth(start) > target) {
73
+ start -= 1;
74
+ }
75
+ while (end < lastOffset && depth(end) > target) {
76
+ end += 1;
77
+ }
78
+
79
+ // Keep a superscript / subscript with its base.
80
+ var first = start + 1;
81
+ while (first <= end && depth(first) > target) {
82
+ first += 1;
83
+ }
84
+ if (start > 0 && first <= end && /^[\^_]/.test(at(first).latex || '') && depth(start) === target) {
85
+ start -= 1;
86
+ }
87
+ return [start, end];
88
+ };
89
+
90
+ /**
91
+ * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).
92
+ *
93
+ * This fixes both what the user sees highlighted and what copy/cut put on the
94
+ * clipboard, since both come from the selection. The selection direction is
95
+ * preserved, so further Shift+Arrow presses keep extending the same end.
96
+ *
97
+ * @param {HTMLElement} mathField a MathLive `<math-field>`
98
+ * @returns {function|undefined} teardown, to call on unmount
99
+ */
100
+ var trackWholeAtomSelection = exports.trackWholeAtomSelection = function trackWholeAtomSelection(mathField) {
101
+ if (!mathField || typeof mathField.addEventListener !== 'function') {
102
+ return undefined;
103
+ }
104
+ var applying = false;
105
+ var update = function update() {
106
+ if (applying) {
107
+ return;
108
+ }
109
+ try {
110
+ if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {
111
+ return;
112
+ }
113
+ var selection = mathField.selection;
114
+ var range = selection && selection.ranges && selection.ranges[0];
115
+ if (!range) {
116
+ return;
117
+ }
118
+ var _range = (0, _slicedToArray2["default"])(range, 2),
119
+ start = _range[0],
120
+ end = _range[1];
121
+ var lo = Math.min(start, end);
122
+ var hi = Math.max(start, end);
123
+ var _expandRange = expandRange(lo, hi, function (i) {
124
+ return mathField.getElementInfo(i);
125
+ }, mathField.lastOffset),
126
+ _expandRange2 = (0, _slicedToArray2["default"])(_expandRange, 2),
127
+ nextStart = _expandRange2[0],
128
+ nextEnd = _expandRange2[1];
129
+ if (nextStart === lo && nextEnd === hi) {
130
+ return;
131
+ }
132
+ applying = true;
133
+ mathField.selection = {
134
+ ranges: [[nextStart, nextEnd]],
135
+ direction: selection.direction
136
+ };
137
+ } catch (e) {
138
+ log('could not normalise the selection: %s', e && e.message);
139
+ } finally {
140
+ applying = false;
141
+ }
142
+ };
143
+ mathField.addEventListener('selection-change', update);
144
+ return function () {
145
+ return mathField.removeEventListener('selection-change', update);
146
+ };
147
+ };
148
+ //# sourceMappingURL=selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.js","names":["_debug","_interopRequireDefault","require","log","debug","expandRange","exports","start","end","info","lastOffset","at","i","depth","isBranchStart","latex","_ref","target","Infinity","Math","min","max","first","test","trackWholeAtomSelection","mathField","addEventListener","undefined","applying","update","selectionIsCollapsed","getElementInfo","selection","range","ranges","_range","_slicedToArray2","lo","hi","_expandRange","_expandRange2","nextStart","nextEnd","direction","e","message","removeEventListener"],"sources":["../src/selection.js"],"sourcesContent":["import debug from 'debug';\n\nconst log = debug('pie-lib:math-input-mathlive:selection');\n\n/**\n * Widen a selection so it covers whole atoms of a single parent.\n *\n * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into\n * a fraction instead of over it: from before `2x` in `5x^2+2x+\\frac{\\pi}{2}-4`,\n * five presses select `2x+` plus the `\\pi` of the numerator, and copying gives\n * `2x+\\pi`. MathQuill treated a fraction (root, superscript...) as one unit,\n * which is what authors expect.\n *\n * The tree is recovered from MathLive's public `getElementInfo(offset)`, where\n * offset `i` describes the atom ending at caret position `i`:\n *\n * - a composite atom (fraction, root, script) owns the offset right *after*\n * its children; the children are one level deeper;\n * - each branch (numerator, denominator, radicand) opens with an empty-latex\n * \"start\" atom.\n *\n * The rule: find the shallowest depth the selection touches, counting the caret\n * it starts from. If the range crosses into a sibling branch at that depth, the\n * real common parent is one level up. Then push both ends out of anything\n * deeper. A script atom (`^2`) is kept with its base, since MathLive stores\n * `x^2` as `x` followed by a separate `^2` atom.\n *\n * Pure, so it can be tested without a browser.\n *\n * @param {number} start\n * @param {number} end\n * @param {function(number): {depth?: number, latex?: string}} info\n * @param {number} lastOffset\n * @returns {[number, number]} the widened range, start <= end\n */\nexport const expandRange = (start, end, info, lastOffset) => {\n const at = (i) => info(i) || {};\n const depth = (i) => at(i).depth || 0;\n const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';\n\n if (end < start) {\n [start, end] = [end, start];\n }\n\n if (end - start < 1) {\n return [start, end];\n }\n\n let target = Infinity;\n\n for (let i = start; i <= end; i++) {\n target = Math.min(target, depth(i));\n }\n\n for (let i = start + 1; i <= end; i++) {\n if (depth(i) === target && isBranchStart(i)) {\n target -= 1;\n break;\n }\n }\n\n target = Math.max(target, 0);\n\n while (start > 0 && depth(start) > target) {\n start -= 1;\n }\n\n while (end < lastOffset && depth(end) > target) {\n end += 1;\n }\n\n // Keep a superscript / subscript with its base.\n let first = start + 1;\n\n while (first <= end && depth(first) > target) {\n first += 1;\n }\n\n if (start > 0 && first <= end && /^[\\^_]/.test(at(first).latex || '') && depth(start) === target) {\n start -= 1;\n }\n\n return [start, end];\n};\n\n/**\n * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).\n *\n * This fixes both what the user sees highlighted and what copy/cut put on the\n * clipboard, since both come from the selection. The selection direction is\n * preserved, so further Shift+Arrow presses keep extending the same end.\n *\n * @param {HTMLElement} mathField a MathLive `<math-field>`\n * @returns {function|undefined} teardown, to call on unmount\n */\nexport const trackWholeAtomSelection = (mathField) => {\n if (!mathField || typeof mathField.addEventListener !== 'function') {\n return undefined;\n }\n\n let applying = false;\n\n const update = () => {\n if (applying) {\n return;\n }\n\n try {\n if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {\n return;\n }\n\n const selection = mathField.selection;\n const range = selection && selection.ranges && selection.ranges[0];\n\n if (!range) {\n return;\n }\n\n const [start, end] = range;\n const lo = Math.min(start, end);\n const hi = Math.max(start, end);\n const [nextStart, nextEnd] = expandRange(\n lo,\n hi,\n (i) => mathField.getElementInfo(i),\n mathField.lastOffset,\n );\n\n if (nextStart === lo && nextEnd === hi) {\n return;\n }\n\n applying = true;\n mathField.selection = { ranges: [[nextStart, nextEnd]], direction: selection.direction };\n } catch (e) {\n log('could not normalise the selection: %s', e && e.message);\n } finally {\n applying = false;\n }\n };\n\n mathField.addEventListener('selection-change', update);\n\n return () => mathField.removeEventListener('selection-change', update);\n};\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.5-beta.10",
6
+ "version": "1.1.5-beta.11",
7
7
  "description": "MathLive-based math input (drop-in alternative to @pie-lib/math-input)",
8
8
  "main": "lib/index.js",
9
9
  "module": "src/index.jsx",
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The editable Input keeps its selection snapped to whole atoms (see
3
+ * selection.js). This checks the wiring: attached on mount, removed on unmount.
4
+ */
5
+ jest.mock('../mathlive-instance', () => {
6
+ class FakeMathfieldElement {
7
+ constructor() {
8
+ const el = global.document.createElement('math-field');
9
+
10
+ el.value = '';
11
+ el.getValue = () => el.value;
12
+
13
+ return el;
14
+ }
15
+ }
16
+
17
+ return {
18
+ loadMathLive: () => Promise.resolve({ MathfieldElement: FakeMathfieldElement }),
19
+ getMacros: () => ({}),
20
+ MATH_MODE_SPACE: '\\ ',
21
+ applyShadowStyles: () => {},
22
+ trackPlaceholderCaret: () => () => {},
23
+ latexToMarkup: (l) => `<span>${l}</span>`,
24
+ };
25
+ });
26
+
27
+ const mockUntrack = jest.fn();
28
+
29
+ jest.mock('../selection', () => ({
30
+ trackWholeAtomSelection: jest.fn(() => mockUntrack),
31
+ }));
32
+
33
+ // eslint-disable-next-line import/first
34
+ import Input from '../mf/input';
35
+ // eslint-disable-next-line import/first
36
+ import { trackWholeAtomSelection } from '../selection';
37
+
38
+ describe('Input: whole-atom selection', () => {
39
+ beforeEach(() => {
40
+ trackWholeAtomSelection.mockClear();
41
+ mockUntrack.mockClear();
42
+ });
43
+
44
+ it('attaches the tracker to the mathfield on mount', async () => {
45
+ const input = new Input({});
46
+
47
+ input.holderRef = { current: document.createElement('span') };
48
+ await input.componentDidMount();
49
+
50
+ expect(trackWholeAtomSelection).toHaveBeenCalledTimes(1);
51
+ expect(trackWholeAtomSelection).toHaveBeenCalledWith(input.mathField);
52
+ });
53
+
54
+ it('removes it on unmount', async () => {
55
+ const input = new Input({});
56
+
57
+ input.holderRef = { current: document.createElement('span') };
58
+ await input.componentDidMount();
59
+ input.componentWillUnmount();
60
+
61
+ expect(mockUntrack).toHaveBeenCalledTimes(1);
62
+ expect(input.untrackSelection).toBeUndefined();
63
+ });
64
+ });
@@ -0,0 +1,212 @@
1
+ import { expandRange, trackWholeAtomSelection } from '../selection';
2
+
3
+ /**
4
+ * Offset tables captured from MathLive 0.110.0 `getElementInfo(i)` in Chrome,
5
+ * as [depth, latex] per offset. Offset `i` is the atom ending at caret `i`.
6
+ */
7
+ const table = (rows) => (i) => (rows[i] ? { depth: rows[i][0], latex: rows[i][1] } : undefined);
8
+
9
+ // 5x^2+2x+\frac{\pi}{2}-4
10
+ const POLY = table([
11
+ [0, ''], [0, '5'], [0, 'x'], [1, ''], [1, '2'], [0, '^2'], [0, '+'], [0, '2'], [0, 'x'], [0, '+'],
12
+ [1, ''], [1, '\\pi'], [1, ''], [1, '2'], [0, '\\frac{\\pi}{2}'], [0, '-'], [0, '4'],
13
+ ]);
14
+ const POLY_LAST = 16;
15
+
16
+ // a+\sqrt{x+1}+b
17
+ const ROOT = table([[0, ''], [0, 'a'], [0, '+'], [1, ''], [1, 'x'], [1, '+'], [1, '1'], [0, '\\sqrt{x+1}'], [0, '+'], [0, 'b']]);
18
+
19
+ // \frac{\frac{a}{b}}{c}
20
+ const NESTED = table([
21
+ [0, ''], [1, ''], [2, ''], [2, 'a'], [2, ''], [2, 'b'], [1, '\\frac{a}{b}'], [1, ''], [1, 'c'], [0, '\\frac{\\frac{a}{b}}{c}'],
22
+ ]);
23
+
24
+ describe('expandRange', () => {
25
+ const poly = (a, b) => expandRange(a, b, POLY, POLY_LAST);
26
+
27
+ it('leaves a selection of whole sibling atoms alone', () => {
28
+ expect(poly(6, 7)).toEqual([6, 7]); // 2
29
+ expect(poly(6, 9)).toEqual([6, 9]); // 2x+
30
+ expect(poly(6, 15)).toEqual([6, 15]); // 2x+\frac{\pi}{2}-
31
+ expect(poly(0, 16)).toEqual([0, 16]);
32
+ });
33
+
34
+ // The reported case: five Shift+Right presses from before `2x` gave `2x+\pi`.
35
+ it('takes the whole fraction when the selection reaches into it', () => {
36
+ expect(poly(6, 10)).toEqual([6, 14]);
37
+ expect(poly(6, 11)).toEqual([6, 14]);
38
+ expect(poly(6, 13)).toEqual([6, 14]);
39
+ });
40
+
41
+ it('takes the whole fraction when the selection leaves it', () => {
42
+ expect(poly(11, 15)).toEqual([9, 15]); // from the pi to after the minus
43
+ });
44
+
45
+ it('crossing from numerator to denominator selects the fraction', () => {
46
+ expect(poly(11, 13)).toEqual([9, 14]);
47
+ expect(poly(11, 12)).toEqual([9, 14]);
48
+ });
49
+
50
+ it('a selection inside one branch stays inside it', () => {
51
+ expect(poly(10, 11)).toEqual([10, 11]); // \pi
52
+ expect(poly(12, 13)).toEqual([12, 13]); // the 2 of the denominator
53
+ });
54
+
55
+ // MathLive stores x^2 as `x` then a separate `^2` atom.
56
+ it('keeps a superscript with its base', () => {
57
+ expect(poly(3, 7)).toEqual([1, 7]); // x^2+2, not ^2+2
58
+ expect(poly(4, 6)).toEqual([1, 6]); // x^2+
59
+ });
60
+
61
+ it('accepts a reversed range', () => {
62
+ expect(poly(11, 6)).toEqual([6, 14]);
63
+ });
64
+
65
+ it('leaves a collapsed range alone', () => {
66
+ expect(poly(11, 11)).toEqual([11, 11]);
67
+ });
68
+
69
+ it('handles roots the same way', () => {
70
+ expect(expandRange(2, 4, ROOT, 9)).toEqual([2, 7]);
71
+ expect(expandRange(4, 6, ROOT, 9)).toEqual([4, 6]); // +1 inside the radicand
72
+ });
73
+
74
+ it('climbs as many levels as needed in nested fractions', () => {
75
+ expect(expandRange(2, 3, NESTED, 9)).toEqual([2, 3]); // a
76
+ expect(expandRange(3, 5, NESTED, 9)).toEqual([1, 6]); // a/b crossing -> inner fraction
77
+ expect(expandRange(6, 7, NESTED, 9)).toEqual([0, 9]); // inner -> outer denominator
78
+ });
79
+
80
+ it('tolerates missing info', () => {
81
+ expect(expandRange(1, 3, () => undefined, 5)).toEqual([1, 3]);
82
+ });
83
+ });
84
+
85
+ describe('trackWholeAtomSelection', () => {
86
+ const stubField = (range, direction = 'forward') => {
87
+ const listeners = {};
88
+
89
+ return {
90
+ selection: { ranges: [range], direction },
91
+ get selectionIsCollapsed() {
92
+ return this.selection.ranges[0][0] === this.selection.ranges[0][1];
93
+ },
94
+ lastOffset: POLY_LAST,
95
+ getElementInfo: POLY,
96
+ sets: 0,
97
+ addEventListener(type, fn) {
98
+ listeners[type] = fn;
99
+ },
100
+ removeEventListener(type, fn) {
101
+ if (listeners[type] === fn) {
102
+ delete listeners[type];
103
+ }
104
+ },
105
+ fire(type) {
106
+ if (listeners[type]) {
107
+ listeners[type]();
108
+ }
109
+ },
110
+ has(type) {
111
+ return !!listeners[type];
112
+ },
113
+ };
114
+ };
115
+
116
+ const track = (field) => {
117
+ // count assignments without breaking the getter
118
+ let current = field.selection;
119
+
120
+ Object.defineProperty(field, 'selection', {
121
+ get: () => current,
122
+ set: (v) => {
123
+ field.sets += 1;
124
+ current = v;
125
+ },
126
+ });
127
+
128
+ return trackWholeAtomSelection(field);
129
+ };
130
+
131
+ it('widens the selection on selection-change', () => {
132
+ const field = stubField([6, 11]);
133
+
134
+ track(field);
135
+ field.fire('selection-change');
136
+
137
+ expect(field.selection.ranges).toEqual([[6, 14]]);
138
+ });
139
+
140
+ it('preserves the direction, so Shift+Arrow keeps extending the same end', () => {
141
+ const field = stubField([11, 16], 'backward');
142
+
143
+ track(field);
144
+ field.fire('selection-change');
145
+
146
+ expect(field.selection).toEqual({ ranges: [[9, 16]], direction: 'backward' });
147
+ });
148
+
149
+ it('does not touch a selection that is already whole', () => {
150
+ const field = stubField([6, 9]);
151
+
152
+ track(field);
153
+ field.fire('selection-change');
154
+
155
+ expect(field.sets).toBe(0);
156
+ });
157
+
158
+ it('ignores a collapsed caret', () => {
159
+ const field = stubField([11, 11]);
160
+
161
+ track(field);
162
+ field.fire('selection-change');
163
+
164
+ expect(field.sets).toBe(0);
165
+ });
166
+
167
+ it('does not recurse when its own assignment fires selection-change', () => {
168
+ const field = stubField([6, 11]);
169
+ let current = field.selection;
170
+
171
+ Object.defineProperty(field, 'selection', {
172
+ get: () => current,
173
+ set: (v) => {
174
+ field.sets += 1;
175
+ current = v;
176
+ field.fire('selection-change');
177
+ },
178
+ });
179
+
180
+ trackWholeAtomSelection(field);
181
+ field.fire('selection-change');
182
+
183
+ expect(field.sets).toBe(1);
184
+ expect(field.selection.ranges).toEqual([[6, 14]]);
185
+ });
186
+
187
+ it('survives a field that throws', () => {
188
+ const field = stubField([6, 11]);
189
+
190
+ field.getElementInfo = () => {
191
+ throw new Error('boom');
192
+ };
193
+
194
+ track(field);
195
+
196
+ expect(() => field.fire('selection-change')).not.toThrow();
197
+ });
198
+
199
+ it('tears down its listener', () => {
200
+ const field = stubField([6, 11]);
201
+ const untrack = track(field);
202
+
203
+ expect(field.has('selection-change')).toBe(true);
204
+ untrack();
205
+ expect(field.has('selection-change')).toBe(false);
206
+ });
207
+
208
+ it('is a no-op without a field', () => {
209
+ expect(trackWholeAtomSelection(undefined)).toBeUndefined();
210
+ expect(trackWholeAtomSelection({})).toBeUndefined();
211
+ });
212
+ });
package/src/mf/input.jsx CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  trackPlaceholderCaret,
11
11
  } from '../mathlive-instance';
12
12
  import { toMathLive, fromMathLive, keyToAction, KEYSTROKES } from '../latex-bridge';
13
+ import { trackWholeAtomSelection } from '../selection';
13
14
 
14
15
  const log = debug('pie-lib:math-input-mathlive:input');
15
16
 
@@ -93,6 +94,9 @@ export class Input extends React.Component {
93
94
  // A selected `\placeholder{}` gets no MathLive caret; draw one.
94
95
  this.untrackCaret = trackPlaceholderCaret(this.mathField);
95
96
 
97
+ // Shift+Arrow would otherwise select half a fraction; snap to whole atoms.
98
+ this.untrackSelection = trackWholeAtomSelection(this.mathField);
99
+
96
100
  this.mathField.addEventListener('input', this.onInputEdit);
97
101
 
98
102
  // Belt and braces: drop anything a previous mount may have left behind.
@@ -136,6 +140,11 @@ export class Input extends React.Component {
136
140
  this.untrackCaret = undefined;
137
141
  }
138
142
 
143
+ if (this.untrackSelection) {
144
+ this.untrackSelection();
145
+ this.untrackSelection = undefined;
146
+ }
147
+
139
148
  if (this.mathField) {
140
149
  this.mathField.removeEventListener('input', this.onInputEdit);
141
150
  this.mathField.remove();
@@ -0,0 +1,146 @@
1
+ import debug from 'debug';
2
+
3
+ const log = debug('pie-lib:math-input-mathlive:selection');
4
+
5
+ /**
6
+ * Widen a selection so it covers whole atoms of a single parent.
7
+ *
8
+ * MathLive extends a selection over *flat offsets*, so Shift+Arrow walks into
9
+ * a fraction instead of over it: from before `2x` in `5x^2+2x+\frac{\pi}{2}-4`,
10
+ * five presses select `2x+` plus the `\pi` of the numerator, and copying gives
11
+ * `2x+\pi`. MathQuill treated a fraction (root, superscript...) as one unit,
12
+ * which is what authors expect.
13
+ *
14
+ * The tree is recovered from MathLive's public `getElementInfo(offset)`, where
15
+ * offset `i` describes the atom ending at caret position `i`:
16
+ *
17
+ * - a composite atom (fraction, root, script) owns the offset right *after*
18
+ * its children; the children are one level deeper;
19
+ * - each branch (numerator, denominator, radicand) opens with an empty-latex
20
+ * "start" atom.
21
+ *
22
+ * The rule: find the shallowest depth the selection touches, counting the caret
23
+ * it starts from. If the range crosses into a sibling branch at that depth, the
24
+ * real common parent is one level up. Then push both ends out of anything
25
+ * deeper. A script atom (`^2`) is kept with its base, since MathLive stores
26
+ * `x^2` as `x` followed by a separate `^2` atom.
27
+ *
28
+ * Pure, so it can be tested without a browser.
29
+ *
30
+ * @param {number} start
31
+ * @param {number} end
32
+ * @param {function(number): {depth?: number, latex?: string}} info
33
+ * @param {number} lastOffset
34
+ * @returns {[number, number]} the widened range, start <= end
35
+ */
36
+ export const expandRange = (start, end, info, lastOffset) => {
37
+ const at = (i) => info(i) || {};
38
+ const depth = (i) => at(i).depth || 0;
39
+ const isBranchStart = (i) => depth(i) > 0 && at(i).latex === '';
40
+
41
+ if (end < start) {
42
+ [start, end] = [end, start];
43
+ }
44
+
45
+ if (end - start < 1) {
46
+ return [start, end];
47
+ }
48
+
49
+ let target = Infinity;
50
+
51
+ for (let i = start; i <= end; i++) {
52
+ target = Math.min(target, depth(i));
53
+ }
54
+
55
+ for (let i = start + 1; i <= end; i++) {
56
+ if (depth(i) === target && isBranchStart(i)) {
57
+ target -= 1;
58
+ break;
59
+ }
60
+ }
61
+
62
+ target = Math.max(target, 0);
63
+
64
+ while (start > 0 && depth(start) > target) {
65
+ start -= 1;
66
+ }
67
+
68
+ while (end < lastOffset && depth(end) > target) {
69
+ end += 1;
70
+ }
71
+
72
+ // Keep a superscript / subscript with its base.
73
+ let first = start + 1;
74
+
75
+ while (first <= end && depth(first) > target) {
76
+ first += 1;
77
+ }
78
+
79
+ if (start > 0 && first <= end && /^[\^_]/.test(at(first).latex || '') && depth(start) === target) {
80
+ start -= 1;
81
+ }
82
+
83
+ return [start, end];
84
+ };
85
+
86
+ /**
87
+ * Keep a mathfield's selection snapped to whole atoms (see {@link expandRange}).
88
+ *
89
+ * This fixes both what the user sees highlighted and what copy/cut put on the
90
+ * clipboard, since both come from the selection. The selection direction is
91
+ * preserved, so further Shift+Arrow presses keep extending the same end.
92
+ *
93
+ * @param {HTMLElement} mathField a MathLive `<math-field>`
94
+ * @returns {function|undefined} teardown, to call on unmount
95
+ */
96
+ export const trackWholeAtomSelection = (mathField) => {
97
+ if (!mathField || typeof mathField.addEventListener !== 'function') {
98
+ return undefined;
99
+ }
100
+
101
+ let applying = false;
102
+
103
+ const update = () => {
104
+ if (applying) {
105
+ return;
106
+ }
107
+
108
+ try {
109
+ if (mathField.selectionIsCollapsed || typeof mathField.getElementInfo !== 'function') {
110
+ return;
111
+ }
112
+
113
+ const selection = mathField.selection;
114
+ const range = selection && selection.ranges && selection.ranges[0];
115
+
116
+ if (!range) {
117
+ return;
118
+ }
119
+
120
+ const [start, end] = range;
121
+ const lo = Math.min(start, end);
122
+ const hi = Math.max(start, end);
123
+ const [nextStart, nextEnd] = expandRange(
124
+ lo,
125
+ hi,
126
+ (i) => mathField.getElementInfo(i),
127
+ mathField.lastOffset,
128
+ );
129
+
130
+ if (nextStart === lo && nextEnd === hi) {
131
+ return;
132
+ }
133
+
134
+ applying = true;
135
+ mathField.selection = { ranges: [[nextStart, nextEnd]], direction: selection.direction };
136
+ } catch (e) {
137
+ log('could not normalise the selection: %s', e && e.message);
138
+ } finally {
139
+ applying = false;
140
+ }
141
+ };
142
+
143
+ mathField.addEventListener('selection-change', update);
144
+
145
+ return () => mathField.removeEventListener('selection-change', update);
146
+ };