@khanacademy/math-input 0.4.1 → 0.5.2
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/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/{build/math-input.css → dist/es/index.css} +0 -150
- package/dist/es/index.js +7798 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.css +586 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7768 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/dist/strings.js +71 -0
- package/index.html +20 -0
- package/less/echo.less +56 -0
- package/less/main.less +5 -0
- package/less/overrides.less +129 -0
- package/less/popover.less +22 -0
- package/less/tabbar.less +6 -0
- package/package.json +60 -89
- package/src/actions/index.js +57 -0
- package/src/components/__tests__/gesture-state-machine_test.js +437 -0
- package/src/components/__tests__/node-manager_test.js +89 -0
- package/src/components/__tests__/two-page-keypad_test.js +42 -0
- package/src/components/app.js +73 -0
- package/src/components/common-style.js +47 -0
- package/src/components/compute-layout-parameters.js +157 -0
- package/src/components/corner-decal.js +56 -0
- package/src/components/echo-manager.js +160 -0
- package/src/components/empty-keypad-button.js +49 -0
- package/src/components/expression-keypad.js +323 -0
- package/src/components/fraction-keypad.js +176 -0
- package/src/components/gesture-manager.js +226 -0
- package/src/components/gesture-state-machine.js +283 -0
- package/src/components/icon.js +74 -0
- package/src/components/iconography/arrow.js +22 -0
- package/src/components/iconography/backspace.js +29 -0
- package/src/components/iconography/cdot.js +29 -0
- package/src/components/iconography/cos.js +30 -0
- package/src/components/iconography/cube-root.js +36 -0
- package/src/components/iconography/dismiss.js +25 -0
- package/src/components/iconography/divide.js +34 -0
- package/src/components/iconography/down.js +16 -0
- package/src/components/iconography/equal.js +33 -0
- package/src/components/iconography/exp-2.js +29 -0
- package/src/components/iconography/exp-3.js +29 -0
- package/src/components/iconography/exp.js +29 -0
- package/src/components/iconography/frac.js +44 -0
- package/src/components/iconography/geq.js +33 -0
- package/src/components/iconography/gt.js +33 -0
- package/src/components/iconography/index.js +45 -0
- package/src/components/iconography/jump-into-numerator.js +41 -0
- package/src/components/iconography/jump-out-base.js +30 -0
- package/src/components/iconography/jump-out-denominator.js +41 -0
- package/src/components/iconography/jump-out-exponent.js +30 -0
- package/src/components/iconography/jump-out-numerator.js +41 -0
- package/src/components/iconography/jump-out-parentheses.js +33 -0
- package/src/components/iconography/left-paren.js +33 -0
- package/src/components/iconography/left.js +16 -0
- package/src/components/iconography/leq.js +33 -0
- package/src/components/iconography/ln.js +29 -0
- package/src/components/iconography/log-n.js +29 -0
- package/src/components/iconography/log.js +29 -0
- package/src/components/iconography/lt.js +33 -0
- package/src/components/iconography/minus.js +32 -0
- package/src/components/iconography/neq.js +33 -0
- package/src/components/iconography/parens.js +33 -0
- package/src/components/iconography/percent.js +49 -0
- package/src/components/iconography/period.js +26 -0
- package/src/components/iconography/plus.js +32 -0
- package/src/components/iconography/radical.js +36 -0
- package/src/components/iconography/right-paren.js +33 -0
- package/src/components/iconography/right.js +16 -0
- package/src/components/iconography/sin.js +30 -0
- package/src/components/iconography/sqrt.js +32 -0
- package/src/components/iconography/tan.js +30 -0
- package/src/components/iconography/times.js +33 -0
- package/src/components/iconography/up.js +16 -0
- package/src/components/input/__tests__/context-tracking_test.js +177 -0
- package/src/components/input/__tests__/math-wrapper.jsx +33 -0
- package/src/components/input/__tests__/mathquill_test.js +747 -0
- package/src/components/input/cursor-contexts.js +29 -0
- package/src/components/input/cursor-handle.js +137 -0
- package/src/components/input/drag-listener.js +75 -0
- package/src/components/input/math-input.js +924 -0
- package/src/components/input/math-wrapper.js +959 -0
- package/src/components/input/scroll-into-view.js +72 -0
- package/src/components/keypad/button-assets.js +492 -0
- package/src/components/keypad/button.js +106 -0
- package/src/components/keypad/button.stories.js +29 -0
- package/src/components/keypad/index.js +64 -0
- package/src/components/keypad/keypad-page-items.js +106 -0
- package/src/components/keypad/keypad-pages.stories.js +32 -0
- package/src/components/keypad/keypad.stories.js +35 -0
- package/src/components/keypad/numeric-input-page.js +100 -0
- package/src/components/keypad/pre-algebra-page.js +98 -0
- package/src/components/keypad/trigonometry-page.js +90 -0
- package/src/components/keypad-button.js +366 -0
- package/src/components/keypad-container.js +303 -0
- package/src/components/keypad.js +154 -0
- package/src/components/many-keypad-button.js +44 -0
- package/src/components/math-icon.js +65 -0
- package/src/components/multi-symbol-grid.js +182 -0
- package/src/components/multi-symbol-popover.js +59 -0
- package/src/components/navigation-pad.js +139 -0
- package/src/components/node-manager.js +129 -0
- package/src/components/popover-manager.js +76 -0
- package/src/components/popover-state-machine.js +173 -0
- package/src/components/prop-types.js +82 -0
- package/src/components/provided-keypad.js +103 -0
- package/src/components/styles.js +38 -0
- package/src/components/svg-icon.js +25 -0
- package/src/components/tabbar/__tests__/tabbar_test.js +65 -0
- package/src/components/tabbar/icons.js +69 -0
- package/src/components/tabbar/item.js +138 -0
- package/src/components/tabbar/tabbar.js +61 -0
- package/src/components/tabbar/tabbar.stories.js +60 -0
- package/src/components/tabbar/types.js +3 -0
- package/src/components/text-icon.js +52 -0
- package/src/components/touchable-keypad-button.js +146 -0
- package/src/components/two-page-keypad.js +99 -0
- package/src/components/velocity-tracker.js +76 -0
- package/src/components/z-indexes.js +9 -0
- package/src/consts.js +74 -0
- package/src/data/key-configs.js +349 -0
- package/src/data/keys.js +72 -0
- package/src/demo.js +8 -0
- package/src/fake-react-native-web/index.js +12 -0
- package/src/fake-react-native-web/text.js +56 -0
- package/src/fake-react-native-web/view.js +91 -0
- package/src/index.js +14 -0
- package/src/native-app.js +84 -0
- package/src/store/index.js +505 -0
- package/src/utils.js +18 -0
- package/tools/svg-to-react/convert.py +111 -0
- package/tools/svg-to-react/icons/math-keypad-icon-0.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-1.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-2.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-3.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-4.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-5.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-6.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-7.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-8.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-9.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-addition.svg +34 -0
- package/tools/svg-to-react/icons/math-keypad-icon-cos.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-delete.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-dismiss.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-division.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals-not.svg +50 -0
- package/tools/svg-to-react/icons/math-keypad-icon-equals.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-2.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-exponent.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-fraction.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-greater-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-base.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-denominator.svg +48 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-exponent.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-jump-out-parentheses.svg +44 -0
- package/tools/svg-to-react/icons/math-keypad-icon-less-than.svg +46 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-10.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log-e.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-log.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-cross.svg +40 -0
- package/tools/svg-to-react/icons/math-keypad-icon-multiplication-dot.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-percent.svg +42 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-2.svg +36 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical-3.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radical.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-radix-character.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-sin.svg +38 -0
- package/tools/svg-to-react/icons/math-keypad-icon-subtraction.svg +32 -0
- package/tools/svg-to-react/icons/math-keypad-icon-tan.svg +38 -0
- package/tools/svg-to-react/symbol_map.py +41 -0
- package/LICENSE.txt +0 -21
- package/build/math-input.js +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import {StyleSheet, css} from "aphrodite";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import type {CSSProperties} from "aphrodite";
|
|
7
|
+
|
|
8
|
+
type Props = {|
|
|
9
|
+
ariaLabel?: string,
|
|
10
|
+
children: React.Node,
|
|
11
|
+
// The `dynamicStyle` prop is provided for animating dynamic
|
|
12
|
+
// properties, as creating Aphrodite StyleSheets in animation loops is
|
|
13
|
+
// expensive. `dynamicStyle` should be a raw style object, rather than
|
|
14
|
+
// a StyleSheet.
|
|
15
|
+
dynamicStyle?: CSSProperties,
|
|
16
|
+
// The `extraClassName` prop should almost never be used. It gives the
|
|
17
|
+
// client a way to provide an additional CSS class name, to augment
|
|
18
|
+
// the class name generated by Aphrodite. (Right now, it's only used to
|
|
19
|
+
// disable some externally-applied CSS that would otherwise be far too
|
|
20
|
+
// difficult to override with inline styles.)
|
|
21
|
+
extraClassName?: string,
|
|
22
|
+
numberOfLines?: number,
|
|
23
|
+
onClick?: () => void,
|
|
24
|
+
onTouchCancel?: () => void,
|
|
25
|
+
onTouchEnd?: () => void,
|
|
26
|
+
onTouchMove?: () => void,
|
|
27
|
+
onTouchStart?: () => void,
|
|
28
|
+
role?: string,
|
|
29
|
+
style?: CSSProperties,
|
|
30
|
+
|};
|
|
31
|
+
|
|
32
|
+
class View extends React.Component<Props> {
|
|
33
|
+
// $FlowFixMe[signature-verification-failure]
|
|
34
|
+
static styles = StyleSheet.create({
|
|
35
|
+
// From: https://github.com/necolas/react-native-web/blob/master/src/components/View/index.js
|
|
36
|
+
// eslint-disable-next-line react-native/no-unused-styles
|
|
37
|
+
initial: {
|
|
38
|
+
alignItems: "stretch",
|
|
39
|
+
borderWidth: 0,
|
|
40
|
+
borderStyle: "solid",
|
|
41
|
+
boxSizing: "border-box",
|
|
42
|
+
display: "flex",
|
|
43
|
+
flexBasis: "auto",
|
|
44
|
+
flexDirection: "column",
|
|
45
|
+
margin: 0,
|
|
46
|
+
padding: 0,
|
|
47
|
+
position: "relative",
|
|
48
|
+
// button and anchor reset
|
|
49
|
+
backgroundColor: "transparent",
|
|
50
|
+
color: "inherit",
|
|
51
|
+
font: "inherit",
|
|
52
|
+
textAlign: "inherit",
|
|
53
|
+
textDecorationLine: "none",
|
|
54
|
+
// list reset
|
|
55
|
+
listStyle: "none",
|
|
56
|
+
// fix flexbox bugs
|
|
57
|
+
maxWidth: "100%",
|
|
58
|
+
minHeight: 0,
|
|
59
|
+
minWidth: 0,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
render(): React.Element<"div"> {
|
|
64
|
+
const className =
|
|
65
|
+
css(
|
|
66
|
+
View.styles.initial,
|
|
67
|
+
...(Array.isArray(this.props.style)
|
|
68
|
+
? this.props.style
|
|
69
|
+
: [this.props.style]),
|
|
70
|
+
) +
|
|
71
|
+
(this.props.extraClassName ? ` ${this.props.extraClassName}` : "");
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
className={className}
|
|
76
|
+
style={this.props.dynamicStyle}
|
|
77
|
+
onClick={this.props.onClick}
|
|
78
|
+
onTouchCancel={this.props.onTouchCancel}
|
|
79
|
+
onTouchEnd={this.props.onTouchEnd}
|
|
80
|
+
onTouchMove={this.props.onTouchMove}
|
|
81
|
+
onTouchStart={this.props.onTouchStart}
|
|
82
|
+
aria-label={this.props.ariaLabel}
|
|
83
|
+
role={this.props.role}
|
|
84
|
+
>
|
|
85
|
+
{this.props.children}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default View;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* A single entry-point for all of the external-facing functionality.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import "../less/main.less";
|
|
7
|
+
|
|
8
|
+
export {default as KeypadInput} from "./components/input/math-input.js";
|
|
9
|
+
export {
|
|
10
|
+
keypadConfigurationPropType,
|
|
11
|
+
keypadElementPropType,
|
|
12
|
+
} from "./components/prop-types.js";
|
|
13
|
+
export {default as Keypad} from "./components/provided-keypad.js";
|
|
14
|
+
export {KeypadTypes} from "./consts.js";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import ReactDOM from "react-dom";
|
|
3
|
+
|
|
4
|
+
import KeyConfigs from "./data/key-configs.js";
|
|
5
|
+
import {View} from "./fake-react-native-web/index.js";
|
|
6
|
+
|
|
7
|
+
import {KeypadInput} from "./index.js";
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line react/prop-types
|
|
10
|
+
const ManualInput = ({handler}) => {
|
|
11
|
+
return (
|
|
12
|
+
<div>
|
|
13
|
+
{Object.keys(KeyConfigs).map((k) => (
|
|
14
|
+
<button
|
|
15
|
+
style={{display: "block"}}
|
|
16
|
+
disabled={!handler}
|
|
17
|
+
onClick={() => handler(k)}
|
|
18
|
+
>
|
|
19
|
+
{k} : {KeyConfigs[k].ariaLabel}
|
|
20
|
+
</button>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
class App extends React.Component {
|
|
27
|
+
state = {
|
|
28
|
+
active: false,
|
|
29
|
+
handler: null,
|
|
30
|
+
keypadElement: {
|
|
31
|
+
activate: () => this.setState({active: true}),
|
|
32
|
+
dismiss: () => this.setState({active: false}),
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
configure: (config) => console.log("configure:", config),
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
setCursor: (cursor) => console.log("Cursor:", cursor),
|
|
37
|
+
setKeyHandler: (handler) => this.setState({handler}),
|
|
38
|
+
getDOMNode: () => null,
|
|
39
|
+
},
|
|
40
|
+
value: "",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
return (
|
|
45
|
+
<View>
|
|
46
|
+
<div
|
|
47
|
+
style={{
|
|
48
|
+
marginTop: 10,
|
|
49
|
+
marginLeft: 20,
|
|
50
|
+
marginRight: 20,
|
|
51
|
+
marginBottom: 40,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<KeypadInput
|
|
55
|
+
value={this.state.value}
|
|
56
|
+
ref={(inp) => (this.inp = inp)}
|
|
57
|
+
keypadElement={this.state.keypadElement}
|
|
58
|
+
onChange={(value, cb) => this.setState({value}, cb)}
|
|
59
|
+
onFocus={() => this.state.keypadElement.activate()}
|
|
60
|
+
onBlur={() => this.state.keypadElement.dismiss()}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<button
|
|
65
|
+
style={{display: "block"}}
|
|
66
|
+
onClick={() => this.inp.focus()}
|
|
67
|
+
>
|
|
68
|
+
Focus
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
<div style={{padding: 20}}>
|
|
72
|
+
Handler assigned: {"" + !!this.state.handler}
|
|
73
|
+
<br />
|
|
74
|
+
Active: {"" + this.state.active}
|
|
75
|
+
</div>
|
|
76
|
+
<br />
|
|
77
|
+
<br />
|
|
78
|
+
<ManualInput handler={this.state.handler} />
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
ReactDOM.render(<App />, document.getElementById("root"));
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import * as Redux from "redux";
|
|
4
|
+
|
|
5
|
+
import {tabletCutoffPx} from "../components/common-style.js";
|
|
6
|
+
import {computeLayoutParameters} from "../components/compute-layout-parameters.js";
|
|
7
|
+
import ExpressionKeypad from "../components/expression-keypad.js";
|
|
8
|
+
import FractionKeypad from "../components/fraction-keypad.js";
|
|
9
|
+
import GestureManager from "../components/gesture-manager.js";
|
|
10
|
+
import * as CursorContexts from "../components/input/cursor-contexts.js";
|
|
11
|
+
import VelocityTracker from "../components/velocity-tracker.js";
|
|
12
|
+
import {
|
|
13
|
+
DeviceOrientations,
|
|
14
|
+
DeviceTypes,
|
|
15
|
+
EchoAnimationTypes,
|
|
16
|
+
KeyTypes,
|
|
17
|
+
KeypadTypes,
|
|
18
|
+
LayoutModes,
|
|
19
|
+
} from "../consts.js";
|
|
20
|
+
import KeyConfigs from "../data/key-configs.js";
|
|
21
|
+
import Keys from "../data/keys.js";
|
|
22
|
+
|
|
23
|
+
const keypadForType = {
|
|
24
|
+
[KeypadTypes.FRACTION]: FractionKeypad,
|
|
25
|
+
[KeypadTypes.EXPRESSION]: ExpressionKeypad,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const createStore = (): $FlowFixMe => {
|
|
29
|
+
const initialInputState: {|
|
|
30
|
+
keyHandler: $FlowFixMe,
|
|
31
|
+
cursor: $FlowFixMe,
|
|
32
|
+
|} = {
|
|
33
|
+
keyHandler: null,
|
|
34
|
+
cursor: {
|
|
35
|
+
context: CursorContexts.NONE,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const inputReducer = function (state = initialInputState, action) {
|
|
40
|
+
switch (action.type) {
|
|
41
|
+
case "SetKeyHandler":
|
|
42
|
+
return {
|
|
43
|
+
...state,
|
|
44
|
+
keyHandler: action.keyHandler,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
case "PressKey":
|
|
48
|
+
const keyConfig = KeyConfigs[action.key];
|
|
49
|
+
if (keyConfig.type !== KeyTypes.KEYPAD_NAVIGATION) {
|
|
50
|
+
// This is probably an anti-pattern but it works for the
|
|
51
|
+
// case where we don't actually control the state but we
|
|
52
|
+
// still want to communicate with the other object
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
cursor: state.keyHandler(keyConfig.id),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// TODO(kevinb) get state from MathQuill and store it?
|
|
60
|
+
return state;
|
|
61
|
+
|
|
62
|
+
case "SetCursor":
|
|
63
|
+
return {
|
|
64
|
+
...state,
|
|
65
|
+
cursor: action.cursor,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const defaultKeypadType = KeypadTypes.EXPRESSION;
|
|
74
|
+
|
|
75
|
+
const initialKeypadState = {
|
|
76
|
+
extraKeys: ["x", "y", Keys.THETA, Keys.PI],
|
|
77
|
+
keypadType: defaultKeypadType,
|
|
78
|
+
active: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const keypadReducer = function (state = initialKeypadState, action) {
|
|
82
|
+
switch (action.type) {
|
|
83
|
+
case "DismissKeypad":
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
active: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
case "ActivateKeypad":
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
active: true,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
case "ConfigureKeypad":
|
|
96
|
+
return {
|
|
97
|
+
...state,
|
|
98
|
+
// Default `extraKeys` to the empty array.
|
|
99
|
+
extraKeys: [],
|
|
100
|
+
...action.configuration,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
case "PressKey":
|
|
104
|
+
const keyConfig = KeyConfigs[action.key];
|
|
105
|
+
// NOTE(charlie): Our keypad system operates by triggering key
|
|
106
|
+
// presses with key IDs in a dumb manner, such that the keys
|
|
107
|
+
// don't know what they can do--instead, the store is
|
|
108
|
+
// responsible for interpreting key presses and triggering the
|
|
109
|
+
// right actions when they occur. Hence, we figure off a
|
|
110
|
+
// dismissal here rather than dispatching a dismiss action in
|
|
111
|
+
// the first place.
|
|
112
|
+
if (keyConfig.id === Keys.DISMISS) {
|
|
113
|
+
return keypadReducer(state, {type: "DismissKeypad"});
|
|
114
|
+
}
|
|
115
|
+
return state;
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
return state;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// We default to the right-most page. This is done so-as to enforce a
|
|
123
|
+
// consistent orientation between the view pager layout and the flattened
|
|
124
|
+
// layout, where our default page appears on the far right.
|
|
125
|
+
const getDefaultPage = (numPages) => numPages - 1;
|
|
126
|
+
|
|
127
|
+
const initialPagerState = {
|
|
128
|
+
animateToPosition: false,
|
|
129
|
+
currentPage: getDefaultPage(keypadForType[defaultKeypadType].numPages),
|
|
130
|
+
// The cumulative differential in the horizontal direction for the
|
|
131
|
+
// current swipe.
|
|
132
|
+
dx: 0,
|
|
133
|
+
numPages: keypadForType[defaultKeypadType].numPages,
|
|
134
|
+
pageWidthPx: 0,
|
|
135
|
+
velocityTracker: new VelocityTracker(),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const pagerReducer = function (state = initialPagerState, action) {
|
|
139
|
+
switch (action.type) {
|
|
140
|
+
case "ConfigureKeypad":
|
|
141
|
+
const {keypadType} = action.configuration;
|
|
142
|
+
const {numPages} = keypadForType[keypadType];
|
|
143
|
+
return {
|
|
144
|
+
...state,
|
|
145
|
+
numPages,
|
|
146
|
+
animateToPosition: false,
|
|
147
|
+
currentPage: getDefaultPage(numPages),
|
|
148
|
+
dx: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
case "SetPageSize":
|
|
152
|
+
return {
|
|
153
|
+
...state,
|
|
154
|
+
pageWidthPx: action.pageWidthPx,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
case "PressKey":
|
|
158
|
+
const keyConfig = KeyConfigs[action.key];
|
|
159
|
+
|
|
160
|
+
// Reset the keypad page if the user performs a math operation.
|
|
161
|
+
if (
|
|
162
|
+
keyConfig.type === KeyTypes.VALUE ||
|
|
163
|
+
keyConfig.type === KeyTypes.OPERATOR
|
|
164
|
+
) {
|
|
165
|
+
return pagerReducer(state, {type: "ResetKeypadPage"});
|
|
166
|
+
}
|
|
167
|
+
return state;
|
|
168
|
+
|
|
169
|
+
case "ResetKeypadPage":
|
|
170
|
+
return {
|
|
171
|
+
...state,
|
|
172
|
+
animateToPosition: true,
|
|
173
|
+
// We start at the right-most page.
|
|
174
|
+
currentPage: getDefaultPage(state.numPages),
|
|
175
|
+
dx: 0,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
case "PageKeypadRight":
|
|
179
|
+
const nextPage = Math.min(
|
|
180
|
+
state.currentPage + 1,
|
|
181
|
+
state.numPages - 1,
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
...state,
|
|
185
|
+
animateToPosition: true,
|
|
186
|
+
currentPage: nextPage,
|
|
187
|
+
dx: 0,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
case "PageKeypadLeft":
|
|
191
|
+
const prevPage = Math.max(state.currentPage - 1, 0);
|
|
192
|
+
return {
|
|
193
|
+
...state,
|
|
194
|
+
animateToPosition: true,
|
|
195
|
+
currentPage: prevPage,
|
|
196
|
+
dx: 0,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
case "OnSwipeChange":
|
|
200
|
+
state.velocityTracker.push(action.dx);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
...state,
|
|
204
|
+
animateToPosition: false,
|
|
205
|
+
dx: action.dx,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
case "OnSwipeEnd":
|
|
209
|
+
const {pageWidthPx, velocityTracker} = state;
|
|
210
|
+
const {dx} = action;
|
|
211
|
+
const velocity = velocityTracker.getVelocity();
|
|
212
|
+
|
|
213
|
+
// NOTE(charlie): These will need refinement. The velocity comes
|
|
214
|
+
// from Framer.
|
|
215
|
+
const minFlingVelocity = 0.1;
|
|
216
|
+
const minFlingDistance = 10;
|
|
217
|
+
|
|
218
|
+
const shouldPageRight =
|
|
219
|
+
dx < -pageWidthPx / 2 ||
|
|
220
|
+
(velocity < -minFlingVelocity && dx < -minFlingDistance);
|
|
221
|
+
|
|
222
|
+
const shouldPageLeft =
|
|
223
|
+
dx > pageWidthPx / 2 ||
|
|
224
|
+
(velocity > minFlingVelocity && dx > minFlingDistance);
|
|
225
|
+
|
|
226
|
+
if (shouldPageRight) {
|
|
227
|
+
return pagerReducer(state, {type: "PageKeypadRight"});
|
|
228
|
+
} else if (shouldPageLeft) {
|
|
229
|
+
return pagerReducer(state, {type: "PageKeypadLeft"});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...state,
|
|
234
|
+
animateToPosition: true,
|
|
235
|
+
dx: 0,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
default:
|
|
239
|
+
return state;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const createGestureManager = (swipeEnabled) => {
|
|
244
|
+
return new GestureManager(
|
|
245
|
+
{
|
|
246
|
+
swipeEnabled,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
onSwipeChange: (dx) => {
|
|
250
|
+
store.dispatch({
|
|
251
|
+
type: "OnSwipeChange",
|
|
252
|
+
dx,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
onSwipeEnd: (dx) => {
|
|
256
|
+
store.dispatch({
|
|
257
|
+
type: "OnSwipeEnd",
|
|
258
|
+
dx,
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
onActiveNodesChanged: (activeNodes) => {
|
|
262
|
+
store.dispatch({
|
|
263
|
+
type: "SetActiveNodes",
|
|
264
|
+
activeNodes,
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
onClick: (key, layoutProps, inPopover) => {
|
|
268
|
+
store.dispatch({
|
|
269
|
+
type: "PressKey",
|
|
270
|
+
key,
|
|
271
|
+
...layoutProps,
|
|
272
|
+
inPopover,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
[],
|
|
277
|
+
[Keys.BACKSPACE, Keys.UP, Keys.RIGHT, Keys.DOWN, Keys.LEFT],
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const initialGestureState = {
|
|
282
|
+
popover: null,
|
|
283
|
+
focus: null,
|
|
284
|
+
gestureManager: createGestureManager(
|
|
285
|
+
keypadForType[defaultKeypadType].numPages > 1,
|
|
286
|
+
),
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const gestureReducer = function (state = initialGestureState, action) {
|
|
290
|
+
switch (action.type) {
|
|
291
|
+
case "DismissKeypad":
|
|
292
|
+
// NOTE(charlie): In the past, we enforced the "gesture manager
|
|
293
|
+
// will not receive any events when the keypad is hidden"
|
|
294
|
+
// assumption by assuming that the keypad would be hidden when
|
|
295
|
+
// dismissed and, as such, that none of its managed DOM nodes
|
|
296
|
+
// would be able to receive touch events. However, on mobile
|
|
297
|
+
// Safari, we're seeing that some of the keys receive touch
|
|
298
|
+
// events even when off-screen, inexplicably. So, to guard
|
|
299
|
+
// against that bug and make the contract explicit, we enable
|
|
300
|
+
// and disable event tracking on activation and dismissal.
|
|
301
|
+
state.gestureManager.disableEventTracking();
|
|
302
|
+
return state;
|
|
303
|
+
|
|
304
|
+
case "ActivateKeypad":
|
|
305
|
+
state.gestureManager.enableEventTracking();
|
|
306
|
+
return state;
|
|
307
|
+
|
|
308
|
+
case "SetActiveNodes":
|
|
309
|
+
return {
|
|
310
|
+
...state,
|
|
311
|
+
...action.activeNodes,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
case "ConfigureKeypad":
|
|
315
|
+
const {keypadType} = action.configuration;
|
|
316
|
+
const {numPages} = keypadForType[keypadType];
|
|
317
|
+
const swipeEnabled = numPages > 1;
|
|
318
|
+
return {
|
|
319
|
+
popover: null,
|
|
320
|
+
focus: null,
|
|
321
|
+
gestureManager: createGestureManager(swipeEnabled),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
default:
|
|
325
|
+
return state;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Used to generate unique animation IDs for the echo animations. The actual
|
|
330
|
+
// values are irrelevant as long as they are unique.
|
|
331
|
+
let _lastAnimationId = 0;
|
|
332
|
+
|
|
333
|
+
const initialEchoState = {
|
|
334
|
+
echoes: [],
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const echoReducer = function (state = initialEchoState, action) {
|
|
338
|
+
switch (action.type) {
|
|
339
|
+
case "PressKey":
|
|
340
|
+
const keyConfig = KeyConfigs[action.key];
|
|
341
|
+
|
|
342
|
+
// Add in the echo animation if the user performs a math
|
|
343
|
+
// operation.
|
|
344
|
+
if (
|
|
345
|
+
keyConfig.type === KeyTypes.VALUE ||
|
|
346
|
+
keyConfig.type === KeyTypes.OPERATOR
|
|
347
|
+
) {
|
|
348
|
+
return {
|
|
349
|
+
...state,
|
|
350
|
+
echoes: [
|
|
351
|
+
...state.echoes,
|
|
352
|
+
{
|
|
353
|
+
animationId: "" + _lastAnimationId++,
|
|
354
|
+
animationType: action.inPopover
|
|
355
|
+
? EchoAnimationTypes.LONG_FADE_ONLY
|
|
356
|
+
: EchoAnimationTypes.FADE_ONLY,
|
|
357
|
+
borders: action.borders,
|
|
358
|
+
id: keyConfig.id,
|
|
359
|
+
initialBounds: action.initialBounds,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return state;
|
|
365
|
+
|
|
366
|
+
case "RemoveEcho":
|
|
367
|
+
const remainingEchoes = state.echoes.filter((echo) => {
|
|
368
|
+
return echo.animationId !== action.animationId;
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
...state,
|
|
372
|
+
echoes: remainingEchoes,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
default:
|
|
376
|
+
return state;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const initialLayoutState = {
|
|
381
|
+
gridDimensions: {
|
|
382
|
+
numRows: keypadForType[defaultKeypadType].rows,
|
|
383
|
+
numColumns: keypadForType[defaultKeypadType].columns,
|
|
384
|
+
numMaxVisibleRows: keypadForType[defaultKeypadType].maxVisibleRows,
|
|
385
|
+
numPages: keypadForType[defaultKeypadType].numPages,
|
|
386
|
+
},
|
|
387
|
+
buttonDimensions: {
|
|
388
|
+
widthPx: 48,
|
|
389
|
+
heightPx: 48,
|
|
390
|
+
},
|
|
391
|
+
pageDimensions: {
|
|
392
|
+
pageWidthPx: 0,
|
|
393
|
+
pageHeightPx: 0,
|
|
394
|
+
},
|
|
395
|
+
layoutMode: LayoutModes.FULLSCREEN,
|
|
396
|
+
paginationEnabled: false,
|
|
397
|
+
navigationPadEnabled: false,
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Compute the additional layout state based on the provided page and grid
|
|
402
|
+
* dimensions.
|
|
403
|
+
*/
|
|
404
|
+
const layoutParametersForDimensions = (pageDimensions, gridDimensions) => {
|
|
405
|
+
const {pageWidthPx, pageHeightPx} = pageDimensions;
|
|
406
|
+
|
|
407
|
+
// Determine the device type and orientation.
|
|
408
|
+
const deviceOrientation =
|
|
409
|
+
pageWidthPx > pageHeightPx
|
|
410
|
+
? DeviceOrientations.LANDSCAPE
|
|
411
|
+
: DeviceOrientations.PORTRAIT;
|
|
412
|
+
const deviceType =
|
|
413
|
+
Math.min(pageWidthPx, pageHeightPx) > tabletCutoffPx
|
|
414
|
+
? DeviceTypes.TABLET
|
|
415
|
+
: DeviceTypes.PHONE;
|
|
416
|
+
|
|
417
|
+
// Using that information, make some decisions (or assumptions)
|
|
418
|
+
// about the resulting layout.
|
|
419
|
+
const navigationPadEnabled = deviceType === DeviceTypes.TABLET;
|
|
420
|
+
const paginationEnabled =
|
|
421
|
+
deviceType === DeviceTypes.PHONE &&
|
|
422
|
+
deviceOrientation === DeviceOrientations.PORTRAIT;
|
|
423
|
+
|
|
424
|
+
const deviceInfo = {deviceOrientation, deviceType};
|
|
425
|
+
const layoutOptions = {
|
|
426
|
+
navigationPadEnabled,
|
|
427
|
+
paginationEnabled,
|
|
428
|
+
// HACK(charlie): It's not great that we're making assumptions about
|
|
429
|
+
// the toolbar (which is rendered by webapp, and should always be
|
|
430
|
+
// visible and anchored to the bottom of the page for phone and
|
|
431
|
+
// tablet exercises). But this is primarily a heuristic (the goal is
|
|
432
|
+
// to preserve a 'good' amount of space between the top of the
|
|
433
|
+
// keypad and the top of the page) so we afford to have some margin
|
|
434
|
+
// of error.
|
|
435
|
+
toolbarEnabled: true,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
...computeLayoutParameters(
|
|
440
|
+
gridDimensions,
|
|
441
|
+
pageDimensions,
|
|
442
|
+
deviceInfo,
|
|
443
|
+
layoutOptions,
|
|
444
|
+
),
|
|
445
|
+
// Pass along some of the layout information, so that other
|
|
446
|
+
// components in the heirarchy can adapt appropriately.
|
|
447
|
+
navigationPadEnabled,
|
|
448
|
+
paginationEnabled,
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const layoutReducer = function (state = initialLayoutState, action) {
|
|
453
|
+
switch (action.type) {
|
|
454
|
+
case "ConfigureKeypad":
|
|
455
|
+
const {keypadType} = action.configuration;
|
|
456
|
+
const gridDimensions = {
|
|
457
|
+
numRows: keypadForType[keypadType].rows,
|
|
458
|
+
numColumns: keypadForType[keypadType].columns,
|
|
459
|
+
numMaxVisibleRows: keypadForType[keypadType].maxVisibleRows,
|
|
460
|
+
numPages: keypadForType[keypadType].numPages,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
...state,
|
|
465
|
+
...layoutParametersForDimensions(
|
|
466
|
+
state.pageDimensions,
|
|
467
|
+
gridDimensions,
|
|
468
|
+
),
|
|
469
|
+
gridDimensions,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
case "SetPageSize":
|
|
473
|
+
const {pageWidthPx, pageHeightPx} = action;
|
|
474
|
+
const pageDimensions = {pageWidthPx, pageHeightPx};
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
...state,
|
|
478
|
+
...layoutParametersForDimensions(
|
|
479
|
+
pageDimensions,
|
|
480
|
+
state.gridDimensions,
|
|
481
|
+
),
|
|
482
|
+
pageDimensions,
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
default:
|
|
486
|
+
return state;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const reducer = Redux.combineReducers({
|
|
491
|
+
input: inputReducer,
|
|
492
|
+
keypad: keypadReducer,
|
|
493
|
+
pager: pagerReducer,
|
|
494
|
+
gestures: gestureReducer,
|
|
495
|
+
echoes: echoReducer,
|
|
496
|
+
layout: layoutReducer,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// TODO(charlie): This non-inlined return is necessary so as to allow the
|
|
500
|
+
// gesture manager to dispatch actions on the store in its callbacks. We
|
|
501
|
+
// should come up with a better pattern to remove the two-way dependency.
|
|
502
|
+
const store = Redux.createStore(reducer);
|
|
503
|
+
|
|
504
|
+
return store;
|
|
505
|
+
};
|