diff --git a/BUILDING b/BUILDING index 33fc2f205..b60c43354 100644 --- a/BUILDING +++ b/BUILDING @@ -15,5 +15,9 @@ re-make, and also serve the demo, the unit tests, and the visual tests. unit tests -> http://localhost:9292/test/unit.html visual tests -> http://localhost:9292/test/visual.html +If building on Windows: + 1. Install GNU Make from http://gnuwin32.sourceforge.net/packages/make.htm. Do not use make derivitives from mSYS or MinGW. Ensure the location of make.exe is added to your PATH environment variable. + 2. Grab the latest Git for Windows from https://git-scm.com/download/win. When installing, add Git and its optional shell tools to your PATH environment variable (these questions are asked during setup). + If any of this does not work, please let us know! We want to make hacking on mathquill as easy as possible. diff --git a/Makefile b/Makefile index a2319d23b..46f811553 100644 --- a/Makefile +++ b/Makefile @@ -32,17 +32,22 @@ BASE_SOURCES = \ $(SRC_DIR)/cursor.js \ $(SRC_DIR)/controller.js \ $(SRC_DIR)/publicapi.js \ - $(SRC_DIR)/services/*.util.js \ - $(SRC_DIR)/services/*.js + $(SRC_DIR)/services/parser.util.js \ + $(SRC_DIR)/services/saneKeyboardEvents.util.js \ + $(SRC_DIR)/services/aria.js \ + $(SRC_DIR)/services/exportText.js \ + $(SRC_DIR)/services/focusBlur.js \ + $(SRC_DIR)/services/keystroke.js \ + $(SRC_DIR)/services/latex.js \ + $(SRC_DIR)/services/mouse.js \ + $(SRC_DIR)/services/scrollHoriz.js \ + $(SRC_DIR)/services/textarea.js SOURCES_FULL = \ $(BASE_SOURCES) \ $(SRC_DIR)/commands/math.js \ $(SRC_DIR)/commands/text.js \ $(SRC_DIR)/commands/math/*.js -# FIXME text.js currently depends on math.js (#435), restore these when fixed: -# $(SRC_DIR)/commands/*.js \ -# $(SRC_DIR)/commands/*/*.js SOURCES_BASIC = \ $(BASE_SOURCES) \ @@ -98,6 +103,7 @@ BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile .PHONY: all basic dev js uglify css font clean all: font css uglify basic: $(UGLY_BASIC_JS) $(BASIC_CSS) +unminified_basic: $(BASIC_JS) $(BASIC_CSS) # dev is like all, but without minification dev: font css js js: $(BUILD_JS) @@ -111,6 +117,7 @@ $(PJS_SRC): $(NODE_MODULES_INSTALLED) $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) @@ -118,6 +125,7 @@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) $(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_BASIC_JS): $(BASIC_JS) $(NODE_MODULES_INSTALLED) @@ -125,10 +133,12 @@ $(UGLY_BASIC_JS): $(BASIC_JS) $(NODE_MODULES_INSTALLED) $(BUILD_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) $(LESS_OPTS) $(CSS_MAIN) > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(BASIC_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) --modify-var="basic=true" $(LESS_OPTS) $(CSS_MAIN) > $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(NODE_MODULES_INSTALLED): package.json diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md index 60f6bb0b8..f532f5ff3 100644 --- a/docs/Api_Methods.md +++ b/docs/Api_Methods.md @@ -221,6 +221,26 @@ Simulates typing text, one character at a time from where the cursor currently i mathField.typedText('x=-b\\pm \\sqrt b^2 -4ac'); ``` +## .setAriaLabel(ariaLabel) + +Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'` + +## .getAriaLabel() + +Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned. + +## .setAriaPostLabel(ariaPostLabel, timeout) + +Specify a suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. Default: `''` (empty string) + +If a timeout (in ms) is supplied, and the math field has keyboard focus when the time has elapsed, an ARIA alert will fire which will cause a screen reader to read the content of the field along with the ARIA post-label. This is useful if the post-label contains an evaluation, error message, or other text that the user needs to know about. + +## .getAriaPostLabel() + +Returns the suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. If no ARIA post-label has been specified, `''` (empty string) is returned. + +[`aria-label`]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute + ## .config(new_config) Changes the [configuration](Config.md) of just this math field. diff --git a/docs/Config.md b/docs/Config.md index d47bc1a1c..b8125ef49 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -87,6 +87,7 @@ Just like [`autoCommands`](#autocommands) above, this takes a string formatted a `maxDepth` specifies the maximum number of nested MathBlocks. When `maxDepth` is set to 1, the user can type simple math symbols directly into the editor but not into nested MathBlocks, e.g. the numerator and denominator of a fraction. Nested content in latex rendered during initialization or pasted into mathquill is truncated to avoid violating `maxDepth`. When `maxDepth` is not set, no depth limit is applied by default. +You can also specify a speech-friendly representation of the operator name by supplying the operator name, a `|` and its speech alternative (separate multiple words with a `-`). For example, `'sin|sine cos|cosine tan|tangent sinh|hyperbolic-sine'`. ## substituteTextarea @@ -158,13 +159,3 @@ For example, to style as white-on-black instead of black-on-white use: border-color: white; background: black; } - #my-math-input .mq-matrixed { - background: black; - } - #my-math-input .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='black'); - } - -## Color Change Support on IE8 - -To support a MathQuill editable background color other than white in IE8, set the background color on both the editable mathField and on elements with class `mq-matrixed`. Then set a Chroma filter on elements with class `mq-matrixed-container`. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..489f69df9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,906 @@ +{ + "name": "mathquill", + "version": "0.10.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "optional": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "optional": true + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "optional": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true, + "optional": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "requires": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "2.81.0", + "source-map": "^0.5.3" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "optional": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "optional": true, + "requires": { + "mime-db": "~1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz", + "integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "pjs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pjs/-/pjs-4.0.0.tgz", + "integrity": "sha1-aMp9me0z1KZSuLe0P5lvOR71Efk=", + "dev": true + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "~2.0.3" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true, + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/package.json b/package.json index 9cb6c4b44..475a7c727 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "quickstart.html" ], "devDependencies": { - "pjs": ">=3.1.0 <5.0.0", + "less": ">=1.5.1 <3.0.0", "mocha": ">=2.4.1", - "uglify-js": "2.x", - "less": ">=1.5.1" + "pjs": ">=3.1.0 <5.0.0", + "uglify-js": "2.x" } } diff --git a/script/test_server.js b/script/test_server.js index 17a7420d9..b653eb424 100644 --- a/script/test_server.js +++ b/script/test_server.js @@ -84,8 +84,9 @@ function run_make_test() { if (code) { console.error('Exit Code ' + code); } else { - console.log('\nMathQuill is now running on localhost:9292'); - console.log('Open http://localhost:9292/test/demo.html\n'); + var serverAddress = HOST === '0.0.0.0' ? 'localhost:' + PORT : HOST + ':' + PORT; + console.log('\nMathQuill is now running on ' + serverAddress); + console.log('Open http://' + serverAddress + '/test/demo.html\n'); } for (var i = 0; i < q.length; i += 1) q[i](); q = undefined; diff --git a/src/commands/math.js b/src/commands/math.js index 50dc4a2ec..48bfd0e8d 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -12,19 +12,19 @@ var MathElement = P(Node, function(_, super_) { // SupSub::contactWeld, and is deliberately only passed in by writeLatex, // see ea7307eb4fac77c149a11ffdf9a831df85247693 var self = this; - self.postOrder('finalizeTree', options); - self.postOrder('contactWeld', cursor); + self.postOrder(function (node) { node.finalizeTree(options) }); + self.postOrder(function (node) { node.contactWeld(cursor) }); // note: this order is important. // empty elements need the empty box provided by blur to // be present in order for their dimensions to be measured // correctly by 'reflow' handlers. - self.postOrder('blur'); + self.postOrder(function (node) { node.blur(); }); - self.postOrder('reflow'); + self.postOrder(function (node) { node.reflow(); }); if (self[R].siblingCreated) self[R].siblingCreated(options, L); if (self[L].siblingCreated) self[L].siblingCreated(options, R); - self.bubble('reflow'); + self.bubble(function (node) { node.reflow(); }); }; // If the maxDepth option is set, make sure // deeply nested content is truncated. Just return @@ -145,6 +145,7 @@ var MathCommand = P(MathElement, function(_, super_) { _.moveTowards = function(dir, cursor, updown) { var updownInto = updown && this[updown+'Into']; cursor.insAtDirEnd(-dir, updownInto || this.ends[-dir]); + aria.queueDirEndOf(-dir).queue(cursor.parent, true); }; _.deleteTowards = function(dir, cursor) { if (this.isEmpty()) cursor[dir] = this.remove()[dir]; @@ -276,17 +277,19 @@ var MathCommand = P(MathElement, function(_, super_) { pray('no unmatched angle brackets', tokens.join('') === this.htmlTemplate); - // add cmdId to all top-level tags + // add cmdId and aria-hidden (for screen reader users) to all top-level tags + // Note: with the RegExp search/replace approach, it's possible that an element which is both a command and block may contain redundant aria-hidden attributes. + // In practice this doesn't appear to cause problems for screen readers. for (var i = 0, token = tokens[0]; token; i += 1, token = tokens[i]) { // top-level self-closing tags if (token.slice(-2) === '/>') { - tokens[i] = token.slice(0,-2) + cmdId + '/>'; + tokens[i] = token.slice(0,-2) + cmdId + ' aria-hidden="true"/>'; } // top-level open tags else if (token.charAt(0) === '<') { pray('not an unmatched top-level close tag', token.charAt(1) !== '/'); - tokens[i] = token.slice(0,-1) + cmdId + '>'; + tokens[i] = token.slice(0,-1) + cmdId + ' aria-hidden="true">'; // skip matching top-level close tag and all tag pairs in between var nesting = 1; @@ -305,7 +308,7 @@ var MathCommand = P(MathElement, function(_, super_) { } } return tokens.join('').replace(/>&(\d+)/g, function($0, $1) { - return ' mathquill-block-id=' + blocks[$1].id + '>' + blocks[$1].join('html'); + return ' mathquill-block-id=' + blocks[$1].id + ' aria-hidden="true">' + blocks[$1].join('html'); }); }; @@ -327,15 +330,24 @@ var MathCommand = P(MathElement, function(_, super_) { return text + child_text + (cmd.textTemplate[i] || ''); }); }; + _.mathspeakTemplate = []; + _.mathspeak = function() { + var cmd = this, i = 0; + return cmd.foldChildren(cmd.mathspeakTemplate[i] || 'Start'+cmd.ctrlSeq+' ', function(speech, block) { + i += 1; + return speech + ' ' + block.mathspeak() + ' ' + (cmd.mathspeakTemplate[i]+' ' || 'End'+cmd.ctrlSeq+' '); + }); + }; }); /** * Lightweight command without blocks or children. */ var Symbol = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, html, text) { - if (!text) text = ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : ctrlSeq; + _.init = function(ctrlSeq, html, text, mathspeak) { + if (!text && !!ctrlSeq) text = ctrlSeq.replace(/^\\/, ''); + this.mathspeakName = mathspeak || text; super_.init.call(this, ctrlSeq, html, [ text ]); }; @@ -351,6 +363,7 @@ var Symbol = P(MathCommand, function(_, super_) { cursor.jQ.insDirOf(dir, this.jQ); cursor[-dir] = this; cursor[dir] = this[dir]; + aria.queue(this); }; _.deleteTowards = function(dir, cursor) { cursor[dir] = this.remove()[dir]; @@ -364,19 +377,20 @@ var Symbol = P(MathCommand, function(_, super_) { }; _.latex = function(){ return this.ctrlSeq; }; - _.text = function(){ return this.textTemplate; }; + _.text = function(){ return this.textTemplate.join(''); }; + _.mathspeak = function(){ return this.mathspeakName; }; _.placeCursor = noop; _.isEmpty = function(){ return true; }; }); var VanillaSymbol = P(Symbol, function(_, super_) { - _.init = function(ch, html) { - super_.init.call(this, ch, ''+(html || ch)+''); + _.init = function(ch, html, mathspeak) { + super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak); }; }); var BinaryOperator = P(Symbol, function(_, super_) { - _.init = function(ctrlSeq, html, text) { + _.init = function(ctrlSeq, html, text, mathspeak) { super_.init.call(this, - ctrlSeq, ''+html+'', text + ctrlSeq, ''+html+'', text, mathspeak ); }; }); @@ -400,6 +414,45 @@ var MathBlock = P(MathElement, function(_, super_) { this.join('text') ; }; + _.mathspeak = function() { + var tempOp = ''; + var autoOps = {}; + if (this.controller) autoOps = this.controller.options.autoOperatorNames; + return this.foldChildren([], function(speechArray, cmd) { + if (cmd.isPartOfOperator) { + tempOp += cmd.mathspeak(); + } else { + if(tempOp!=='') { + if(autoOps !== {} && autoOps._maxLength > 0) { + var x = autoOps[tempOp.toLowerCase()]; + if(typeof x === 'string') tempOp = x; + } + speechArray.push(tempOp+' '); + tempOp = ''; + } + var mathspeakText = cmd.mathspeak(); + var cmdText = cmd.ctrlSeq; + if ( + isNaN(cmdText) && + cmdText !== '.' && + (!cmd.parent || !cmd.parent.parent || !cmd.parent.parent.isTextBlock()) + ) { + mathspeakText = ' ' + mathspeakText + ' '; + } + speechArray.push(mathspeakText); + } + return speechArray; + }) + .join('') + .replace(/ +(?= )/g,'') + // For Apple devices in particular, split out digits after a decimal point so they aren't read aloud as whole words. + // Not doing so makes 123.456 potentially spoken as "one hundred twenty-three point four hundred fifty-six." + // Instead, add spaces so it is spoken as "one hundred twenty-three point four five six." + .replace(/(\.)([0-9]+)/g, function(match, p1, p2) { + return p1 + p2.split('').join(' ').trim(); + }); + }; + _.ariaLabel = 'block'; _.keystroke = function(key, e, ctrlr) { if (ctrlr.options.spaceBehavesLikeTab @@ -416,8 +469,14 @@ var MathBlock = P(MathElement, function(_, super_) { // the cursor _.moveOutOf = function(dir, cursor, updown) { var updownInto = updown && this.parent[updown+'Into']; - if (!updownInto && this[dir]) cursor.insAtDirEnd(-dir, this[dir]); - else cursor.insDirOf(dir, this.parent); + if (!updownInto && this[dir]) { + cursor.insAtDirEnd(-dir, this[dir]); + aria.queueDirEndOf(-dir).queue(cursor.parent, true); + } + else { + cursor.insDirOf(dir, this.parent); + aria.queueDirOf(dir).queue(this.parent); + } }; _.selectOutOf = function(dir, cursor) { cursor.insDirOf(dir, this.parent); @@ -445,6 +504,8 @@ var MathBlock = P(MathElement, function(_, super_) { return LatexCmds['÷'](ch); else if (options && options.typingAsteriskWritesTimesSymbol && ch === '*') return LatexCmds['×'](ch); + else if (options && options.typingPercentWritesPercentOf && ch === '%') + return LatexCmds.percentof(ch); else if (cons = CharCmds[ch] || LatexCmds[ch]) return cons(ch); else @@ -455,6 +516,12 @@ var MathBlock = P(MathElement, function(_, super_) { if (cursor.selection) cmd.replaces(cursor.replaceSelection()); if (!cursor.isTooDeep()) { cmd.createLeftOf(cursor.show()); + // special-case the slash so that fractions are voiced while typing + if (ch === '/') { + aria.alert('over'); + } else { + aria.alert(cmd.mathspeak({ createdLeftOf: cursor })); + } } }; @@ -473,7 +540,7 @@ var MathBlock = P(MathElement, function(_, super_) { block.finalizeInsert(cursor.options, cursor); if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); - cursor.parent.bubble('reflow'); + cursor.parent.bubble(function (node) { node.reflow(); }); } }; @@ -485,9 +552,14 @@ var MathBlock = P(MathElement, function(_, super_) { }; _.blur = function() { this.jQ.removeClass('mq-hasCursor'); - if (this.isEmpty()) + if (this.isEmpty()) { this.jQ.addClass('mq-empty'); - + if (this.isEmptyParens()) { + this.jQ.addClass('mq-empty-parens'); + } else if (this.isEmptySquareBrackets()) { + this.jQ.addClass('mq-empty-square-brackets'); + } + } return this; }; }); @@ -507,17 +579,30 @@ API.StaticMath = function(APIClasses) { }; _.init = function() { super_.init.apply(this, arguments); - this.__controller.root.postOrder( - 'registerInnerField', this.innerFields = [], APIClasses.InnerMathField); + var innerFields = this.innerFields = []; + this.__controller.root.postOrder(function (node) { + node.registerInnerField(innerFields, APIClasses.InnerMathField); + }); }; _.latex = function() { var returned = super_.latex.apply(this, arguments); if (arguments.length > 0) { - this.__controller.root.postOrder( - 'registerInnerField', this.innerFields = [], APIClasses.InnerMathField); + var innerFields = this.innerFields = []; + this.__controller.root.postOrder(function (node) { + node.registerInnerField(innerFields, APIClasses.InnerMathField); + }); + // Force an ARIA label update to remain in sync with the new LaTeX value. + this.__controller.updateMathspeak(); } return returned; }; + _.setAriaLabel = function(ariaLabel) { + this.__controller.setAriaLabel(ariaLabel); + return this; + }; + _.getAriaLabel = function () { + return this.__controller.getAriaLabel(); + }; }); }; diff --git a/src/commands/math/LatexCommandInput.js b/src/commands/math/LatexCommandInput.js index 19dfc82a6..dca839ab7 100644 --- a/src/commands/math/LatexCommandInput.js +++ b/src/commands/math/LatexCommandInput.js @@ -30,15 +30,24 @@ CharCmds['\\'] = P(MathCommand, function(_, super_) { this.ends[L].write = function(cursor, ch) { cursor.show().deleteSelection(); - if (ch.match(/[a-z]/i)) VanillaSymbol(ch).createLeftOf(cursor); + if (ch.match(/[a-z]/i)) { + VanillaSymbol(ch).createLeftOf(cursor); + // TODO needs tests + aria.alert(ch); + } else { - this.parent.renderCommand(cursor); + var cmd = this.parent.renderCommand(cursor); + // TODO needs tests + aria.queue(cmd.mathspeak({ createdLeftOf: cursor })); if (ch !== '\\' || !this.isEmpty()) cursor.parent.write(cursor, ch); + else aria.alert(); } }; this.ends[L].keystroke = function(key, e, ctrlr) { if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') { - this.parent.renderCommand(ctrlr.cursor); + var cmd = this.parent.renderCommand(ctrlr.cursor); + // TODO needs tests + aria.alert(cmd.mathspeak({ createdLeftOf: ctrlr.cursor })); e.preventDefault(); return; } @@ -88,6 +97,7 @@ CharCmds['\\'] = P(MathCommand, function(_, super_) { if (this._replacedFragment) this._replacedFragment.remove(); } + return cmd; }; }); diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js index 1cd72f042..865edfc9c 100644 --- a/src/commands/math/advancedSymbols.js +++ b/src/commands/math/advancedSymbols.js @@ -12,55 +12,52 @@ LatexCmds.otimes = P(BinaryOperator, function(_, super_) { }; }); -LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠'); - LatexCmds['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = - bind(BinaryOperator,'\\ast ','∗'); - + bind(BinaryOperator,'\\ast ','∗', 'low asterisk'); LatexCmds.therefor = LatexCmds.therefore = - bind(BinaryOperator,'\\therefore ','∴'); + bind(BinaryOperator,'\\therefore ','∴', 'therefore'); LatexCmds.cuz = // l33t -LatexCmds.because = bind(BinaryOperator,'\\because ','∵'); +LatexCmds.because = bind(BinaryOperator,'\\because ','∵', 'because'); -LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝'); +LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝', 'proportional to'); -LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈'); +LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈'), 'approximately equal to'; -LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈'); +LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈', 'is in'); -LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋'); +LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋', 'is not in'); LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain = - bind(BinaryOperator,'\\not\\ni ','∌'); + bind(BinaryOperator,'\\not\\ni ','∌', 'does not contain'); -LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂'); +LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂', 'subset'); LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = - bind(BinaryOperator,'\\supset ','⊃'); + bind(BinaryOperator,'\\supset ','⊃', 'superset'); LatexCmds.nsub = LatexCmds.notsub = LatexCmds.nsubset = LatexCmds.notsubset = - bind(BinaryOperator,'\\not\\subset ','⊄'); + bind(BinaryOperator,'\\not\\subset ','⊄', 'not a subset'); LatexCmds.nsup = LatexCmds.notsup = LatexCmds.nsupset = LatexCmds.notsupset = LatexCmds.nsuperset = LatexCmds.notsuperset = - bind(BinaryOperator,'\\not\\supset ','⊅'); + bind(BinaryOperator,'\\not\\supset ','⊅', 'not a superset'); LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq = - bind(BinaryOperator,'\\subseteq ','⊆'); + bind(BinaryOperator,'\\subseteq ','⊆', 'subset or equal to'); LatexCmds.supe = LatexCmds.supeq = LatexCmds.supsete = LatexCmds.supseteq = LatexCmds.supersete = LatexCmds.superseteq = - bind(BinaryOperator,'\\supseteq ','⊇'); + bind(BinaryOperator,'\\supseteq ','⊇', 'superset or equal to'); LatexCmds.nsube = LatexCmds.nsubeq = LatexCmds.notsube = LatexCmds.notsubeq = LatexCmds.nsubsete = LatexCmds.nsubseteq = LatexCmds.notsubsete = LatexCmds.notsubseteq = - bind(BinaryOperator,'\\not\\subseteq ','⊈'); + bind(BinaryOperator,'\\not\\subseteq ','⊈', 'not subset or equal to'); LatexCmds.nsupe = LatexCmds.nsupeq = LatexCmds.notsupe = LatexCmds.notsupeq = @@ -68,7 +65,7 @@ LatexCmds.nsupsete = LatexCmds.nsupseteq = LatexCmds.notsupsete = LatexCmds.notsupseteq = LatexCmds.nsupersete = LatexCmds.nsuperseteq = LatexCmds.notsupersete = LatexCmds.notsuperseteq = - bind(BinaryOperator,'\\not\\supseteq ','⊉'); + bind(BinaryOperator,'\\not\\supseteq ','⊉', 'not superset or equal to'); //the canonical sets of numbers LatexCmds.mathbb = P(MathCommand, function(_) { @@ -91,256 +88,251 @@ LatexCmds.mathbb = P(MathCommand, function(_) { }); LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = - bind(VanillaSymbol,'\\mathbb{N}','ℕ'); + bind(VanillaSymbol,'\\mathbb{N}','ℕ', 'naturals'); LatexCmds.P = LatexCmds.primes = LatexCmds.Primes = LatexCmds.projective = LatexCmds.Projective = LatexCmds.probability = LatexCmds.Probability = - bind(VanillaSymbol,'\\mathbb{P}','ℙ'); + bind(VanillaSymbol,'\\mathbb{P}','ℙ', 'P'); LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = - bind(VanillaSymbol,'\\mathbb{Z}','ℤ'); + bind(VanillaSymbol,'\\mathbb{Z}','ℤ', 'integers'); LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals = - bind(VanillaSymbol,'\\mathbb{Q}','ℚ'); + bind(VanillaSymbol,'\\mathbb{Q}','ℚ', 'rationals'); LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = - bind(VanillaSymbol,'\\mathbb{R}','ℝ'); + bind(VanillaSymbol,'\\mathbb{R}','ℝ', 'reals'); LatexCmds.C = LatexCmds.complex = LatexCmds.Complex = LatexCmds.complexes = LatexCmds.Complexes = LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane = - bind(VanillaSymbol,'\\mathbb{C}','ℂ'); + bind(VanillaSymbol,'\\mathbb{C}','ℂ', 'complexes'); LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions = - bind(VanillaSymbol,'\\mathbb{H}','ℍ'); + bind(VanillaSymbol,'\\mathbb{H}','ℍ', 'quaternions'); //spacing -LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' '); -LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' '); +LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' ', '4 spaces'); +LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' ', '8 spaces'); /* spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow case ',': - return VanillaSymbol('\\, ',' '); + return VanillaSymbol('\\, ',' ', 'comma'); case ':': - return VanillaSymbol('\\: ',' '); + return VanillaSymbol('\\: ',' ', 'colon'); case ';': - return VanillaSymbol('\\; ',' '); + return VanillaSymbol('\\; ',' ', 'semicolon'); case '!': - return Symbol('\\! ',''); + return Symbol('\\! ','', 'exclamation point'); */ //binary operators -LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇'); -LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△'); -LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖'); -LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎'); -LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽'); -LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓'); -LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲'); -LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔'); -LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳'); +LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇', 'diamond'); +LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△', 'triangle up'); +LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖', 'o minus'); +LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎', 'disjoint union'); +LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽', 'triangle down'); +LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓', 'greatest lower bound'); +LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲', 'triangle left'); +LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔', 'least upper bound'); +LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳', 'triangle right'); //circledot is not a not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details -LatexCmds.odot = LatexCmds.circledot = bind(VanillaSymbol, '\\odot ', '⊙'); -LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯'); -LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '†'); -LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '‡'); -LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀'); -LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐'); +LatexCmds.odot = LatexCmds.circledot = bind(VanillaSymbol, '\\odot ', '⊙', 'circle dot'); +LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯', 'circle'); +LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '†', 'dagger'); +LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '‡', 'big dagger'); +LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀', 'wreath'); +LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐', 'amalgam'); //relationship symbols -LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨'); -LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺'); -LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻'); -LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼'); -LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽'); -LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃'); -LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣'); -LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪'); -LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫'); -LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥'); -LatexCmds.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦'); -LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈'); -LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏'); -LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐'); -LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣'); -LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑'); -LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒'); -LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐'); -LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢'); -LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦'); -LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣'); -LatexCmds.nless = bind(VanillaSymbol, '\\nless ', '≮'); -LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯'); +LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨', 'models'); +LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺', 'precedes'); +LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻', 'succeeds'); +LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼', 'precedes or equals'); +LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽', 'succeeds or equals'); +LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃', 'similar or equal to'); +LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣', 'divides'); +LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪', 'll'); +LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫', 'gg'); +LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥', 'parallel with'); +LatexCmds.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel with'); +LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈', 'bowtie'); +LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏', 'square subset'); +LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐', 'square superset'); +LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣', 'smile'); +LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑', 'square subset or equal to'); +LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒', 'square superset or equal to'); +LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐', 'dotted equals'); +LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢', 'frown'); +LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦', 'v dash'); +LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣', 'dash v'); +LatexCmds.nless = bind(VanillaSymbol, '\\nless ', '≮', 'not less than'); +LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯', 'not greater than'); //arrows -LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←'); -LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→'); -LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐'); -LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒'); -LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔'); -LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕'); -LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔'); -LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕'); -LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦'); -LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗'); -LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩'); -LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪'); -LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘'); -LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼'); -LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀'); -LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙'); -LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽'); -LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁'); -LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖'); +LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←', 'left arrow'); +LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→', 'right arrow'); +LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐', 'left arrow'); +LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒', 'right arrow'); +LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔', 'left and right arrow'); +LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕', 'up and down arrow'); +LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔', 'left and right arrow'); +LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕', 'up and down arrow'); +LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦', 'maps to'); +LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗', 'northeast arrow'); +LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩', 'hook left arrow'); +LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪', 'hook right arrow'); +LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘', 'southeast arrow'); +LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼', 'left harpoon up'); +LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀', 'right harpoon up'); +LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙', 'southwest arrow'); +LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽', 'left harpoon down'); +LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁', 'right harpoon down'); +LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖', 'northwest arrow'); //Misc -LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…'); -LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯'); -LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮'); -LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱'); -LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√'); -LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '△'); -LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ'); -LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤'); -LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭'); -LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮'); -LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯'); -LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘'); -LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥'); -LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣'); -LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢'); -LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡'); -LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠'); +LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…', 'l dots'); +LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯', 'c dots'); +LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮', 'v dots'); +LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱', 'd dots'); +LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√', 'unresolved root'); +LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '△', 'triangle'); +LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ', 'ell'); +LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤', 'top'); +LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭', 'flat'); +LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮', 'natural'); +LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯', 'sharp'); +LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘', 'wp'); +LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥', 'bot'); +LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣', 'club suit'); +LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢', 'diamond suit'); +LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡', 'heart suit'); +LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠', 'spade suit'); //not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details -LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '▱'); -LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜'); +LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '▱', 'parallelogram'); +LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜', 'square'); //variable-sized -LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮'); -LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩'); -LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪'); -LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔'); -LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨'); -LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧'); -LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙'); -LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗'); -LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕'); -LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎'); +LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮', 'o int'); +LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩', 'big cap'); +LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪', 'big cup'); +LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔', 'big square cup'); +LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨', 'big vee'); +LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧', 'big wedge'); +LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙', 'big o dot'); +LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗', 'big o times'); +LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕', 'big o plus'); +LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎', 'big u plus'); //delimiters -LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊'); -LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋'); -LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈'); -LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉'); -LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{'); -LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}'); -LatexCmds.lbrack = bind(VanillaSymbol, '['); -LatexCmds.rbrack = bind(VanillaSymbol, ']'); +LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊', 'left floor'); +LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋', 'right floor'); +LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈', 'left ceiling'); +LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉', 'right ceiling'); +LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{', 'left brace'); +LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}', 'right brace'); +LatexCmds.lbrack = bind(VanillaSymbol, '[', 'left bracket'); +LatexCmds.rbrack = bind(VanillaSymbol, ']', 'right bracket'); //various symbols -LatexCmds.slash = bind(VanillaSymbol, '/'); -LatexCmds.vert = bind(VanillaSymbol,'|'); -LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥'); +LatexCmds.slash = bind(VanillaSymbol, '/', 'slash'); +LatexCmds.vert = bind(VanillaSymbol,'|', 'vertical bar'); +LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥', 'perpendicular'); LatexCmds.nabla = LatexCmds.del = bind(VanillaSymbol,'\\nabla ','∇'); -LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ'); +LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ', 'horizontal bar'); LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom = - bind(VanillaSymbol,'\\text\\AA ','Å'); + bind(VanillaSymbol,'\\text\\AA ','Å', 'AA'); LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = - bind(VanillaSymbol,'\\circ ','∘'); + bind(VanillaSymbol,'\\circ ','∘', 'circle'); -LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•'); +LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•', 'bullet'); LatexCmds.setminus = LatexCmds.smallsetminus = - bind(VanillaSymbol,'\\setminus ','∖'); + bind(VanillaSymbol,'\\setminus ','∖', 'set minus'); -LatexCmds.not = //bind(Symbol,'\\not ','/'); -LatexCmds['¬'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬'); +LatexCmds.not = //bind(Symbol,'\\not ','/', 'not'); +LatexCmds['¬'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬', 'not'); LatexCmds['…'] = LatexCmds.dots = LatexCmds.ellip = LatexCmds.hellip = LatexCmds.ellipsis = LatexCmds.hellipsis = - bind(VanillaSymbol,'\\dots ','…'); + bind(VanillaSymbol,'\\dots ','…', 'ellipsis'); LatexCmds.converges = LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow = - bind(VanillaSymbol,'\\downarrow ','↓'); + bind(VanillaSymbol,'\\downarrow ','↓', 'converges with'); LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow = - bind(VanillaSymbol,'\\Downarrow ','⇓'); + bind(VanillaSymbol,'\\Downarrow ','⇓', 'down arrow'); LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = - bind(VanillaSymbol,'\\uparrow ','↑'); + bind(VanillaSymbol,'\\uparrow ','↑', 'diverges from'); -LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑'); +LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑', 'up arrow'); -LatexCmds.to = bind(BinaryOperator,'\\to ','→'); +LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→', 'right arrow'); -LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→'); +LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒', 'implies'); -LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒'); +LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒', 'right arrow'); -LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒'); +LatexCmds.gets = bind(BinaryOperator,'\\gets ','←', 'gets'); -LatexCmds.gets = bind(BinaryOperator,'\\gets ','←'); +LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←', 'left arrow'); -LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←'); +LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐', 'implied by'); -LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐'); - -LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐'); +LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐', 'left arrow'); LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow = - bind(VanillaSymbol,'\\leftrightarrow ','↔'); + bind(VanillaSymbol,'\\leftrightarrow ','↔', 'left and right arrow'); -LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔'); +LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔', 'if and only if'); LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow = - bind(VanillaSymbol,'\\Leftrightarrow ','⇔'); + bind(VanillaSymbol,'\\Leftrightarrow ','⇔', 'left and right arrow'); -LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ'); +LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ', 'real'); LatexCmds.Im = LatexCmds.imag = LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = - bind(VanillaSymbol,'\\Im ','ℑ'); - -LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂'); + bind(VanillaSymbol,'\\Im ','ℑ', 'imaginary'); -LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = - bind(VanillaSymbol,'\\infty ','∞'); +LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂', 'partial'); LatexCmds.pounds = bind(VanillaSymbol,'\\pounds ','£'); LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym = - bind(VanillaSymbol,'\\aleph ','ℵ'); + bind(VanillaSymbol,'\\aleph ','ℵ', 'alef sym'); LatexCmds.xist = //LOL LatexCmds.xists = LatexCmds.exist = LatexCmds.exists = - bind(VanillaSymbol,'\\exists ','∃'); + bind(VanillaSymbol,'\\exists ','∃', 'there exists at least 1'); LatexCmds.nexists = LatexCmds.nexist = - bind(VanillaSymbol, '\\nexists ', '∄'); + bind(VanillaSymbol, '\\nexists ', '∄', 'there is no'); LatexCmds.and = LatexCmds.land = LatexCmds.wedge = - bind(BinaryOperator,'\\wedge ','∧'); + bind(BinaryOperator,'\\wedge ','∧', 'and'); -LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\vee ','∨'); +LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\vee ','∨', 'or'); LatexCmds.o = LatexCmds.O = LatexCmds.empty = LatexCmds.emptyset = LatexCmds.oslash = LatexCmds.Oslash = LatexCmds.nothing = LatexCmds.varnothing = - bind(BinaryOperator,'\\varnothing ','∅'); + bind(BinaryOperator,'\\varnothing ','∅', 'nothing'); -LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪'); +LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪', 'union'); LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection = - bind(BinaryOperator,'\\cap ','∩'); + bind(BinaryOperator,'\\cap ','∩', 'intersection'); // FIXME: the correct LaTeX would be ^\circ but we can't parse that -LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°'); +LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°', 'degrees'); -LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠'); -LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡'); +LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠', 'angle'); +LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡', 'measured angle'); diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 49198a404..45b16c62d 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -1,8 +1,160 @@ /********************************* * Symbols for Basic Mathematics ********************************/ +var DigitGroupingChar = P(Symbol, function(_, super_) { + _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) { + // don't try to fix digit grouping if the sibling to my right changed (dir === R or + // undefined) and it's now a DigitGroupingChar, it will try to fix grouping + if (dir !== L && this[R] instanceof DigitGroupingChar) return; + this.fixDigitGrouping(opts); + }; + + _.fixDigitGrouping = function (opts) { + if (!opts.enableDigitGrouping) return; + + var left = this; + var right = this; + + var spacesFound = 0; + var dots = []; + + var SPACE = '\\ '; + var DOT = '.'; + + // traverse left as far as possible (starting at this char) + var node = left; + do { + if (/^[0-9]$/.test(node.ctrlSeq)) { + left = node + } else if (node.ctrlSeq === SPACE) { + left = node + spacesFound += 1; + } else if (node.ctrlSeq === DOT) { + left = node + dots.push(node); + } else { + break; + } + } while (node = left[L]); + + // traverse right as far as possible (starting to right of this char) + while (node = right[R]) { + if (/^[0-9]$/.test(node.ctrlSeq)) { + right = node + } else if (node.ctrlSeq === SPACE) { + right = node + spacesFound += 1; + } else if (node.ctrlSeq === DOT) { + right = node + dots.push(node); + } else { + break; + } + } + + // trim the leading spaces + while (right !== left && left.ctrlSeq === SPACE) { + left = left[R]; + spacesFound -= 1; + } + + // trim the trailing spaces + while (right !== left && right.ctrlSeq === SPACE) { + right = right[L]; + spacesFound -= 1; + } + + // happens when you only have a space + if (left === right && left.ctrlSeq === SPACE) return; + + var disableFormatting = spacesFound > 0 || dots.length > 1; + if (disableFormatting) { + this.removeGroupingBetween(left, right); + } else if (dots[0]) { + if (dots[0] !== left) { + this.addGroupingBetween(dots[0][L], left); + } + if (dots[0] !== right) { + // we do not show grouping to the right of a decimal place #yet + this.removeGroupingBetween(dots[0][R], right); + } + } else { + this.addGroupingBetween(right, left); + } + }; + + _.removeGroupingBetween = function (left, right) { + var node = left; + do { + node.setGroupingClass(undefined); + if (node === right) break; + } while (node = node[R]); + }; + + _.addGroupingBetween = function (start, end) { + var node = start; + var count = 0; + + var totalDigits = 0; + var node = start; + while (node) { + totalDigits += 1; + + if (node === end) break; + node = node[L]; + } + + var numDigitsInFirstGroup = totalDigits % 3; + if (numDigitsInFirstGroup === 0) numDigitsInFirstGroup = 3; + + var node = start; + while (node) { + count += 1; + + var cls = undefined; + + // only do grouping if we have at least 4 numbers + if (totalDigits >= 4) { + if (count === totalDigits) { + cls = 'mq-group-leading-' + numDigitsInFirstGroup; + } else if (count % 3 === 0) { + if (count !== totalDigits) { + cls = 'mq-group-start' + } + } + + if (!cls) { + cls = 'mq-group-other' + } + } + + node.setGroupingClass(cls); + + if (node === end) break; + node = node[L]; + } + }; + + _.setGroupingClass = function (cls) { + // nothing changed (either class is the same or it's still undefined) + if (this._groupingClass === cls) return; + + // remove existing class + if (this._groupingClass) this.jQ.removeClass(this._groupingClass); + + // add new class + if (cls) this.jQ.addClass(cls); + + // cache the groupingClass + this._groupingClass = cls; + } +}); + +var Digit = P(DigitGroupingChar, function(_, super_) { + _.init = function(ch, html, mathspeak) { + super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak); + }; -var Digit = P(VanillaSymbol, function(_, super_) { _.createLeftOf = function(cursor) { if (cursor.options.autoSubscriptNumerals && cursor.parent !== cursor.parent.parent.sub @@ -16,6 +168,20 @@ var Digit = P(VanillaSymbol, function(_, super_) { } else super_.createLeftOf.call(this, cursor); }; + _.mathspeak = function(opts) { + if (opts && opts.createdLeftOf) { + var cursor = opts.createdLeftOf; + if (cursor.options.autoSubscriptNumerals + && cursor.parent !== cursor.parent.parent.sub + && ((cursor[L] instanceof Variable && cursor[L].isItalic !== false) + || (cursor[L] instanceof SupSub + && cursor[L][L] instanceof Variable + && cursor[L][L].isItalic !== false))) { + return 'Subscript ' + super_.mathspeak.call(this) + ' Baseline'; + } + } + return super_.mathspeak.apply(this, arguments); + }; }); var Variable = P(Symbol, function(_, super_) { @@ -42,6 +208,23 @@ var Variable = P(Symbol, function(_, super_) { } return text; }; + _.mathspeak = function() { + var text = this.ctrlSeq; + if ( + this.isPartOfOperator || + text.length > 1 || + (this.parent && this.parent.parent && this.parent.parent.isTextBlock()) + ) { + return super_.mathspeak.call(this); + } else { + // Apple voices in VoiceOver (such as Alex, Bruce, and Victoria) do + // some strange pronunciation given certain expressions, + // e.g. "y-2" is spoken as "ee minus 2" (as if the y is short). + // Not an ideal solution, but surrounding non-numeric text blocks with quotation marks works. + // This bug has been acknowledged by Apple. + return '"'+text+'"'; + } + }; }); Options.p.autoCommands = { _maxLength: 0 }; @@ -65,10 +248,28 @@ optionProcessors.autoCommands = function(cmds) { return dict; }; +Options.p.autoParenthesizedFunctions = {_maxLength: 0}; +optionProcessors.autoParenthesizedFunctions = function (cmds) { + if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) { + throw '"'+cmds+'" not a space-delimited list of only letters'; + } + var list = cmds.split(' '), dict = {}, maxLength = 0; + for (var i = 0; i < list.length; i += 1) { + var cmd = list[i]; + if (cmd.length < 2) { + throw 'autocommand "'+cmd+'" not minimum length of 2'; + } + dict[cmd] = 1; + maxLength = max(maxLength, cmd.length); + } + dict._maxLength = maxLength; + return dict; +} + var Letter = P(Variable, function(_, super_) { _.init = function(ch) { return super_.init.call(this, this.letter = ch); }; - _.createLeftOf = function(cursor) { - super_.createLeftOf.apply(this, arguments); + _.checkAutoCmds = function (cursor) { + //handle autoCommands var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength; if (maxLength > 0) { // want longest possible autocommand, so join together longest @@ -89,6 +290,43 @@ var Letter = P(Variable, function(_, super_) { str = str.slice(1); } } + } + + _.autoParenthesize = function (cursor) { + //exit early if already parenthesized + var right = cursor.parent.ends[R] + if (right && right instanceof Bracket && right.ctrlSeq === '\\left(') { + return + } + + //exit early if in simple subscript + if (this.isParentSimpleSubscript()) { + return; + } + + //handle autoParenthesized functions + var str = '', l = this, i = 0; + + var autoParenthesizedFunctions = cursor.options.autoParenthesizedFunctions, maxLength = autoParenthesizedFunctions._maxLength; + var autoOperatorNames = cursor.options.autoOperatorNames + while (l instanceof Letter && i < maxLength) { + str = l.letter + str, l = l[L], i += 1; + } + // check for an autoParenthesized functions, going thru substrings longest to shortest + // only allow autoParenthesized functions that are also autoOperatorNames + while (str.length) { + if (autoParenthesizedFunctions.hasOwnProperty(str) && autoOperatorNames.hasOwnProperty(str)) { + return cursor.parent.write(cursor, '('); + } + str = str.slice(1); + } + } + + _.createLeftOf = function(cursor) { + super_.createLeftOf.apply(this, arguments); + + this.checkAutoCmds(cursor); + this.autoParenthesize(cursor); }; _.italicize = function(bool) { this.isItalic = bool; @@ -105,6 +343,12 @@ var Letter = P(Variable, function(_, super_) { _.autoUnItalicize = function(opts) { var autoOps = opts.autoOperatorNames; if (autoOps._maxLength === 0) return; + + //exit early if in simple subscript + if (this.isParentSimpleSubscript()) { + return; + } + // want longest possible operator names, so join together entire contiguous // sequence of letters var str = this.letter; @@ -157,8 +401,19 @@ var Letter = P(Variable, function(_, super_) { } }; function shouldOmitPadding(node) { - // omit padding if no node, or if node already has padding (to avoid double-padding) - return !node || (node instanceof BinaryOperator) || (node instanceof SummationNotation); + // omit padding if no node + if (!node) return true; + + // do not add padding between letter and '.' + if (node.ctrlSeq === '.') return true; + + // do not add padding between letter and binary operator. The + // binary operator already has padding + if (node instanceof BinaryOperator) return true; + + if (node instanceof SummationNotation) return true; + + return false; } }); var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that @@ -199,9 +454,13 @@ var TwoWordOpNames = { limsup: 1, liminf: 1, projlim: 1, injlim: 1 }; AutoOpNames[moreNonstandardOps[i]] = 1; } }()); + optionProcessors.autoOperatorNames = function(cmds) { - if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) { - throw '"'+cmds+'" not a space-delimited list of only letters'; + if(typeof cmds !== 'string') { + throw '"'+cmds+'" not a space-delimited list'; + } + if (!/^[a-z\|\-]+(?: [a-z\|\-]+)*$/i.test(cmds)) { + throw '"'+cmds+'" not a space-delimited list of letters or "|"'; } var list = cmds.split(' '), dict = {}, maxLength = 0; for (var i = 0; i < list.length; i += 1) { @@ -209,8 +468,21 @@ optionProcessors.autoOperatorNames = function(cmds) { if (cmd.length < 2) { throw '"'+cmd+'" not minimum length of 2'; } - dict[cmd] = 1; - maxLength = max(maxLength, cmd.length); + if(cmd.indexOf('|') < 0) { // normal auto operator + dict[cmd] = cmd; + maxLength = max(maxLength, cmd.length); + } + else { // this item has a speech-friendly alternative + var cmdArray = cmd.split('|'); + if(cmdArray.length > 2) { + throw '"'+cmd+'" has more than 1 mathspeak delimiter'; + } + if (cmdArray[0].length < 2) { + throw '"'+cmd[0]+'" not minimum length of 2'; + } + dict[cmdArray[0]] = cmdArray[1].replace(/-/g, ' '); // convert dashes to spaces for the sake of speech + maxLength = max(maxLength, cmdArray[0].length); + } } dict._maxLength = maxLength; return dict; @@ -239,7 +511,23 @@ LatexCmds.operatorname = P(MathCommand, function(_) { _.createLeftOf = noop; _.numBlocks = function() { return 1; }; _.parser = function() { - return latexMathParser.block.map(function(b) { return b.children(); }); + return latexMathParser.block.map(function(b) { + // Check for the special case of \operatorname{ans}, which has + // a special html representation + var isAllLetters = true; + var str = ''; + var children = b.children(); + children.each(function(child) { + if (child instanceof Letter) { + str += child.letter; + } else { + isAllLetters = false; + } + }); + if (isAllLetters && str === 'ans') return LatexCmds[str](str); + // In cases other than `ans`, just return the children directly + return children; + }); }; }); @@ -254,15 +542,28 @@ LatexCmds.f = P(Letter, function(_, super_) { }); // VanillaSymbol's -LatexCmds[' '] = LatexCmds.space = bind(VanillaSymbol, '\\ ', ' '); +LatexCmds[' '] = LatexCmds.space = P(DigitGroupingChar, function(_, super_) { + _.init = function () { + super_.init.call(this, '\\ ', ' ', ' '); + }; +}); -LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′'); -LatexCmds['″'] = LatexCmds.dprime = bind(VanillaSymbol, '″', '″'); +LatexCmds['.'] = P(DigitGroupingChar, function(_, super_) { + _.init = function () { + super_.init.call(this, '.', '.', '.'); + }; +}); -LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\'); +LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′', 'prime'); +LatexCmds['″'] = LatexCmds.dprime = bind(VanillaSymbol, '″', '″', 'double prime'); + +LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\', 'backslash'); if (!CharCmds['\\']) CharCmds['\\'] = LatexCmds.backslash; -LatexCmds.$ = bind(VanillaSymbol, '\\$', '$'); +LatexCmds.$ = bind(VanillaSymbol, '\\$', '$', 'dollar'); + +LatexCmds.square = bind(VanillaSymbol, '\\square ', '\u25A1', 'square'); +LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '\u2223', 'mid'); // does not use Symbola font var NonSymbolaSymbol = P(Symbol, function(_, super_) { @@ -272,8 +573,36 @@ var NonSymbolaSymbol = P(Symbol, function(_, super_) { }); LatexCmds['@'] = NonSymbolaSymbol; -LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&'); -LatexCmds['%'] = bind(NonSymbolaSymbol, '\\%', '%'); +LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&', 'and'); +LatexCmds['%'] = P(NonSymbolaSymbol, function(_, super_) { + _.init = function () { + super_.init.call(this, '\\%', '%', 'percent'); + }; + _.parser = function () { + var optWhitespace = Parser.optWhitespace; + var string = Parser.string; + + // Parse `\%\operatorname{of}` as special `percentof` node so that + // it will be serialized properly and deleted as a unit. + return optWhitespace + .then( + string('\\operatorname{of}') + .map(function () { + return LatexCmds.percentof(); + }) + ).or(super_.parser.call(this)) + ; + } +}); + +LatexCmds['∥'] = LatexCmds.parallel = + bind(VanillaSymbol, '\\parallel ', '∥', 'parallel'); + +LatexCmds['∦'] = LatexCmds.nparallel = + bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel'); + +LatexCmds['⟂'] = LatexCmds.perp = + bind(VanillaSymbol, '\\perp ', '⟂', 'perpendicular'); //the following are all Greek to me, but this helped a lot: http://www.ams.org/STIX/ion/stixsig03.html @@ -303,54 +632,54 @@ LatexCmds.omega = P(Variable, function(_, super_) { //why can't anybody FUCKING agree on these LatexCmds.phi = //W3C or Unicode? - bind(Variable,'\\phi ','ϕ'); + bind(Variable,'\\phi ','ϕ', 'phi'); LatexCmds.phiv = //Elsevier and 9573-13 LatexCmds.varphi = //AMS and LaTeX - bind(Variable,'\\varphi ','φ'); + bind(Variable,'\\varphi ','φ', 'phi'); LatexCmds.epsilon = //W3C or Unicode? - bind(Variable,'\\epsilon ','ϵ'); + bind(Variable,'\\epsilon ','ϵ', 'epsilon'); LatexCmds.epsiv = //Elsevier and 9573-13 LatexCmds.varepsilon = //AMS and LaTeX - bind(Variable,'\\varepsilon ','ε'); + bind(Variable,'\\varepsilon ','ε', 'epsilon'); LatexCmds.piv = //W3C/Unicode and Elsevier and 9573-13 LatexCmds.varpi = //AMS and LaTeX - bind(Variable,'\\varpi ','ϖ'); + bind(Variable,'\\varpi ','ϖ', 'piv'); LatexCmds.sigmaf = //W3C/Unicode LatexCmds.sigmav = //Elsevier LatexCmds.varsigma = //LaTeX - bind(Variable,'\\varsigma ','ς'); + bind(Variable,'\\varsigma ','ς', 'sigma'); LatexCmds.thetav = //Elsevier and 9573-13 LatexCmds.vartheta = //AMS and LaTeX LatexCmds.thetasym = //W3C/Unicode - bind(Variable,'\\vartheta ','ϑ'); + bind(Variable,'\\vartheta ','ϑ', 'theta'); LatexCmds.upsilon = //AMS and LaTeX and W3C/Unicode LatexCmds.upsi = //Elsevier and 9573-13 - bind(Variable,'\\upsilon ','υ'); + bind(Variable,'\\upsilon ','υ', 'upsilon'); //these aren't even mentioned in the HTML character entity references LatexCmds.gammad = //Elsevier LatexCmds.Gammad = //9573-13 -- WTF, right? I dunno if this was a typo in the reference (see above) LatexCmds.digamma = //LaTeX - bind(Variable,'\\digamma ','ϝ'); + bind(Variable,'\\digamma ','ϝ', 'gamma'); LatexCmds.kappav = //Elsevier LatexCmds.varkappa = //AMS and LaTeX - bind(Variable,'\\varkappa ','ϰ'); + bind(Variable,'\\varkappa ','ϰ', 'kappa'); LatexCmds.rhov = //Elsevier and 9573-13 LatexCmds.varrho = //AMS and LaTeX - bind(Variable,'\\varrho ','ϱ'); + bind(Variable,'\\varrho ','ϱ', 'rho'); //Greek constants, look best in non-italicized Times New Roman -LatexCmds.pi = LatexCmds['π'] = bind(NonSymbolaSymbol,'\\pi ','π'); -LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ'); +LatexCmds.pi = LatexCmds['π'] = bind(NonSymbolaSymbol,'\\pi ','π', 'pi'); +LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ', 'lambda'); //uppercase greek letters @@ -358,7 +687,7 @@ LatexCmds.Upsilon = //LaTeX LatexCmds.Upsi = //Elsevier and 9573-13 LatexCmds.upsih = //W3C/Unicode "upsilon with hook" LatexCmds.Upsih = //'cos it makes sense to me - bind(Symbol,'\\Upsilon ','ϒ'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :( + bind(Symbol,'\\Upsilon ','ϒ', 'capital upsilon'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :( //other symbols with the same LaTeX command and HTML character entity reference LatexCmds.Gamma = @@ -389,8 +718,9 @@ var LatexFragment = P(MathCommand, function(_) { block.finalizeInsert(cursor.options, cursor); if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); - cursor.parent.bubble('reflow'); + cursor.parent.bubble(function (node) { node.reflow(); }); }; + _.mathspeak = function() { return latexMathParser.parse(this.latex).mathspeak(); }; _.parser = function() { var frag = latexMathParser.parse(this.latex).children(); return Parser.succeed(frag); @@ -421,54 +751,125 @@ var LatexFragment = P(MathCommand, function(_) { // [2]: http://en.wikipedia.org/wiki/Number_Forms // [3]: http://en.wikipedia.org/wiki/ISO/IEC_8859-1 // [4]: http://en.wikipedia.org/wiki/Windows-1252 +LatexCmds['⁰'] = bind(LatexFragment, '^0'); LatexCmds['¹'] = bind(LatexFragment, '^1'); LatexCmds['²'] = bind(LatexFragment, '^2'); LatexCmds['³'] = bind(LatexFragment, '^3'); +LatexCmds['⁴'] = bind(LatexFragment, '^4'); +LatexCmds['⁵'] = bind(LatexFragment, '^5'); +LatexCmds['⁶'] = bind(LatexFragment, '^6'); +LatexCmds['⁷'] = bind(LatexFragment, '^7'); +LatexCmds['⁸'] = bind(LatexFragment, '^8'); +LatexCmds['⁹'] = bind(LatexFragment, '^9'); + LatexCmds['¼'] = bind(LatexFragment, '\\frac14'); LatexCmds['½'] = bind(LatexFragment, '\\frac12'); LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); +// this is a hack to make pasting the √ symbol +// actually insert a sqrt command. This isn't ideal, +// but it's way better than what we have now. I think +// before we invest any more time into this single character +// we should consider how to make the pipe (|) automatically +// insert absolute value. We also will want the percent (%) +// to expand to '% of'. I've always just thought mathquill's +// ability to handle pasted latex magical until I started actually +// testing it. It's a lot more buggy that I previously thought. +// +// KNOWN ISSUES: +// 1) pasting √ does not put focus in side the sqrt symbol +// 2) pasting √2 puts the 2 outside of the sqrt symbol. +// +// The first issue seems like we could invest more time into this to +// fix it, but doesn't feel worth special casing. I think we'd want +// to address it by addressing ALL pasting issues. +// +// The second issue seems like it might go away too if you fix paste to +// act more like simply typing the characters out. I'd be scared to try +// to make that change because I'm fairly confident I'd break something +// around handling valid latex as latex rather than treating it as keystrokes. +LatexCmds['√'] = bind(LatexFragment, '\\sqrt{}'); + +// Binary operator determination is used in several contexts for PlusMinus nodes and their descendants. +// For instance, we set the item's class name based on this factor, and also assign different mathspeak values (plus vs positive, negative vs minus). +function isBinaryOperator(node) { + if (node[L]) { + // If the left sibling is a binary operator or a separator (comma, semicolon, colon, space) + // or an open bracket (open parenthesis, open square bracket) + // consider the operator to be unary + if (node[L] instanceof BinaryOperator || /^(\\ )|[,;:\(\[]$/.test(node[L].ctrlSeq)) { + return false; + } + } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { + //if we are in a style block at the leftmost edge, determine unary/binary based on + //the style block + //this allows style blocks to be transparent for unary/binary purposes + return isBinaryOperator(node.parent.parent); + } else { + return false; + } + + return true; +} + var PlusMinus = P(BinaryOperator, function(_) { - _.init = VanillaSymbol.prototype.init; + _.init = VanillaSymbol.prototype.init; _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { - function determineOpClassType(node) { - if (node[L]) { - // If the left sibling is a binary operator or a separator (comma, semicolon, colon) - // or an open bracket (open parenthesis, open square bracket) - // consider the operator to be unary - if (node[L] instanceof BinaryOperator || /^[,;:\(\[]$/.test(node[L].ctrlSeq)) { - return ''; - } - } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { - //if we are in a style block at the leftmost edge, determine unary/binary based on - //the style block - //this allows style blocks to be transparent for unary/binary purposes - return determineOpClassType(node.parent.parent); - } else { - return ''; - } - - return 'mq-binary-operator'; - }; - if (dir === R) return; // ignore if sibling only changed on the right - this.jQ[0].className = determineOpClassType(this); + this.jQ[0].className = isBinaryOperator(this) + ? 'mq-binary-operator' + : ''; + return this; }; }); -LatexCmds['+'] = bind(PlusMinus, '+', '+'); -//yes, these are different dashes, I think one is an en dash and the other is a hyphen -LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−'); +LatexCmds['+'] = P(PlusMinus, function(_, super_) { + _.init = function () { + super_.init.call(this, '+', '+'); + }; + _.mathspeak = function() { + return isBinaryOperator(this) ? 'plus' : 'positive'; + }; +}); + +//yes, these are different dashes, en-dash, em-dash, unicode minus, actual dash +LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(PlusMinus, function(_, super_) { + _.init = function () { + super_.init.call(this, '-', '−'); + }; + _.mathspeak = function() { + return isBinaryOperator(this) ? 'minus' : 'negative'; + }; +}); + LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = - bind(PlusMinus,'\\pm ','±'); + bind(PlusMinus,'\\pm ','±', 'plus-or-minus'); LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = - bind(PlusMinus,'\\mp ','∓'); + bind(PlusMinus,'\\mp ','∓', 'minus-or-plus'); CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = - bind(BinaryOperator, '\\cdot ', '·', '*'); -//semantically should be ⋅, but · looks better + bind(BinaryOperator, '\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better + +var To = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\to ','→', 'to'); + } + _.deleteTowards = function(dir, cursor) { + if (dir === L) { + var l = cursor[L]; + Fragment(l, this).remove(); + cursor[L] = l[L]; + LatexCmds['−']().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.deleteTowards.apply(this, arguments); + }; +}) + +LatexCmds['→'] = LatexCmds.to = To; var Inequality = P(BinaryOperator, function(_, super_) { _.init = function(data, strict) { @@ -476,7 +877,7 @@ var Inequality = P(BinaryOperator, function(_, super_) { this.strict = strict; var strictness = (strict ? 'Strict' : ''); super_.init.call(this, data['ctrlSeq'+strictness], data['html'+strictness], - data['text'+strictness]); + data['text'+strictness], data['mathspeak'+strictness]); }; _.swap = function(strict) { this.strict = strict; @@ -484,35 +885,56 @@ var Inequality = P(BinaryOperator, function(_, super_) { this.ctrlSeq = this.data['ctrlSeq'+strictness]; this.jQ.html(this.data['html'+strictness]); this.textTemplate = [ this.data['text'+strictness] ]; + this.mathspeakName = this.data['mathspeak'+strictness]; }; _.deleteTowards = function(dir, cursor) { if (dir === L && !this.strict) { this.swap(true); - this.bubble('reflow'); + this.bubble(function (node) { node.reflow(); }); return; } super_.deleteTowards.apply(this, arguments); }; }); -var less = { ctrlSeq: '\\le ', html: '≤', text: '≤', - ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<' }; -var greater = { ctrlSeq: '\\ge ', html: '≥', text: '≥', - ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>' }; +var less = { ctrlSeq: '\\le ', html: '≤', text: '≤', mathspeak: 'less than or equal to', + ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<', mathspeakStrict: 'less than'}; +var greater = { ctrlSeq: '\\ge ', html: '≥', text: '≥', mathspeak: 'greater than or equal to', + ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>', mathspeakStrict: 'greater than'}; + +var Greater = P(Inequality, function(_, super_) { + _.init = function() { + super_.init.call(this, greater, true); + }; + _.createLeftOf = function(cursor) { + if (cursor[L] instanceof BinaryOperator && cursor[L].ctrlSeq === '-') { + var l = cursor[L]; + cursor[L] = l[L]; + l.remove(); + To().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.createLeftOf.apply(this, arguments); + }; +}) LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true); -LatexCmds['>'] = LatexCmds.gt = bind(Inequality, greater, true); +LatexCmds['>'] = LatexCmds.gt = Greater; LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); +LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = + bind(VanillaSymbol,'\\infty ','∞', 'infinity'); +LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal'); var Equality = P(BinaryOperator, function(_, super_) { _.init = function() { - super_.init.call(this, '=', '='); + super_.init.call(this, '=', '=', '=', 'equals'); }; _.createLeftOf = function(cursor) { if (cursor[L] instanceof Inequality && cursor[L].strict) { cursor[L].swap(false); - cursor[L].bubble('reflow'); + cursor[L].bubble(function (node) { node.reflow(); }); return; } super_.createLeftOf.apply(this, arguments); @@ -520,9 +942,45 @@ var Equality = P(BinaryOperator, function(_, super_) { }); LatexCmds['='] = Equality; -LatexCmds['×'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]'); +LatexCmds['×'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]', 'times'); LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = - bind(BinaryOperator,'\\div ','÷', '[/]'); + bind(BinaryOperator,'\\div ','÷', '[/]', 'over'); + + +var Sim = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\sim ', '~', '~', 'tilde'); + }; + _.createLeftOf = function(cursor) { + if (cursor[L] instanceof Sim) { + var l = cursor[L]; + cursor[L] = l[L]; + l.remove(); + Approx().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.createLeftOf.apply(this, arguments); + }; +}); + +var Approx = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\approx ', '≈', '≈', 'approximately equal'); + }; + _.deleteTowards = function(dir, cursor) { + if (dir === L) { + var l = cursor[L]; + Fragment(l, this).remove(); + cursor[L] = l[L]; + Sim().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.deleteTowards.apply(this, arguments); + }; +}); -CharCmds['~'] = LatexCmds.sim = bind(BinaryOperator, '\\sim ', '~', '~'); +CharCmds['~'] = LatexCmds.sim = Sim; +LatexCmds['≈'] = LatexCmds.approx = Approx; diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 1889ba6a9..3a5214ac8 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -1,88 +1,127 @@ /*************************** * Commands and Operators. **************************/ - -var scale, // = function(jQ, x, y) { ... } -//will use a CSS 2D transform to scale the jQuery-wrapped HTML elements, -//or the filter matrix transform fallback for IE 5.5-8, or gracefully degrade to -//increasing the fontSize to match the vertical Y scaling factor. - -//ideas from http://github.com/louisremi/jquery.transform.js -//see also http://msdn.microsoft.com/en-us/library/ms533014(v=vs.85).aspx - - forceIERedraw = noop, - div = document.createElement('div'), - div_style = div.style, - transformPropNames = { - transform:1, - WebkitTransform:1, - MozTransform:1, - OTransform:1, - msTransform:1 +var SVG_SYMBOLS = { + 'sqrt': { + html: + '' + + '' + + '' }, - transformPropName; - -for (var prop in transformPropNames) { - if (prop in div_style) { - transformPropName = prop; - break; + '|': { + width: '.4em', + html: + '' + + '' + + '' + }, + '[': { + width: '.55em', + html: + '' + + '' + + '' + }, + ']': { + width: '.55em', + html: + '' + + '' + + '' + }, + '(': { + width: '.55em', + html: + '' + + '' + + '' + }, + ')': { + width: '.55em', + html: + '' + + '' + + '' + }, + '{': { + width: '.7em', + html: + '' + + '' + + '' + }, + '}': { + width: '.7em', + html: + '' + + '' + + '' + }, + '∥': { + width: '.7em', + html: + '' + + '' + + '' + }, + '⟨': { + width: '.55em', + html: + '' + + '' + + '' + }, + '⟩': { + width: '.55em', + html: + '' + + '' + + '' } -} - -if (transformPropName) { - scale = function(jQ, x, y) { - jQ.css(transformPropName, 'scale('+x+','+y+')'); - }; -} -else if ('filter' in div_style) { //IE 6, 7, & 8 fallback, see https://github.com/laughinghan/mathquill/wiki/Transforms - forceIERedraw = function(el){ el.className = el.className; }; - scale = function(jQ, x, y) { //NOTE: assumes y > x - x /= (1+(y-1)/2); - jQ.css('fontSize', y + 'em'); - if (!jQ.hasClass('mq-matrixed-container')) { - jQ.addClass('mq-matrixed-container') - .wrapInner(''); - } - var innerjQ = jQ.children() - .css('filter', 'progid:DXImageTransform.Microsoft' - + '.Matrix(M11=' + x + ",SizingMethod='auto expand')" - ); - function calculateMarginRight() { - jQ.css('marginRight', (innerjQ.width()-1)*(x-1)/x + 'px'); - } - calculateMarginRight(); - var intervalId = setInterval(calculateMarginRight); - $(window).load(function() { - clearTimeout(intervalId); - calculateMarginRight(); - }); - }; -} -else { - scale = function(jQ, x, y) { - jQ.css('fontSize', y + 'em'); - }; -} +}; var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs) { + _.init = function(ctrlSeq, tagName, attrs, ariaLabel, opts) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); + _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); + _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; + // In most cases, mathspeak should announce the start and end of style blocks. + // There is one exception currently (mathrm). + _.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters; + }; + _.mathspeak = function(opts) { + if ( + !this.shouldNotSpeakDelimiters || + (opts && opts.ignoreShorthand) + ) { + return super_.mathspeak.call(this); + } + return this.foldChildren('', function(speech, block) { + return speech + ' ' + block.mathspeak(opts); + }).trim(); }; }); //fonts -LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"'); -LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"'); -LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"'); -LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"'); -LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"'); +LatexCmds.mathrm = P(Style, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', { shouldNotSpeakDelimiters: true }); + }; + _.isTextBlock = function() { + return true; + }; +}); +LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"', 'Italic Font'); +LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"', 'Bold Font'); +LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"', 'Serif Font'); +LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"', 'Math Text'); //text-decoration -LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"'); -LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"'); -LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"'); -LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"'); -LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-both"'); -LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"'); +LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"', 'Underline'); +LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"', 'Overline'); +LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"', 'Over Right Arrow'); +LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"', 'Over Left Arrow'); +LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow ', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-leftright"', 'Over Left and Right Arrow'); +LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"', 'Over Arc'); LatexCmds.dot = P(MathCommand, function(_, super_) { _.init = function() { super_.init.call(this, '\\dot', '' @@ -105,6 +144,8 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { this.color = color; this.htmlTemplate = '&0'; + _.ariaLabel = color.replace(/^\\/, ''); + _.mathspeakTemplate = ['Start ' + _.ariaLabel + ',', 'End ' + _.ariaLabel]; }; _.latex = function() { return '\\textcolor{' + this.color + '}{' + this.blocks[0].latex() + '}'; @@ -144,6 +185,8 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { .then(function(cls) { self.cls = cls || ''; self.htmlTemplate = '&0'; + self.ariaLabel = cls + ' class'; + self.mathspeakTemplate = ['Start ' + self.ariaLabel + ',', 'End ' + self.ariaLabel]; return super_.parser.call(self); }) ; @@ -156,6 +199,28 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { }; }); +// This test is used to determine whether an item may be treated as a whole number +// for shortening the verbalized (mathspeak) forms of some fractions and superscripts. +var intRgx = /^[\+\-]?[\d]+$/; + +// Traverses the top level of the passed block's children and returns the concatenation of their ctrlSeq properties. +// Used in shortened mathspeak computations as a block's .text() method can be potentially expensive. +// +function getCtrlSeqsFromBlock(block) { + if ( + typeof(block) !== 'object' || + typeof(block.children) !== 'function' + ) + return block; + var children = block.children(); + if (!children || !children.ends[L]) return block; + var chars = ''; + for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) { + if (sibling.ctrlSeq !== undefined) chars += sibling.ctrlSeq; + } + return chars; +} + var SupSub = P(MathCommand, function(_, super_) { _.ctrlSeq = '_{...}^{...}'; _.createLeftOf = function(cursor) { @@ -211,11 +276,14 @@ var SupSub = P(MathCommand, function(_, super_) { var cmd = this.chToCmd(ch, cursor.options); if (cmd instanceof Symbol) cursor.deleteSelection(); else cursor.clearSelection().insRightOf(this.parent); - return cmd.createLeftOf(cursor.show()); + cmd.createLeftOf(cursor.show()); + aria.queue('Baseline').alert(cmd.mathspeak({ createdLeftOf: cursor })); + return; } if (cursor[L] && !cursor[R] && !cursor.selection && cursor.options.charsThatBreakOutOfSupSub.indexOf(ch) > -1) { cursor.insRightOf(this.parent); + aria.queue('Baseline'); } MathBlock.p.write.apply(this, arguments); }; @@ -246,7 +314,7 @@ var SupSub = P(MathCommand, function(_, super_) { _.latex = function() { function latex(prefix, block) { var l = block && block.latex(); - return block ? prefix + (l.length === 1 ? l : '{' + (l || ' ') + '}') : ''; + return block ? prefix + '{' + (l || ' ') + '}' : ''; } return latex('_', this.sub) + latex('^', this.sup); }; @@ -261,16 +329,19 @@ var SupSub = P(MathCommand, function(_, super_) { if (this.supsub === 'sub') { this.sup = this.upInto = this.sub.upOutOf = block; block.adopt(this, this.sub, 0).downOutOf = this.sub; - block.jQ = $('').append(block.jQ.children()) - .attr(mqBlockId, block.id).prependTo(this.jQ); + block.jQ = $('').append(block.jQ.children()).prependTo(this.jQ); + Node.linkElementByBlockNode(block.jQ[0], block); } else { this.sub = this.downInto = this.sup.downOutOf = block; block.adopt(this, 0, this.sup).upOutOf = this.sup; block.jQ = $('').append(block.jQ.children()) - .attr(mqBlockId, block.id).appendTo(this.jQ.removeClass('mq-sup-only')); + .appendTo(this.jQ.removeClass('mq-sup-only')); + Node.linkElementByBlockNode(block.jQ[0], block); this.jQ.append(''); } + + // like 'sub sup'.split(' ').forEach(function(supsub) { ... }); for (var i = 0; i < 2; i += 1) (function(cmd, supsub, oppositeSupsub, updown) { cmd[supsub].deleteOutOf = function(dir, cursor) { @@ -292,28 +363,6 @@ var SupSub = P(MathCommand, function(_, super_) { }; }(this, 'sub sup'.split(' ')[i], 'sup sub'.split(' ')[i], 'down up'.split(' ')[i])); }; - _.reflow = function() { - var $block = this.jQ ;//mq-supsub - var $prev = $block.prev() ; - - if ( !$prev.length ) { - //we cant normalize it without having prev. element (which is base) - return ; - } - - var $sup = $block.children( '.mq-sup' );//mq-supsub -> mq-sup - if ( $sup.length ) { - var sup_fontsize = parseInt( $sup.css('font-size') ) ; - var sup_bottom = $sup.offset().top + $sup.height() ; - //we want that superscript overlaps top of base on 0.7 of its font-size - //this way small superscripts like x^2 look ok, but big ones like x^(1/2/3) too - var needed = sup_bottom - $prev.offset().top - 0.7*sup_fontsize ; - var cur_margin = parseInt( $sup.css('margin-bottom' ) ) ; - //we lift it up with margin-bottom - $sup.css( 'margin-bottom', cur_margin + needed ) ; - } - } ; - }); function insLeftOfMeUnlessAtEnd(cursor) { @@ -337,6 +386,8 @@ LatexCmds._ = P(SupSub, function(_, super_) { + '' ; _.textTemplate = [ '_' ]; + _.mathspeakTemplate = [ 'Subscript,', ', Baseline']; + _.ariaLabel = 'subscript'; _.finalizeTree = function() { this.downInto = this.sub = this.ends[L]; this.sub.upOutOf = insLeftOfMeUnlessAtEnd; @@ -354,6 +405,53 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { + '' ; _.textTemplate = ['^(', ')']; + _.mathspeak = function(opts) { + // Simplify basic exponent speech for common whole numbers. + var child = this.upInto; + if (child !== undefined) { + // Calculate this item's inner text to determine whether to shorten the returned speech. + // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. + // Since the mathspeak computation is recursive, we want to call it only once in this function to avoid performance bottlenecks. + var innerText = getCtrlSeqsFromBlock(child); + // If the superscript is a whole number, shorten the speech that is returned. + if ( + (!opts || !opts.ignoreShorthand) && + intRgx.test(innerText) + ) { + // Simple cases + if (innerText === '0') { + return 'to the 0 power'; + } else if (innerText === '2') { + return 'squared'; + } else if (innerText === '3') { + return 'cubed'; + } + + // More complex cases. + var suffix = ''; + // Limit suffix addition to exponents < 1000. + if (/^[+-]?\d{1,3}$/.test(innerText)) { + if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { + suffix = 'th'; + } else if (/1$/.test(innerText)) { + suffix = 'st'; + } else if (/2$/.test(innerText)) { + suffix = 'nd'; + } else if (/3$/.test(innerText)) { + suffix = 'rd'; + } + } + var innerMathspeak = typeof(child) === 'object' + ? child.mathspeak() + : innerText; + return 'to the ' + innerMathspeak + suffix + ' power'; + } + } + return super_.mathspeak.call(this); + }; + + _.ariaLabel = 'superscript'; + _.mathspeakTemplate = [ 'Superscript,', ', Baseline']; _.finalizeTree = function() { this.upInto = this.sup = this.ends[R]; this.sup.downOutOf = insLeftOfMeUnlessAtEnd; @@ -362,7 +460,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { }); var SummationNotation = P(MathCommand, function(_, super_) { - _.init = function(ch, html) { + _.init = function(ch, html, ariaLabel) { + _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); var htmlTemplate = '' + '&1' @@ -381,11 +480,15 @@ var SummationNotation = P(MathCommand, function(_, super_) { }; _.latex = function() { function simplify(latex) { - return latex.length === 1 ? latex : '{' + (latex || ' ') + '}'; + return '{' + (latex || ' ') + '}'; } return this.ctrlSeq + '_' + simplify(this.ends[L].latex()) + '^' + simplify(this.ends[R].latex()); }; + _.mathspeak = function() { + return 'Start ' + this.ariaLabel + ' from ' + this.ends[L].mathspeak() + + ' to ' + this.ends[R].mathspeak() + ', end ' + this.ariaLabel + ', '; + }; _.parser = function() { var string = Parser.string; var optWhitespace = Parser.optWhitespace; @@ -407,6 +510,8 @@ var SummationNotation = P(MathCommand, function(_, super_) { }).many().result(self); }; _.finalizeTree = function() { + this.ends[L].ariaLabel = 'lower bound'; + this.ends[R].ariaLabel = 'upper bound'; this.downInto = this.ends[L]; this.upInto = this.ends[R]; this.ends[L].upOutOf = this.ends[R]; @@ -416,19 +521,20 @@ var SummationNotation = P(MathCommand, function(_, super_) { LatexCmds['∑'] = LatexCmds.sum = -LatexCmds.summation = bind(SummationNotation,'\\sum ','∑'); +LatexCmds.summation = bind(SummationNotation,'\\sum ','∑', 'sum'); LatexCmds['∏'] = LatexCmds.prod = -LatexCmds.product = bind(SummationNotation,'\\prod ','∏'); +LatexCmds.product = bind(SummationNotation,'\\prod ','∏', 'product'); LatexCmds.coprod = -LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐'); +LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐', 'co product'); LatexCmds['∫'] = LatexCmds['int'] = LatexCmds.integral = P(SummationNotation, function(_, super_) { _.init = function() { + _.ariaLabel = 'integral'; var htmlTemplate = '' + '' @@ -439,12 +545,11 @@ LatexCmds.integral = P(SummationNotation, function(_, super_) { + '' + '' ; - Symbol.prototype.init.call(this, '\\int ', htmlTemplate); + Symbol.prototype.init.call(this, '\\int ', htmlTemplate, 'integral'); }; // FIXME: refactor rather than overriding _.createLeftOf = MathCommand.p.createLeftOf; }); - var Fraction = LatexCmds.frac = LatexCmds.dfrac = @@ -462,6 +567,100 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { _.finalizeTree = function() { this.upInto = this.ends[R].upOutOf = this.ends[L]; this.downInto = this.ends[L].downOutOf = this.ends[R]; + this.ends[L].ariaLabel = 'numerator'; + this.ends[R].ariaLabel = 'denominator'; + if(this.getFracDepth() > 1) { + this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction']; + } else { + this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction']; + } + }; + + _.mathspeak = function(opts) { + if (opts && opts.createdLeftOf) { + var cursor = opts.createdLeftOf; + return cursor.parent.mathspeak(); + } + + var numText = getCtrlSeqsFromBlock(this.ends[L]); + var denText = getCtrlSeqsFromBlock(this.ends[R]); + + // Shorten mathspeak value for whole number fractions whose denominator is less than 10. + if ( + (!opts || !opts.ignoreShorthand) && + intRgx.test(numText) && intRgx.test(denText) + ) { + var isSingular = numText === '1' || numText === '-1'; + var newDenSpeech = ''; + if (denText === '2') { + newDenSpeech = isSingular + ? 'half' + : 'halves'; + } else if (denText === '3') { + newDenSpeech = isSingular + ? 'third' + : 'thirds'; + } else if (denText === '4') { + newDenSpeech = isSingular + ? 'quarter' + : 'quarters'; + } else if (denText === '5') { + newDenSpeech = isSingular + ? 'fifth' + : 'fifths'; + } else if (denText === '6') { + newDenSpeech = isSingular + ? 'sixth' + : 'sixths'; + } else if (denText === '7') { + newDenSpeech = isSingular + ? 'seventh' + : 'sevenths'; + } else if (denText === '8') { + newDenSpeech = isSingular + ? 'eighth' + : 'eighths'; + } else if (denText === '9') { + newDenSpeech = isSingular + ? 'ninth' + : 'ninths'; + } + if (newDenSpeech !== '') { + var output = ''; + // Handle the case of an integer followed by a simplified fraction such as 1\frac{1}{2}. + // Such combinations should be spoken aloud as "1 and 1 half." + // Start at the left sibling of the fraction and continue leftward until something other than a digit or whitespace is found. + var precededByInteger = false; + for (var sibling = this[L]; sibling[L] !== undefined; sibling = sibling[L]) { + // Ignore whitespace + if (sibling.ctrlSeq === '\\ ') { + continue; + } else if (intRgx.test(sibling.ctrlSeq)) { + precededByInteger = true; + } else { + precededByInteger = false; + break; + } + } + if (precededByInteger) { + output += 'and '; + } + output += this.ends[L].mathspeak() + ' ' + newDenSpeech; + return output; + } + } + + return super_.mathspeak.apply(this, arguments); + }; + + _.getFracDepth = function() { + var level = 0; + var walkUp = function(item, level) { + if(item instanceof Node && item.ctrlSeq && item.ctrlSeq.toLowerCase().search('frac') >= 0) level += 1; + if(item.parent) return walkUp(item.parent, level); + else return level; + }; + return walkUp(this, level); }; }); @@ -471,16 +670,18 @@ CharCmds['/'] = P(Fraction, function(_, super_) { _.createLeftOf = function(cursor) { if (!this.replacedFragment) { var leftward = cursor[L]; - while (leftward && - !( - leftward instanceof BinaryOperator || - leftward instanceof (LatexCmds.text || noop) || - leftward instanceof SummationNotation || - leftward.ctrlSeq === '\\ ' || - /^[,;:]$/.test(leftward.ctrlSeq) - ) //lookbehind for operator - ) leftward = leftward[L]; + if (!cursor.options.typingSlashCreatesNewFraction) { + while (leftward && + !( + leftward instanceof BinaryOperator || + leftward instanceof (LatexCmds.text || noop) || + leftward instanceof SummationNotation || + leftward.ctrlSeq === '\\ ' || + /^[,;:]$/.test(leftward.ctrlSeq) + ) //lookbehind for operator + ) leftward = leftward[L]; + } if (leftward instanceof SummationNotation && leftward[R] instanceof SupSub) { leftward = leftward[R]; if (leftward[R] instanceof SupSub && leftward[R].ctrlSeq != leftward.ctrlSeq) @@ -496,17 +697,42 @@ CharCmds['/'] = P(Fraction, function(_, super_) { }; }); +LatexCmds.ans = P(Symbol, function(_, super_) { + _.init = function(ch) { + super_.init.call(this, + '\\operatorname{ans}', + 'ans', + 'ans' + ); + }; +}); + +LatexCmds.percent = +LatexCmds.percentof = P(Symbol, function (_, super_) { + _.init = function () { + super_.init.call( + this, + '\\%\\operatorname{of}', + '% of ', + 'percent of' + ) + }; +}); + var SquareRoot = -LatexCmds.sqrt = -LatexCmds['√'] = P(MathCommand, function(_, super_) { +LatexCmds.sqrt = P(MathCommand, function(_, super_) { _.ctrlSeq = '\\sqrt'; _.htmlTemplate = - '' - + '' + '' + + '' + + SVG_SYMBOLS.sqrt.html + + '' + '&0' + '' ; _.textTemplate = ['sqrt(', ')']; + _.mathspeakTemplate = ['StartRoot,', ', EndRoot']; + _.ariaLabel = 'root'; _.parser = function() { return latexMathParser.optBlock.then(function(optBlock) { return latexMathParser.block.map(function(block) { @@ -518,10 +744,6 @@ LatexCmds['√'] = P(MathCommand, function(_, super_) { }); }).or(super_.parser.call(this)); }; - _.reflow = function() { - var block = this.ends[R].jQ; - scale(block.prev(), 1, block.innerHeight()/+block.css('fontSize').slice(0,-2) - .1); - }; }); var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { @@ -538,16 +760,40 @@ var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { var NthRoot = LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.htmlTemplate = - '&0' - + '' - + '' - + '&1' + '' + + '&0' + + '' + + '' + + SVG_SYMBOLS.sqrt.html + + '' + + '&1' + + '' + '' ; _.textTemplate = ['sqrt[', '](', ')']; _.latex = function() { return '\\sqrt['+this.ends[L].latex()+']{'+this.ends[R].latex()+'}'; }; + _.mathspeak = function() { + var indexMathspeak = this.ends[L].mathspeak(); + var radicandMathspeak = this.ends[R].mathspeak(); + this.ends[L].ariaLabel = 'Index'; + this.ends[R].ariaLabel = 'Radicand'; + if (indexMathspeak === '3') { // cube root + return 'Start Cube Root, '+radicandMathspeak+', End Cube Root'; + } else { + return 'Root Index '+indexMathspeak+', Start Root, '+radicandMathspeak+', End Root'; + } + }; +}); + +var CubeRoot = +LatexCmds.cbrt = P(NthRoot, function(_, super_) { + _.createLeftOf = function(cursor) { + super_.createLeftOf.apply(this, arguments); + Digit('3').createLeftOf(cursor); + cursor.controller.moveRight(); + }; }); var DiacriticAbove = P(MathCommand, function(_, super_) { @@ -571,11 +817,6 @@ function DelimsMixin(_, super_) { this.delimjQs = this.jQ.children(':first').add(this.jQ.children(':last')); this.contentjQ = this.jQ.children(':eq(1)'); }; - _.reflow = function() { - var height = this.contentjQ.outerHeight() - / parseFloat(this.contentjQ.css('fontSize')); - scale(this.delimjQs, min(1 + .2*(height - 1), 1.2), 1.2*height); - }; } // Round/Square/Curly/Angle Brackets (aka Parens/Brackets/Braces) @@ -590,23 +831,48 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { this.sides[R] = { ch: close, ctrlSeq: end }; }; _.numBlocks = function() { return 1; }; - _.html = function() { // wait until now so that .side may + _.html = function() { + var leftSymbol = this.getSymbol(L); + var rightSymbol = this.getSymbol(R); + + // wait until now so that .side may this.htmlTemplate = // be set by createLeftOf or parser - '' - + '' - + this.sides[L].ch + '' + + '' + + leftSymbol.html + '' - + '&0' - + '' - + this.sides[R].ch + + '&0' + + '' + + rightSymbol.html + '' + '' ; return super_.html.call(this); }; + _.getSymbol = function (side) { + return SVG_SYMBOLS[this.sides[side || R].ch] || {width: '0', html: ''}; + }; _.latex = function() { return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq; }; + _.mathspeak = function(opts) { + var open = this.sides[L].ch, close = this.sides[R].ch; + if (open === '|' && close === '|') { + this.mathspeakTemplate = ['StartAbsoluteValue,', ', EndAbsoluteValue']; + this.ariaLabel = 'absolute value'; + } + else if (opts && opts.createdLeftOf && this.side) { + var ch = ''; + if (this.side === L) ch = this.textTemplate[0]; + else if (this.side === R) ch = this.textTemplate[1]; + return (this.side === L ? 'left ' : 'right ') + BRACKET_NAMES[ch]; + } + else { + this.mathspeakTemplate = ['left ' + BRACKET_NAMES[open]+',', ', right ' + BRACKET_NAMES[close]]; + this.ariaLabel = BRACKET_NAMES[open]+' block'; + } + return super_.mathspeak.call(this); + }; _.matchBrack = function(opts, expectedSide, node) { // return node iff it's a matching 1-sided bracket of expected side (if any) return node instanceof Bracket && node.side && node.side !== -expectedSide @@ -617,8 +883,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { _.closeOpposing = function(brack) { brack.side = 0; brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be - brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) - .removeClass('mq-ghost').html(this.sides[this.side].ch); + var $brack = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) + .removeClass('mq-ghost'); + this.replaceBracket($brack, this.side); }; _.createLeftOf = function(cursor) { if (!this.replacedFragment) { // unless wrapping seln in brackets, @@ -642,7 +909,7 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { .disown().withDirAdopt(-side, brack.parent, brack, brack[side]) .jQ.insDirOf(side, brack.jQ); } - brack.bubble('reflow'); + brack.bubble(function (node) { node.reflow(); }); } else { brack = this, side = brack.side; @@ -694,8 +961,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { else { // else deleting just one of a pair of brackets, become one-sided this.sides[side] = { ch: OPP_BRACKS[this.sides[this.side].ch], ctrlSeq: OPP_BRACKS[this.sides[this.side].ctrlSeq] }; - this.delimjQs.removeClass('mq-ghost') - .eq(side === L ? 0 : 1).addClass('mq-ghost').html(this.sides[side].ch); + var $brack = this.delimjQs.removeClass('mq-ghost') + .eq(side === L ? 0 : 1).addClass('mq-ghost'); + this.replaceBracket($brack, side); } if (sib) { // auto-expand so ghost is at far end var origEnd = this.ends[L].ends[side]; @@ -709,6 +977,16 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { : cursor.insAtDirEnd(side, this.ends[L])); } }; + _.replaceBracket = function ($brack, side) { + var symbol = this.getSymbol(side); + $brack.html(symbol.html).css('width', symbol.width); + + if (side === L) { + $brack.next().css('margin-left', symbol.width); + } else { + $brack.prev().css('margin-right', symbol.width); + } + }; _.deleteTowards = function(dir, cursor) { this.deleteSide(-dir, false, cursor); }; @@ -746,20 +1024,28 @@ var OPP_BRACKS = { '\\rVert ' : '\\lVert ', }; -function bindCharBracketPair(open, ctrlSeq) { +var BRACKET_NAMES = { + '⟨': 'angle-bracket', + '⟩': 'angle-bracket', + '|': 'pipe' +}; + +function bindCharBracketPair(open, ctrlSeq, name) { var ctrlSeq = ctrlSeq || open, close = OPP_BRACKS[open], end = OPP_BRACKS[ctrlSeq]; CharCmds[open] = bind(Bracket, L, open, close, ctrlSeq, end); CharCmds[close] = bind(Bracket, R, open, close, ctrlSeq, end); + BRACKET_NAMES[open] = BRACKET_NAMES[close] = name; } -bindCharBracketPair('('); -bindCharBracketPair('['); -bindCharBracketPair('{', '\\{'); +bindCharBracketPair('(', null, 'parenthesis'); +bindCharBracketPair('[', null, 'bracket'); +bindCharBracketPair('{', '\\{', 'brace'); LatexCmds.langle = bind(Bracket, L, '⟨', '⟩', '\\langle ', '\\rangle '); LatexCmds.rangle = bind(Bracket, R, '⟨', '⟩', '\\langle ', '\\rangle '); CharCmds['|'] = bind(Bracket, L, '|', '|', '|', '|'); LatexCmds.lVert = bind(Bracket, L, '∥', '∥', '\\lVert ', '\\rVert '); LatexCmds.rVert = bind(Bracket, R, '∥', '∥', '\\lVert ', '\\rVert '); + LatexCmds.left = P(MathCommand, function(_) { _.parser = function() { var regex = Parser.regex; @@ -769,13 +1055,13 @@ LatexCmds.left = P(MathCommand, function(_) { return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle(?![a-zA-Z])|\\lVert(?![a-zA-Z]))/)) .then(function(ctrlSeq) { - var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : ctrlSeq); + var open = ctrlSeq.replace(/^\\/, ''); if (ctrlSeq=="\\langle") { open = '⟨'; ctrlSeq = ctrlSeq + ' '; } if (ctrlSeq=="\\lVert") { open = '∥'; ctrlSeq = ctrlSeq + ' '; } return latexMathParser.then(function (block) { return string('\\right').skip(optWhitespace) .then(regex(/^(?:[\])|]|\\\}|\\rangle(?![a-zA-Z])|\\rVert(?![a-zA-Z]))/)).map(function(end) { - var close = (end.charAt(0) === '\\' ? end.slice(1) : end); + var close = end.replace(/^\\/, ''); if (end=="\\rangle") { close = '⟩'; end = end + ' '; } if (end=="\\rVert") { close = '∥'; end = end + ' '; } var cmd = Bracket(0, open, close, ctrlSeq, end); @@ -799,20 +1085,29 @@ LatexCmds.right = P(MathCommand, function(_) { var Binomial = LatexCmds.binom = LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) { + var leftSymbol = SVG_SYMBOLS['(']; + var rightSymbol = SVG_SYMBOLS[')']; + _.ctrlSeq = '\\binom'; _.htmlTemplate = - '' - + '(' - + '' + '' + + '' + + leftSymbol.html + + '' + + '' + '' + '&0' + '&1' + '' + '' - + ')' + + '' + + rightSymbol.html + + '' + '' ; _.textTemplate = ['choose(',',',')']; + _.mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial']; + _.ariaLabel = 'binomial'; }); var Choose = diff --git a/src/commands/text.js b/src/commands/text.js index f1dcc4771..fdaaf5da0 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -10,6 +10,7 @@ */ var TextBlock = P(Node, function(_, super_) { _.ctrlSeq = '\\text'; + _.ariaLabel = 'Text'; _.replaces = function(replacedText) { if (replacedText instanceof Fragment) @@ -35,7 +36,7 @@ var TextBlock = P(Node, function(_, super_) { if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); - textBlock.bubble('reflow'); + textBlock.bubble(function (node) { node.reflow(); }); }; _.parser = function() { @@ -65,7 +66,7 @@ var TextBlock = P(Node, function(_, super_) { _.latex = function() { var contents = this.textContents(); if (contents.length === 0) return ''; - return '\\text{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}'; + return this.ctrlSeq + '{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}'; }; _.html = function() { return ( @@ -74,12 +75,29 @@ var TextBlock = P(Node, function(_, super_) { + '' ); }; + _.mathspeakTemplate = ['Start'+_.ariaLabel, 'End'+_.ariaLabel]; + _.mathspeak = function(opts) { + if (opts && opts.ignoreShorthand) { + return this.mathspeakTemplate[0]+', '+this.textContents() +', '+this.mathspeakTemplate[1] + } else { + return this.textContents(); + } + }; + _.isTextBlock = function() { + return true; + }; // editability methods: called by the cursor for editing, cursor movements, // and selection of the MathQuill tree, these all take in a direction and // the cursor - _.moveTowards = function(dir, cursor) { cursor.insAtDirEnd(-dir, this); }; - _.moveOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; + _.moveTowards = function(dir, cursor) { + cursor.insAtDirEnd(-dir, this); + aria.queueDirEndOf(-dir).queue(cursor.parent, true); + }; + _.moveOutOf = function(dir, cursor) { + cursor.insDirOf(dir, this); + aria.queueDirOf(dir).queue(this); + }; _.unselectInto = _.moveTowards; // TODO: make these methods part of a shared mixin or something. @@ -115,12 +133,14 @@ var TextBlock = P(Node, function(_, super_) { cursor.insLeftOf(this); super_.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness } - this.bubble('reflow'); + this.bubble(function (node) { node.reflow(); }); + // TODO needs tests + aria.alert(ch); }; _.writeLatex = function(cursor, latex) { if (!cursor[L]) TextPiece(latex).createLeftOf(cursor); else cursor[L].appendText(latex); - this.bubble('reflow'); + this.bubble(function (node) { node.reflow(); }); }; _.seek = function(pageX, cursor) { @@ -248,29 +268,34 @@ var TextPiece = P(Node, function(_, super_) { var from = this[-dir]; if (from) from.insTextAtDirEnd(ch, dir); else TextPiece(ch).createDir(-dir, cursor); - return this.deleteTowards(dir, cursor); }; + _.mathspeak = _.latex = function() { return this.text; }; _.deleteTowards = function(dir, cursor) { if (this.text.length > 1) { + var deletedChar; if (dir === R) { this.dom.deleteData(0, 1); + deletedChar = this.text[0]; this.text = this.text.slice(1); } else { // note that the order of these 2 lines is annoyingly important // (the second line mutates this.text.length) this.dom.deleteData(-1 + this.text.length, 1); + deletedChar = this.text[this.text.length - 1]; this.text = this.text.slice(0, -1); } + aria.queue(deletedChar); } else { this.remove(); this.jQ.remove(); cursor[dir] = this[dir]; + aria.queue(this.text); } }; @@ -308,28 +333,33 @@ LatexCmds.textrm = LatexCmds.textup = LatexCmds.textmd = TextBlock; -function makeTextBlock(latex, tagName, attrs) { +function makeTextBlock(latex, ariaLabel, tagName, attrs) { return P(TextBlock, { ctrlSeq: latex, - htmlTemplate: '<'+tagName+' '+attrs+'>&0' + ariaLabel: ariaLabel, + mathspeakTemplate: ['Start'+ariaLabel, 'End'+ariaLabel], + html: function() { + var cmdId = 'mathquill-command-id=' + this.id; + return '<'+tagName+' '+attrs+' '+cmdId+'>'+this.textContents()+''; + } }); } LatexCmds.em = LatexCmds.italic = LatexCmds.italics = LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = - makeTextBlock('\\textit', 'i', 'class="mq-text-mode"'); + makeTextBlock('\\textit', 'Italic', 'i', 'class="mq-text-mode"'); LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = - makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"'); + makeTextBlock('\\textbf', 'Bold', 'b', 'class="mq-text-mode"'); LatexCmds.sf = LatexCmds.textsf = - makeTextBlock('\\textsf', 'span', 'class="mq-sans-serif mq-text-mode"'); + makeTextBlock('\\textsf', 'Sans serif font', 'span', 'class="mq-sans-serif mq-text-mode"'); LatexCmds.tt = LatexCmds.texttt = - makeTextBlock('\\texttt', 'span', 'class="mq-monospace mq-text-mode"'); + makeTextBlock('\\texttt', 'Mono space font', 'span', 'class="mq-monospace mq-text-mode"'); LatexCmds.textsc = - makeTextBlock('\\textsc', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); + makeTextBlock('\\textsc', 'Variable font', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); LatexCmds.uppercase = - makeTextBlock('\\uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); + makeTextBlock('\\uppercase', 'Uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); LatexCmds.lowercase = - makeTextBlock('\\lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); + makeTextBlock('\\lowercase', 'Lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); var RootMathCommand = P(MathCommand, function(_, super_) { diff --git a/src/controller.js b/src/controller.js index 73c8a4bba..3d2b999f3 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,9 +15,12 @@ var Controller = P(function(_) { this.container = container; this.options = options; + this.ariaLabel = 'Math Input'; + this.ariaPostLabel = ''; + root.controller = this; - this.cursor = root.cursor = Cursor(root, options); + this.cursor = root.cursor = Cursor(root, options, this); // TODO: stop depending on root.cursor, and rm it }; @@ -38,4 +41,66 @@ var Controller = P(function(_) { } return this; }; + _.setAriaLabel = function(ariaLabel) { + var oldAriaLabel = this.getAriaLabel(); + if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') { + this.ariaLabel = ariaLabel; + } else if (this.editable) { + this.ariaLabel = 'Math Input'; + } else { + this.ariaLabel = ''; + } + // If this field doesn't have focus, update its computed mathspeak value. + // We check for focus because updating the aria-label attribute of a focused element will cause most screen readers to announce the new value (in our case, label along with the expression's mathspeak). + // If the field does have focus at the time, it will be updated once a blur event occurs. + // Unless we stop using fake text inputs and emulating screen reader behavior, this is going to remain a problem. + if (this.ariaLabel !== oldAriaLabel && !this.containerHasFocus()) { + this.updateMathspeak(); + } + return this; + }; + _.getAriaLabel = function () { + if (this.ariaLabel !== 'Math Input') { + return this.ariaLabel; + } else if (this.editable) { + return 'Math Input'; + } else { + return ''; + } + }; + _.setAriaPostLabel = function(ariaPostLabel, timeout) { + if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel !== '') { + if ( + ariaPostLabel !== this.ariaPostLabel && + typeof timeout === 'number' + ) { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this._ariaAlertTimeout = setTimeout(function() { + if (this.containerHasFocus()) { + // Voice the new label, but do not update content mathspeak to prevent double-speech. + aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); + } else { + // This mathquill does not have focus, so update its mathspeak. + this.updateMathspeak(); + } + }.bind(this), timeout); + } + this.ariaPostLabel = ariaPostLabel; + } else { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this.ariaPostLabel = ''; + } + return this; + }; + _.getAriaPostLabel = function () { + return this.ariaPostLabel || ''; + }; + _.containerHasFocus = function () { + return ( + document.activeElement && + this.container && + this.container[0] && + this.container[0].contains(document.activeElement) + ); + }; }); diff --git a/src/css/editable.less b/src/css/editable.less index 26bd9671e..0a0fcd6c8 100644 --- a/src/css/editable.less +++ b/src/css/editable.less @@ -1,7 +1,7 @@ .mq-editable-field { .inline-block; .mq-cursor { - border-left: 1px solid black; + border-left: 1px solid currentColor; margin-left: -1px; position: relative; z-index: 1; @@ -19,7 +19,7 @@ &.mq-focused { .box-shadow(~"#8bd 0 0 1px 2px, inset #6ae 0 0 2px 0"); border-color: #709AC0; - border-radius: 1px; + aria-hidden: true; } } // special styles for editables within static math diff --git a/src/css/main.less b/src/css/main.less index ba94dd2ed..691e742d5 100644 --- a/src/css/main.less +++ b/src/css/main.less @@ -18,4 +18,3 @@ @import "selections.less"; @import "textarea.less"; -@import "matrixed.less"; diff --git a/src/css/math.less b/src/css/math.less index dfa53e265..107190a62 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -1,3 +1,9 @@ +// look here to see the digit layout strategy: +// https://www.desmos.com/calculator/ctvh9utz0t +@digit-separator: .11em; +@expand-margin: .009em; +@contract-margin: -.01em; + .mq-root-block, .mq-math-mode .mq-root-block { .inline-block; width: 100%; @@ -6,7 +12,40 @@ white-space: nowrap; overflow: hidden; vertical-align: middle; + + .mq-digit { + margin-left: @expand-margin; + margin-right: @expand-margin; + } + + .mq-group-start { + margin-left: @digit-separator; + margin-right: @contract-margin; + } + + .mq-group-other { + margin-left: @contract-margin; + margin-right: @contract-margin; + } + + .mq-group-leading-1, .mq-group-leading-2 { + margin-left: 0; + margin-right: @contract-margin; + } + + .mq-group-leading-3 { + margin-left: 4 * @expand-margin; + margin-right: @contract-margin; + } + + &.mq-suppress-grouping { + .mq-group-start, .mq-group-other, .mq-group-leading-1, .mq-group-leading-2, .mq-group-leading-3 { + margin-left: @expand-margin; + margin-right: @expand-margin; + } + } } + .mq-math-mode { font-variant: normal; font-weight: normal; @@ -25,6 +64,20 @@ line-height: .9; } + svg { + // svg symbols are sometimes used for autoscaling brackets and + // square root symbols. This piece of css magic allows you to copy + // over the current value of the font color to the svg symbols. + fill: currentColor; + + // the svg symbols fill their container + position:absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + * { font-size: inherit; line-height: inherit; @@ -37,17 +90,19 @@ // TODO: what's the difference between these? .mq-empty { - background: #ccc; + background: rgba(0,0,0,.2); &.mq-root-block { background: transparent; } + &.mq-empty-parens, &.mq-empty-square-brackets { + background: transparent + } } &.mq-empty { background: transparent; } - .mq-text-mode { display: inline-block; white-space: pre; @@ -127,11 +182,11 @@ } .mq-overline { - border-top: 1px solid black; + border-top: 1px solid; margin-top: 1px; } .mq-underline { - border-bottom: 1px solid black; + border-bottom: 1px solid; margin-bottom: 1px; } @@ -160,7 +215,7 @@ &.mq-sup-only { vertical-align: .5em; - .mq-sup { + & > .mq-sup { display: inline-block; vertical-align: text-bottom; } @@ -196,21 +251,24 @@ //// // parentheses - .mq-paren { - padding: 0 .1em; - vertical-align: top; - -webkit-transform-origin: center .06em; - -moz-transform-origin: center .06em; - -ms-transform-origin: center .06em; - -o-transform-origin: center .06em; - transform-origin: center .06em; - - &.mq-ghost { color: silver; } - - + span { - margin-top: .1em; - margin-bottom: .1em; - } + .mq-ghost svg { opacity: .2 } + .mq-bracket-middle { + margin-top: .1em; + margin-bottom: .1em; + } + .mq-bracket-l, .mq-bracket-r { + position: absolute; + top: 0; + bottom: 2px; + } + .mq-bracket-l { + left: 0; + } + .mq-bracket-r { + right:0; + } + .mq-bracket-container { + position: relative; } .mq-array { @@ -285,16 +343,20 @@ // \sqrt // square roots .mq-sqrt-prefix { - padding-top: 0; + position: absolute; + top: 1px; + bottom: 0.15em; + width: 0.95em; + } + + .mq-sqrt-container { position: relative; - top: 0.1em; - vertical-align: top; - .transform-origin(top); } .mq-sqrt-stem { border-top: 1px solid; margin-top: 1px; + margin-left: 0.9em; padding-left: .15em; padding-right: .2em; margin-right: .1em; @@ -311,7 +373,7 @@ display: block; text-align: center; } - + .mq-hat-prefix { display: block; text-align: center; @@ -369,16 +431,24 @@ padding-top: 0.2em; text-align: center; - &:before { - display: block; - position: relative; - top: -0.34em; + &:after { + position: absolute; + right: -0.1em; + top: -0.48em; font-size: 0.5em; - line-height: 0em; content: '\27A4'; - text-align: right; } - &.mq-arrow-left:before { + //really wish I could use :not here, but less doesn't seem to be happy with that + &.mq-arrow-left:after { + content: ''; + display: none; + } + &.mq-arrow-left:before, &.mq-arrow-leftright:before { + position: absolute; + top: -0.48em; + left: -0.1em; + font-size: 0.5em; + content: '\27A4'; -moz-transform: scaleX(-1); -o-transform: scaleX(-1); -webkit-transform: scaleX(-1); diff --git a/src/css/matrixed.less b/src/css/matrixed.less deleted file mode 100644 index 4ebf2d211..000000000 --- a/src/css/matrixed.less +++ /dev/null @@ -1,20 +0,0 @@ -@import "./mixins/display"; - -// We have to set an opaque background color for matrix-stretched -// elements to anti-alias correctly, so we use the Chroma filter -// on the immediate parent to make the solid background color -// transparent. - -// See http://github.com/laughinghan/mathquill/wiki/Transforms -// for more details. - -.mq-math-mode { - .mq-matrixed { - background: white; - .inline-block; - } - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); - margin-top: -.1em; - } -} diff --git a/src/css/mixins/css3.less b/src/css/mixins/css3.less index b53bf6b93..e1a608aab 100644 --- a/src/css/mixins/css3.less +++ b/src/css/mixins/css3.less @@ -1,10 +1,3 @@ -.transform-origin (...) { - -webkit-transform-origin: @arguments; - -moz-transform-origin: @arguments; - -ms-transform-origin: @arguments; - -o-transform-origin: @arguments; - transform-origin: @arguments; -} .transform (...) { -webkit-transform: @arguments; -moz-transform: @arguments; diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index 1b08a3afe..0d1a9b9b4 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -2,3 +2,24 @@ display: -moz-inline-box; display: inline-block; } + +// ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. +.mq-aria-alert { + position: absolute; + left: -1000px; + top: -1000px; + width: 0px; + height: 0px; + text-align: left; + overflow: hidden; +} + +.mq-mathspeak { + position: absolute; + left: -1000px; + top: -1000px; + width: 0px; + height: 0px; + text-align: left; + overflow: hidden; +} diff --git a/src/css/selections.less b/src/css/selections.less index 930de6c62..eb8702a4c 100644 --- a/src/css/selections.less +++ b/src/css/selections.less @@ -12,45 +12,29 @@ .mq-selection { &, & .mq-non-leaf, & .mq-scaled { background: #B4D5FE !important; - background: Highlight !important; - color: HighlightText; - border-color: HighlightText; - } - - .mq-matrixed { - // The Chroma filter doesn't support the 'Highlight' keyword, - // but is only used in IE 8 and below anyway, so just use the - // default Windows highlight color. Even if the highlight color - // of the system has been customized, it's not a big deal, - // most of the solid blue area is chroma keyed, there'll just - // be a blue anti-aliased fringe around the matrix-filter- - // stretched text. - - // If you use IE 8 or below and customized your highlight - // color, and after the effort I put into making everything - // else in MathQuill work in IE 8 and below have the *gall* - // to complain about the blue fringe that appears in selections - // around the otherwise beautifully stretched square roots and - // stuff, and you have no ideas for how to solve the problem, - // just a complaint, then I'd like to politely suggest that you - // go choke on a dick. Unless you're into that, in which case, - // go do something that would make you unhappy instead. - - background: #39F !important; - } - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; } &.mq-blur { - &, & .mq-non-leaf, & .mq-scaled, & .mq-matrixed { + &, & .mq-non-leaf, & .mq-scaled { background: #D4D4D4 !important; color: black; border-color: black; } + } + } +} + - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; +html body { // adding 'html body' for specificity + .mq-math-mode, .mq-editable-field { + .mq-selection { + // do not show a background inside any of the + // children of nthroot. We draw a background on + // the nthroot itself. We don't want the index + // to be covered up by the background of the + // radical. + .mq-nthroot-container * { + background: transparent !important; } } } diff --git a/src/cursor.js b/src/cursor.js index 3a6b0f2c7..3df5ac0b6 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -11,7 +11,8 @@ JS environment could actually contain many instances. */ //A fake cursor in the fake textbox that the math is rendered in. var Cursor = P(Point, function(_) { - _.init = function(initParent, options) { + _.init = function(initParent, options, controller) { + this.controller = controller; this.parent = initParent; this.options = options; @@ -98,6 +99,7 @@ var Cursor = P(Point, function(_) { var pageX = self.offset().left; to.seek(pageX, self); } + aria.queue(to, true); }; _.offset = function() { //in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset() @@ -232,7 +234,13 @@ var Cursor = P(Point, function(_) { this.selectionChanged(); return true; }; - + _.resetToEnd = function (controller) { + this.clearSelection(); + var root = controller.root; + this[R] = 0; + this[L] = root.ends[R]; + this.parent = root; + }; _.clearSelection = function() { if (this.selection) { this.selection.clear(); @@ -290,9 +298,10 @@ var Selection = P(Fragment, function(_, super_) { this.jQ.replaceWith(this.jQ[0].childNodes); return this; }; - _.join = function(methodName) { + _.join = function(methodName, separatorToken) { + var separator = separatorToken || ''; return this.fold('', function(fold, child) { - return fold + child[methodName](); + return fold + separator + child[methodName](); }); }; }); diff --git a/src/fonts/Symbola-basic.css b/src/fonts/Symbola-basic.css new file mode 100644 index 000000000..3a8186b98 --- /dev/null +++ b/src/fonts/Symbola-basic.css @@ -0,0 +1,4 @@ +@font-face { + font-family: Symbola; + src: url(data:application/font-woff;base64,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) format('woff'); +} \ No newline at end of file diff --git a/src/fonts/Symbola-basic.eot b/src/fonts/Symbola-basic.eot index 2e39ec7bb..3c1edaa5a 100644 Binary files a/src/fonts/Symbola-basic.eot and b/src/fonts/Symbola-basic.eot differ diff --git a/src/fonts/Symbola-basic.ttf b/src/fonts/Symbola-basic.ttf index cf968b9ad..d1eda7b9e 100644 Binary files a/src/fonts/Symbola-basic.ttf and b/src/fonts/Symbola-basic.ttf differ diff --git a/src/fonts/Symbola-basic.woff b/src/fonts/Symbola-basic.woff index 104a15090..a262924b9 100644 Binary files a/src/fonts/Symbola-basic.woff and b/src/fonts/Symbola-basic.woff differ diff --git a/src/fonts/Symbola-basic.woff2 b/src/fonts/Symbola-basic.woff2 index 7c0d6c8cf..44e703bb4 100644 Binary files a/src/fonts/Symbola-basic.woff2 and b/src/fonts/Symbola-basic.woff2 differ diff --git a/src/intro.js b/src/intro.js index 2057d8404..1ff07bbb6 100644 --- a/src/intro.js +++ b/src/intro.js @@ -12,8 +12,6 @@ var jQuery = window.jQuery, undefined, - mqCmdId = 'mathquill-command-id', - mqBlockId = 'mathquill-block-id', min = Math.min, max = Math.max; @@ -21,82 +19,11 @@ if (!jQuery) throw 'MathQuill requires jQuery 1.5.2+ to be loaded first'; function noop() {} -/** - * A utility higher-order function that makes defining variadic - * functions more convenient by letting you essentially define functions - * with the last argument as a splat, i.e. the last argument "gathers up" - * remaining arguments to the function: - * var doStuff = variadic(function(first, rest) { return rest; }); - * doStuff(1, 2, 3); // => [2, 3] - */ -var __slice = [].slice; -function variadic(fn) { - var numFixedArgs = fn.length - 1; - return function() { - var args = __slice.call(arguments, 0, numFixedArgs); - var varArg = __slice.call(arguments, numFixedArgs); - return fn.apply(this, args.concat([ varArg ])); - }; -} - -/** - * A utility higher-order function that makes combining object-oriented - * programming and functional programming techniques more convenient: - * given a method name and any number of arguments to be bound, returns - * a function that calls it's first argument's method of that name (if - * it exists) with the bound arguments and any additional arguments that - * are passed: - * var sendMethod = send('method', 1, 2); - * var obj = { method: function() { return Array.apply(this, arguments); } }; - * sendMethod(obj, 3, 4); // => [1, 2, 3, 4] - * // or more specifically, - * var obj2 = { method: function(one, two, three) { return one*two + three; } }; - * sendMethod(obj2, 3); // => 5 - * sendMethod(obj2, 4); // => 6 - */ -var send = variadic(function(method, args) { - return variadic(function(obj, moreArgs) { - if (method in obj) return obj[method].apply(obj, args.concat(moreArgs)); - }); -}); - -/** - * A utility higher-order function that creates "implicit iterators" - * from "generators": given a function that takes in a sole argument, - * a "yield_" function, that calls "yield_" repeatedly with an object as - * a sole argument (presumably objects being iterated over), returns - * a function that calls it's first argument on each of those objects - * (if the first argument is a function, it is called repeatedly with - * each object as the first argument, otherwise it is stringified and - * the method of that name is called on each object (if such a method - * exists)), passing along all additional arguments: - * var a = [ - * { method: function(list) { list.push(1); } }, - * { method: function(list) { list.push(2); } }, - * { method: function(list) { list.push(3); } } - * ]; - * a.each = iterator(function(yield_) { - * for (var i in this) yield_(this[i]); - * }); - * var list = []; - * a.each('method', list); - * list; // => [1, 2, 3] - * // Note that the for-in loop will yield 'each', but 'each' maps to - * // the function object created by iterator() which does not have a - * // .method() method, so that just fails silently. - */ -function iterator(generator) { - return variadic(function(fn, args) { - if (typeof fn !== 'function') fn = send(fn); - var yield_ = function(obj) { return fn.apply(obj, [ obj ].concat(args)); }; - return generator.call(this, yield_); - }); -} - /** * sugar to make defining lots of commands easier. * TODO: rethink this. */ +var __slice = [].slice; function bind(cons /*, args... */) { var args = __slice.call(arguments, 1); return function() { diff --git a/src/publicapi.js b/src/publicapi.js index 85afad967..c3a2dab1a 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -70,8 +70,8 @@ function getInterface(v) { function MQ(el) { if (!el || !el.nodeType) return null; // check that `el` is a HTML element, using the // same technique as jQuery: https://github.com/jquery/jquery/blob/679536ee4b7a92ae64a5f58d90e9cc38c001e807/src/core/init.js#L92 - var blockId = $(el).children('.mq-root-block').attr(mqBlockId); - var ctrlr = blockId && Node.byId[blockId].controller; + var blockNode = Node.getNodeOfElement($(el).children('.mq-root-block')[0]); + var ctrlr = blockNode && blockNode.controller; return ctrlr ? APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null; }; var APIClasses = {}; @@ -109,8 +109,8 @@ function getInterface(v) { ctrlr.createTextarea(); var contents = el.addClass(classNames).contents().detach(); - root.jQ = - $('').attr(mqBlockId, root.id).appendTo(el); + root.jQ = $('').appendTo(el); + Node.linkElementByBlockId(root.jQ[0], root.id); this.latex(contents.text()); this.revert = function() { @@ -122,6 +122,7 @@ function getInterface(v) { _.config = function(opts) { config(this.__options, opts); return this; }; _.el = function() { return this.__controller.container[0]; }; _.text = function() { return this.__controller.exportText(); }; + _.mathspeak = function() { return this.__controller.exportMathSpeak(); }; _.latex = function(latex) { if (arguments.length > 0) { this.__controller.renderLatexMath(latex); @@ -138,7 +139,7 @@ function getInterface(v) { .replace(/ class=(""|(?= |>))/g, ''); }; _.reflow = function() { - this.__controller.root.postOrder('reflow'); + this.__controller.root.postOrder(function (node) { node.reflow(); }); return this; }; }); @@ -152,7 +153,11 @@ function getInterface(v) { this.__controller.editablesTextareaEvents(); return this; }; - _.focus = function() { this.__controller.textarea.focus(); return this; }; + _.focus = function() { + this.__controller.textarea[0].focus(); + this.__controller.scrollHoriz(); + return this; + }; _.blur = function() { this.__controller.textarea.blur(); return this; }; _.write = function(latex) { this.__controller.writeLatex(latex); @@ -162,7 +167,7 @@ function getInterface(v) { }; _.empty = function() { var root = this.__controller.root, cursor = this.__controller.cursor; - root.eachChild('postOrder', 'dispose'); + root.ends[L] = root.ends[R] = 0; root.jQ.empty(); delete cursor.selection; @@ -178,11 +183,12 @@ function getInterface(v) { cmd = klass(cmd); if (cursor.selection) cmd.replaces(cursor.replaceSelection()); cmd.createLeftOf(cursor.show()); - this.__controller.scrollHoriz(); } else /* TODO: API needs better error reporting */; } else cursor.parent.write(cursor, cmd); + + ctrlr.scrollHoriz(); if (ctrlr.blurred) cursor.hide().parent.blur(); return this; }; @@ -204,10 +210,10 @@ function getInterface(v) { _.moveToLeftEnd = function() { return this.moveToDirEnd(L); }; _.moveToRightEnd = function() { return this.moveToDirEnd(R); }; - _.keystroke = function(keys) { + _.keystroke = function(keys, evt) { var keys = keys.replace(/^\s+|\s+$/g, '').split(/\s+/); for (var i = 0; i < keys.length; i += 1) { - this.__controller.keystroke(keys[i], { preventDefault: noop }); + this.__controller.keystroke(keys[i], evt || { preventDefault: noop }); } return this; }; @@ -224,9 +230,22 @@ function getInterface(v) { var cmd = Embed().setOptions(options); cmd.createLeftOf(this.__controller.cursor); }; + _.setAriaLabel = function(ariaLabel) { + this.__controller.setAriaLabel(ariaLabel); + return this; + }; + _.getAriaLabel = function () { + return this.__controller.getAriaLabel(); + }; + _.setAriaPostLabel = function(ariaPostLabel, timeout) { + this.__controller.setAriaPostLabel(ariaPostLabel, timeout); + return this; + }; + _.getAriaPostLabel = function () { + return this.__controller.getAriaPostLabel(); + }; _.clickAt = function(clientX, clientY, target) { target = target || document.elementFromPoint(clientX, clientY); - var ctrlr = this.__controller, root = ctrlr.root; if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0]; ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset); diff --git a/src/services/aria.js b/src/services/aria.js new file mode 100755 index 000000000..ea7f43096 --- /dev/null +++ b/src/services/aria.js @@ -0,0 +1,87 @@ +/***************************************** + + * Add the capability for mathquill to generate ARIA alerts. Necessary so MQ can convey information as a screen reader user navigates the fake MathQuill textareas. + * Official ARIA specification: https://www.w3.org/TR/wai-aria/ + * WAI-ARIA is still catching on, thus only more recent browsers support it, and even then to varying degrees. + * The below implementation attempts to be as broad as possible and may not conform precisely to the spec. But, neither do any browsers or adaptive technologies at this point. + * At time of writing, IE 11, FF 44, and Safari 8.0.8 work. Older versions of these browsers should speak as well, but haven't tested precisely which earlier editions pass. + + * Tested AT: on Windows, Window-Eyes, ZoomText Fusion, NVDA, and JAWS (all supported). + * VoiceOver on Mac platforms also supported (only tested with OSX 10.10.5 and iOS 9.2.1+). + * Chrome 54+ on Android works reliably with Talkback. + ****************************************/ + +var Aria = P(function(_) { + _.init = function() { + this.jQ = jQuery([]); // empty element + // Add the alert DOM element only after the page has loaded. + jQuery(document).ready(function() { + var el = '.mq-aria-alert'; + // No matter how many Mathquill instances exist, we only need one alert object to say something. + if (!jQuery(el).length) jQuery('body').append("

"); // make this as noisy as possible in hopes that all modern screen reader/browser combinations will speak when triggered later. + this.jQ = jQuery(el); + }.bind(this)); + this.items = []; + this.msg = ''; + }; + + _.queue = function(item, shouldDescribe) { + var output = ''; + if (item instanceof Node) { + // Some constructs include verbal shorthand (such as simple fractions and exponents). + // Since ARIA alerts relate to moving through interactive content, we don't want to use that shorthand if it exists + // since doing so may be ambiguous or confusing. + var itemMathspeak = item.mathspeak({ignoreShorthand: true}); + if (shouldDescribe) { // used to ensure item is described when cursor reaches block boundaries + if ( + item.parent && + item.parent.ariaLabel && + item.ariaLabel === 'block' + ) { + output = item.parent.ariaLabel+' '+itemMathspeak; + } else if (item.ariaLabel) { + output = item.ariaLabel+' '+itemMathspeak; + } + } + if (output === '') { + output = itemMathspeak; + } + } else { + output = item; + } + this.items.push(output); + return this; + }; + _.queueDirOf = function(dir) { + prayDirection(dir); + return this.queue(dir === L ? 'before' : 'after'); + }; + _.queueDirEndOf = function(dir) { + prayDirection(dir); + return this.queue(dir === L ? 'beginning of' : 'end of'); + }; + + _.alert = function(t) { + if (t) this.queue(t); + if (this.items.length) { + this.msg = this.items.join(' ').replace(/ +(?= )/g,'').trim(); + this.jQ.empty().text(this.msg); + } + return this.clear(); + }; + + _.clear = function() { + this.items.length = 0; + return this; + }; +}); + +// We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules. +var aria = Aria(); + +Controller.open(function(_) { + _.aria = aria; + // based on http://www.gh-mathspeak.com/examples/quick-tutorial/ + // and http://www.gh-mathspeak.com/examples/grammar-rules/ + _.exportMathSpeak = function() { return this.root.mathspeak(); }; +}); diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js index be876a23c..148082a91 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.js @@ -1,25 +1,63 @@ Controller.open(function(_) { + this.onNotify(function (e) { + // these try to cover all ways that mathquill can be modified + if (e === 'edit' || e === 'replace' || e === undefined) { + var controller = this.controller; + if (!controller) return; + if (!controller.options.enableDigitGrouping) return; + + // blurred === false means we are focused. blurred === true or + // blurred === undefined means we are not focused. + if (controller.blurred !== false) return; + + controller.disableGroupingForSeconds(1); + } + }); + + _.disableGroupingForSeconds = function (seconds) { + clearTimeout(this.__disableGroupingTimeout); + var jQ = this.root.jQ; + + if (seconds === 0) { + jQ.removeClass('mq-suppress-grouping'); + } else { + jQ.addClass('mq-suppress-grouping'); + this.__disableGroupingTimeout = setTimeout(function () { + jQ.removeClass('mq-suppress-grouping'); + }, seconds * 1000); + } + } + _.focusBlurEvents = function() { var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; var blurTimeout; ctrlr.textarea.focus(function() { + ctrlr.updateMathspeak(); ctrlr.blurred = false; clearTimeout(blurTimeout); ctrlr.container.addClass('mq-focused'); - if (!cursor.parent) - cursor.insAtRightEnd(root); + if (!cursor.parent) cursor.insAtRightEnd(root); if (cursor.selection) { cursor.selection.jQ.removeClass('mq-blur'); ctrlr.selectionChanged(); //re-select textarea contents after tabbing away and back - } - else + } else { cursor.show(); + } + ctrlr.setOverflowClasses(); + }).blur(function() { + if (ctrlr.textareaSelectionTimeout) { + clearTimeout(ctrlr.textareaSelectionTimeout); + ctrlr.textareaSelectionTimeout = undefined; + } + ctrlr.disableGroupingForSeconds(0); ctrlr.blurred = true; blurTimeout = setTimeout(function() { // wait for blur on window; if - root.postOrder('intentionalBlur'); // none, intentional blur: #264 + root.postOrder(function (node) { node.intentionalBlur(); }); // none, intentional blur: #264 cursor.clearSelection().endSelection(); blur(); + ctrlr.updateMathspeak(); + ctrlr.scrollHoriz(); }); $(window).bind('blur', windowBlur); }); @@ -27,11 +65,16 @@ Controller.open(function(_) { clearTimeout(blurTimeout); // tabs/windows, not intentional blur if (cursor.selection) cursor.selection.jQ.addClass('mq-blur'); blur(); + ctrlr.updateMathspeak(); } function blur() { // not directly in the textarea blur handler so as to be cursor.hide().parent.blur(); // synchronous with/in the same frame as ctrlr.container.removeClass('mq-focused'); // clearing/blurring selection $(window).unbind('blur', windowBlur); + + if (ctrlr.options && ctrlr.options.resetCursorOnBlur) { + cursor.resetToEnd(ctrlr); + } } ctrlr.blurred = true; cursor.hide().parent.blur(); diff --git a/src/services/keystroke.js b/src/services/keystroke.js index a53cb5a4d..fa59223b5 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -39,11 +39,13 @@ Node.open(function(_) { // End -> move to the end of the current block. case 'End': ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); + aria.queue("end of").queue(cursor.parent, true); break; // Ctrl-End -> move all the way to the end of the root block. case 'Ctrl-End': ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); + aria.queue("end of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-End -> select to the end of the current block. @@ -53,21 +55,23 @@ Node.open(function(_) { } break; - // Ctrl-Shift-End -> select to the end of the root block. + // Ctrl-Shift-End -> select all the way to the end of the root block. case 'Ctrl-Shift-End': while (cursor[R] || cursor.parent !== ctrlr.root) { ctrlr.selectRight(); } break; - // Home -> move to the start of the root block or the current block. + // Home -> move to the start of the current block. case 'Home': ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); + aria.queue("beginning of").queue(cursor.parent, true); break; - // Ctrl-Home -> move to the start of the current block. + // Ctrl-Home -> move all the way to the start of the root block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); + aria.queue("beginning of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-Home -> select to the start of the current block. @@ -77,7 +81,7 @@ Node.open(function(_) { } break; - // Ctrl-Shift-Home -> move to the start of the root block. + // Ctrl-Shift-Home -> select all the way to the start of the root block. case 'Ctrl-Shift-Home': while (cursor[L] || cursor.parent !== ctrlr.root) { ctrlr.selectLeft(); @@ -129,9 +133,58 @@ Node.open(function(_) { while (cursor[L]) ctrlr.selectLeft(); break; + // These remaining hotkeys are only of benefit to people running screen readers. + case 'Ctrl-Alt-Up': // speak parent block that has focus + if (cursor.parent.parent && cursor.parent.parent instanceof Node) aria.queue(cursor.parent.parent); + else aria.queue('nothing above'); + break; + + case 'Ctrl-Alt-Down': // speak current block that has focus + if (cursor.parent && cursor.parent instanceof Node) aria.queue(cursor.parent); + else aria.queue('block is empty'); + break; + + case 'Ctrl-Alt-Left': // speak left-adjacent block + if ( + cursor.parent.parent && + cursor.parent.parent.ends && + cursor.parent.parent.ends[L] && + cursor.parent.parent.ends[L] instanceof Node + ) { + aria.queue(cursor.parent.parent.ends[L]); + } else { + aria.queue('nothing to the left'); + } + break; + + case 'Ctrl-Alt-Right': // speak right-adjacent block + if ( + cursor.parent.parent && + cursor.parent.parent.ends && + cursor.parent.parent.ends[R] && + cursor.parent.parent.ends[R] instanceof Node + ) { + aria.queue(cursor.parent.parent.ends[R]); + } else { + aria.queue('nothing to the right'); + } + break; + + case 'Ctrl-Alt-Shift-Down': // speak selection + if (cursor.selection) aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); + else aria.queue('nothing selected'); + break; + + case 'Ctrl-Alt-=': + case 'Ctrl-Alt-Shift-Right': // speak ARIA post label (evaluation or error) + if (ctrlr.ariaPostLabel.length) aria.queue(ctrlr.ariaPostLabel); + else aria.queue('no answer'); + break; + default: return; } + aria.alert(); e.preventDefault(); ctrlr.scrollHoriz(); }; @@ -162,6 +215,7 @@ Controller.open(function(_) { if (cursor.parent === this.root) return; cursor.parent.moveOutOf(dir, cursor); + aria.alert(); return this.notify('move'); }; @@ -224,6 +278,31 @@ Controller.open(function(_) { _.deleteDir = function(dir) { prayDirection(dir); var cursor = this.cursor; + var cursorEl = cursor[dir], cursorElParent = cursor.parent.parent; + if(cursorEl && cursorEl instanceof Node) { + if(cursorEl.sides) { + aria.queue(cursorEl.parent.chToCmd(cursorEl.sides[-dir].ch).mathspeak({createdLeftOf: cursor})); + // generally, speak the current element if it has no blocks, + // but don't for text block commands as the deleteTowards method + // in the TextCommand class is responsible for speaking the new character under the cursor. + } else if (!cursorEl.blocks && cursorEl.parent.ctrlSeq !== '\\text') { + aria.queue(cursorEl); + } + } else if(cursorElParent && cursorElParent instanceof Node) { + if(cursorElParent.sides) { + aria.queue(cursorElParent.parent.chToCmd(cursorElParent.sides[dir].ch).mathspeak({createdLeftOf: cursor})); + } else if (cursorElParent.blocks && cursorElParent.mathspeakTemplate) { + if (cursorElParent.upInto && cursorElParent.downInto) { // likely a fraction, and we just backspaced over the slash + aria.queue(cursorElParent.mathspeakTemplate[1]); + } else { + var mst = cursorElParent.mathspeakTemplate; + var textToQueue = dir === L ? mst[0] : mst[mst.length - 1]; + aria.queue(textToQueue); + } + } else { + aria.queue(cursorElParent); + } + } var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present @@ -234,7 +313,7 @@ Controller.open(function(_) { if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); - cursor.parent.bubble('reflow'); + cursor.parent.bubble(function (node) { node.reflow(); }); return this; }; @@ -244,16 +323,20 @@ Controller.open(function(_) { if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); this.notify('edit'); + var fragRemoved; if (dir === L) { - Fragment(cursor.parent.ends[L], cursor[L]).remove(); + fragRemoved = Fragment(cursor.parent.ends[L], cursor[L]); } else { - Fragment(cursor[R], cursor.parent.ends[R]).remove(); - }; + fragRemoved = Fragment(cursor[R], cursor.parent.ends[R]); + } + aria.queue(fragRemoved); + fragRemoved.remove(); + cursor.insAtDirEnd(dir, cursor.parent); if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); - cursor.parent.bubble('reflow'); + cursor.parent.bubble(function (node) { node.reflow(); }); return this; }; @@ -281,6 +364,7 @@ Controller.open(function(_) { cursor.clearSelection(); cursor.select() || cursor.show(); + if (cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); // clearing first because selection fires several times, and we don't want repeated speech. }; _.selectLeft = function() { return this.selectDir(L); }; _.selectRight = function() { return this.selectDir(R); }; diff --git a/src/services/latex.js b/src/services/latex.js index a9fd123a1..432c93261 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -18,6 +18,7 @@ var latexMathParser = (function() { var string = Parser.string; var regex = Parser.regex; var letter = Parser.letter; + var digit = Parser.digit; var any = Parser.any; var optWhitespace = Parser.optWhitespace; var succeed = Parser.succeed; @@ -26,6 +27,7 @@ var latexMathParser = (function() { // Parsers yielding either MathCommands, or Fragments of MathCommands // (either way, something that can be adopted by a MathBlock) var variable = letter.map(function(c) { return Letter(c); }); + var number = digit.map(function (c) { return Digit(c); }); var symbol = regex(/^[^${}\\_^]/).map(function(c) { return VanillaSymbol(c); }); var controlSequence = @@ -49,6 +51,7 @@ var latexMathParser = (function() { var command = controlSequence .or(variable) + .or(number) .or(symbol) ; @@ -74,8 +77,12 @@ var latexMathParser = (function() { })(); Controller.open(function(_, super_) { + _.cleanLatex = function (latex) { + //prune unnecessary spaces + return latex.replace(/(\\[a-z]+) (?![a-z])/ig,'$1') + } _.exportLatex = function() { - return this.root.latex().replace(/(\\[a-z]+) (?![a-z])/ig,'$1'); + return this.cleanLatex(this.root.latex()); }; optionProcessors.maxDepth = function(depth) { @@ -87,39 +94,208 @@ Controller.open(function(_, super_) { return this; }; - _.renderLatexMath = function(latex) { + + _.classifyLatexForEfficientUpdate = function (latex) { + if (typeof latex !== 'string') return; + + var matches = latex.match(/-?[0-9.]+$/g); + if (matches && matches.length === 1) { + return { + latex: latex, + prefix: latex.substr(0, latex.length - matches[0].length), + digits: matches[0] + }; + } + }; + _.renderLatexMathEfficiently = function (latex) { var root = this.root; - var cursor = this.cursor; - var options = cursor.options; - var jQ = root.jQ; + var oldLatex = this.exportLatex(); + if (root.ends[L] && root.ends[R] && oldLatex === latex) { + return true; + } + var oldClassification; + var classification = this.classifyLatexForEfficientUpdate(latex); + if (classification) { + oldClassification = this.classifyLatexForEfficientUpdate(oldLatex); + if (!oldClassification || oldClassification.prefix !== classification.prefix) { + return false; + } + } else { + return false; + } + + + // check if minus sign is changing + var oldDigits = oldClassification.digits; + var newDigits = classification.digits; + var oldMinusSign = false; + var newMinusSign = false; + if (oldDigits[0] === '-') { + oldMinusSign = true; + oldDigits = oldDigits.substr(1); + } + if (newDigits[0] === '-') { + newMinusSign = true; + newDigits = newDigits.substr(1); + } + + // start at the very end + var charNode = this.root.ends[R]; + var oldCharNodes = []; + for (var i= oldDigits.length - 1; i >= 0; i--) { + // the tree does not match what we expect + if (charNode.ctrlSeq !== oldDigits[i]) { + return false; + } + + // the trailing digits are not just under the root. We require the root + // to be the parent so that we can be sure we do not need a reflow to + // grow parens. + if (charNode.parent !== root) { + return false; + } + + // push to the start. We're traversing backwards + oldCharNodes.unshift(charNode); + + // move left one character + charNode = charNode[L]; + } + + // remove the minus sign + if (oldMinusSign && !newMinusSign) { + var oldMinusNode = charNode; + if (oldMinusNode.ctrlSeq !== '-') return false; + if (oldMinusNode[R] !== oldCharNodes[0]) return false; + if (oldMinusNode.parent !== root) return false; + if (oldMinusNode[L] && oldMinusNode[L].parent !== root) return false; + + oldCharNodes[0][L] = oldMinusNode[L]; + + if (root.ends[L] === oldMinusNode) root.ends[L] = oldCharNodes[0]; + if (oldMinusNode[L]) oldMinusNode[L][R] = oldCharNodes[0]; + + oldMinusNode.jQ.remove(); + } + + // add a minus sign + if (!oldMinusSign && newMinusSign) { + var newMinusNode = PlusMinus('-'); + var minusSpan = document.createElement('span'); + minusSpan.textContent = '-'; + newMinusNode.jQ = $(minusSpan); + + if (oldCharNodes[0][L]) oldCharNodes[0][L][R] = newMinusNode; + if (root.ends[L] === oldCharNodes[0]) root.ends[L] = newMinusNode; + + newMinusNode.parent = root; + newMinusNode[L] = oldCharNodes[0][L]; + newMinusNode[R] = oldCharNodes[0]; + oldCharNodes[0][L] = newMinusNode; + + newMinusNode.contactWeld(); // decide if binary operator + newMinusNode.jQ.insertBefore(oldCharNodes[0].jQ); + } + // update the text of the current nodes + var commonLength = Math.min(oldDigits.length, newDigits.length); + for (i=0; i < commonLength; i++) { + var newText = newDigits[i]; + charNode = oldCharNodes[i]; + if (charNode.ctrlSeq !== newText) { + charNode.ctrlSeq = newText; + charNode.jQ[0].textContent = newText; + charNode.mathspeakName = newText; + } + } + + // remove the extra digits at the end + if (oldDigits.length > newDigits.length) { + charNode = oldCharNodes[newDigits.length - 1]; + root.ends[R] = charNode; + charNode[R] = 0; + + for (i = oldDigits.length - 1; i >= commonLength; i--) { + oldCharNodes[i].jQ.remove(); + } + } + + // add new digits after the existing ones + if (newDigits.length > oldDigits.length) { + var frag = document.createDocumentFragment(); + + for (i = commonLength; i < newDigits.length; i++) { + var span = document.createElement('span'); + span.className = "mq-digit"; + span.textContent = newDigits[i]; + + var newNode = Digit(newDigits[i]); + newNode.parent = root; + newNode.jQ = $(span); + frag.appendChild(span); + + // splice this node in + newNode[L] = root.ends[R]; + newNode[R] = 0; + newNode[L][R] = newNode; + root.ends[R] = newNode; + } + + root.jQ[0].appendChild(frag); + } + + var currentLatex = this.exportLatex(); + if (currentLatex !== latex) { + console.warn('tried updating latex efficiently but did not work. Attempted: ' + latex + ' but wrote: ' + currentLatex); + return false; + } + + this.cursor.resetToEnd(this); + + var rightMost = root.ends[R]; + if (rightMost.fixDigitGrouping) { + rightMost.fixDigitGrouping(this.cursor.options); + } + + return true; + }; + _.renderLatexMathFromScratch = function (latex) { + var root = this.root, cursor = this.cursor; var all = Parser.all; var eof = Parser.eof; var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); - root.eachChild('postOrder', 'dispose'); root.ends[L] = root.ends[R] = 0; - if (block && block.prepareInsertionAt(cursor)) { + if (block) { block.children().adopt(root, 0, 0); + } + + var jQ = root.jQ; + + if (block) { var html = block.join('html'); jQ.html(html); root.jQize(jQ.children()); root.finalizeInsert(cursor.options); - } - else { + } else { jQ.empty(); } - + this.updateMathspeak(); delete cursor.selection; cursor.insAtRightEnd(root); }; + _.renderLatexMath = function(latex) { + this.notify('replace'); + + if (this.renderLatexMathEfficiently(latex)) return; + this.renderLatexMathFromScratch(latex); + }; _.renderLatexText = function(latex) { var root = this.root, cursor = this.cursor; root.jQ.children().slice(1).remove(); - root.eachChild('postOrder', 'dispose'); root.ends[L] = root.ends[R] = 0; delete cursor.selection; cursor.show().insAtRightEnd(root); diff --git a/src/services/mouse.js b/src/services/mouse.js index b2b543a51..b6d4b8b96 100644 --- a/src/services/mouse.js +++ b/src/services/mouse.js @@ -4,12 +4,29 @@ Controller.open(function(_) { Options.p.ignoreNextMousedown = noop; + + // Whenever edits to the tree occur, in-progress selection events + // must be invalidated and selection changes must not be applied to + // the edited tree. cancelSelectionOnEdit takes care of this. + var cancelSelectionOnEdit; + this.onNotify(function (e) { + if ((e === 'edit' || e === 'replace')) { + // this will be called any time ANY mathquill is edited. We only want + // to cancel selection if the selection is happening within the mathquill + // that dispatched the notify. Otherwise you won't be able to select any + // mathquills while a slider is playing. + if (cancelSelectionOnEdit && cancelSelectionOnEdit.cursor === this) { + cancelSelectionOnEdit.cb(); + } + } + }); + _.delegateMouseEvents = function() { var ultimateRootjQ = this.root.jQ; //drag-to-select event handling this.container.bind('mousedown.mathquill', function(e) { var rootjQ = $(e.target).closest('.mq-root-block'); - var root = Node.byId[rootjQ.attr(mqBlockId) || ultimateRootjQ.attr(mqBlockId)]; + var root = Node.getNodeOfElement(rootjQ[0]) || Node.getNodeOfElement(ultimateRootjQ[0]); var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink; var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea; @@ -24,30 +41,58 @@ Controller.open(function(_) { function docmousemove(e) { if (!cursor.anticursor) cursor.startSelection(); ctrlr.seek(target, e.pageX, e.pageY).cursor.select(); + if(cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak') + ' selected').alert(); target = undefined; } // outside rootjQ, the MathQuill node corresponding to the target (if any) // won't be inside this root, so don't mislead Controller::seek with it - function mouseup(e) { - cursor.blink = blink; - if (!cursor.selection) { - if (ctrlr.editable) { - cursor.show(); - } - else { - textareaSpan.detach(); - } - } - + function unbindListeners (e) { // delete the mouse handlers now that we're not dragging anymore rootjQ.unbind('mousemove', mousemove); $(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup); + cancelSelectionOnEdit = undefined; + } + + function updateCursor () { + if (ctrlr.editable) { + cursor.show(); + aria.queue(cursor.parent).alert(); + } + else { + textareaSpan.detach(); + } + } + + function mouseup(e) { + cursor.blink = blink; + if (!cursor.selection) updateCursor(); + unbindListeners(e); + } + + var wasEdited; + cancelSelectionOnEdit = { + cursor: cursor, + cb: function () { + // If an edit happens while the mouse is down, the existing + // selection is no longer valid. Clear it and unbind listeners, + // similar to what happens on mouseup. + wasEdited = true; + cursor.blink = blink; + cursor.clearSelection(); + updateCursor(); + unbindListeners(e); + } } if (ctrlr.blurred) { if (!ctrlr.editable) rootjQ.prepend(textareaSpan); - textarea.focus(); + textarea[0].focus(); + // focus call may bubble to clients, who may then write to + // mathquill, triggering cancelSelectionOnEdit. If that happens, we + // don't want to stop the cursor blink or bind listeners, + // so return early. + if (wasEdited) return; } cursor.blink = noop; @@ -62,18 +107,27 @@ Controller.open(function(_) { }); Controller.open(function(_) { - _.seek = function(target, pageX, pageY) { + _.seek = function($target, pageX, pageY) { var cursor = this.notify('select').cursor; + var node; + var targetElm = $target && $target[0]; - if (target) { - var nodeId = target.attr(mqBlockId) || target.attr(mqCmdId); - if (!nodeId) { - var targetParent = target.parent(); - nodeId = targetParent.attr(mqBlockId) || targetParent.attr(mqCmdId); - } + // we can click on an element that is deeply nested past the point + // that mathquill knows about. We need to traverse up to the first + // node that mathquill is aware of + while (targetElm) { + // try to find the MQ Node associated with the DOM Element + node = Node.getNodeOfElement(targetElm); + if (node) break; + + // must be too deep, traverse up to the parent DOM Element + targetElm = targetElm.parentElement; + } + + // Could not find any nodes, just use the root + if (!node) { + node = this.root; } - var node = nodeId ? Node.byId[nodeId] : this.root; - pray('nodeId is the id of some Node that exists', node); // don't clear selection until after getting node from target, in case // target was selection span, otherwise target will have no parent and will diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index 5d01abedc..4238a0107 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -88,12 +88,12 @@ var saneKeyboardEvents = (function() { // create a keyboard events shim that calls callbacks at useful times // and exports useful public methods - return function saneKeyboardEvents(el, handlers) { + return function saneKeyboardEvents(el, controller) { var keydown = null; var keypress = null; var textarea = jQuery(el); - var target = jQuery(handlers.container || textarea); + var target = jQuery(controller.container || textarea); // checkTextareaFor() is called after key or clipboard events to // say "Hey, I think something was just typed" or "pasted" etc, @@ -116,8 +116,20 @@ var saneKeyboardEvents = (function() { checker(e); }); } - target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); }); + target.bind('keydown keypress input keyup paste', function(e) { + checkTextarea(e); + }); + function guardedTextareaSelect () { + try { + // IE can throw an 'Incorrect Function' error if you + // try to select a textarea that is hidden. It seems + // likely that we don't really care if the selection + // fails to happen in this case. Why would the textarea + // be hidden? And who would even be able to tell? + textarea[0].select(); + } catch (e) {}; + } // -*- public methods -*- // function select(text) { @@ -129,7 +141,7 @@ var saneKeyboardEvents = (function() { clearTimeout(timeoutId); textarea.val(text); - if (text && textarea[0].select) textarea[0].select(); + if (text) guardedTextareaSelect(); shouldBeSelected = !!text; } var shouldBeSelected = false; @@ -147,7 +159,11 @@ var saneKeyboardEvents = (function() { } function handleKey() { - handlers.keystroke(stringify(keydown), keydown); + if (controller.options && controller.options.overrideKeystroke) { + controller.options.overrideKeystroke(stringify(keydown), keydown); + } else { + controller.keystroke(stringify(keydown), keydown); + } } // -*- event handlers -*- // @@ -158,16 +174,31 @@ var saneKeyboardEvents = (function() { keypress = null; if (shouldBeSelected) checkTextareaOnce(function(e) { - if (!(e && e.type === 'focusout') && textarea[0].select) { + if (!(e && e.type === 'focusout')) { // re-select textarea in case it's an unrecognized key that clears // the selection, then never again, 'cos next thing might be blur - textarea[0].select(); + guardedTextareaSelect() } }); handleKey(); } + function isArrowKey (e) { + if (!e || !e.originalEvent) return false; + + // The keyPress event in FF reports which=0 for some reason. The new + // .key property seems to report reasonable results, so we're using that + switch (e.originalEvent.key) { + case 'ArrowRight': + case 'ArrowLeft': + case 'ArrowDown': + case 'ArrowUp': + return true; + } + + return false; + } function onKeypress(e) { if (e.target !== textarea[0]) return; @@ -179,13 +210,28 @@ var saneKeyboardEvents = (function() { keypress = e; - checkTextareaFor(typedText); + // only check for typed text if this key can type text. Otherwise + // you can end up with mathquill thinking text was typed if you + // use the mq.keystroke('Right') command while a single character + // is selected. Only detected in FF. + if (!isArrowKey(e)) { + checkTextareaFor(typedText); + } } function onKeyup(e) { if (e.target !== textarea[0]) return; // Handle case of no keypress event being sent - if (!!keydown && !keypress) checkTextareaFor(typedText); + if (!!keydown && !keypress) { + + // only check for typed text if this key can type text. Otherwise + // you can end up with mathquill thinking text was typed if you + // use the mq.keystroke('Right') command while a single character + // is selected. Only detected in FF. + if (!isArrowKey(e)) { + checkTextareaFor(typedText); + } + } } function typedText() { // If there is a selection, the contents of the textarea couldn't @@ -210,13 +256,23 @@ var saneKeyboardEvents = (function() { var text = textarea.val(); if (text.length === 1) { textarea.val(''); - handlers.typedText(text); + if (controller.options && controller.options.overrideTypedText) { + controller.options.overrideTypedText(text); + } else { + controller.typedText(text); + } } // in Firefox, keys that don't type text, just clear seln, fire keypress // https://github.com/mathquill/mathquill/issues/293#issuecomment-40997668 - else if (text && textarea[0].select) textarea[0].select(); // re-select if that's why we're here + else if (text) guardedTextareaSelect(); // re-select if that's why we're here } - function onBlur() { keydown = keypress = null; } + function onBlur() { + keydown = null; + keypress = null; + checkTextarea = noop; + clearTimeout(timeoutId); + textarea.val(''); + } function onPaste(e) { if (e.target !== textarea[0]) return; @@ -234,7 +290,7 @@ var saneKeyboardEvents = (function() { // // And by nifty, we mean dumb (but useful sometimes). if (document.activeElement !== textarea[0]) { - textarea.focus(); + textarea[0].focus(); } checkTextareaFor(pastedText); @@ -242,19 +298,32 @@ var saneKeyboardEvents = (function() { function pastedText() { var text = textarea.val(); textarea.val(''); - if (text) handlers.paste(text); + if (text) controller.paste(text); } // -*- attach event handlers -*- // - target.bind({ - keydown: onKeydown, - keypress: onKeypress, - keyup: onKeyup, - focusout: onBlur, - cut: function() { checkTextareaOnce(function() { handlers.cut(); }); }, - copy: function() { checkTextareaOnce(function() { handlers.copy(); }); }, - paste: onPaste - }); + + if (controller.options && controller.options.disableCopyPaste) { + target.bind({ + keydown: onKeydown, + keypress: onKeypress, + keyup: onKeyup, + focusout: onBlur, + copy: function(e) { e.preventDefault(); }, + cut: function(e) { e.preventDefault(); }, + paste: function(e) { e.preventDefault(); } + }); + } else { + target.bind({ + keydown: onKeydown, + keypress: onKeypress, + keyup: onKeyup, + focusout: onBlur, + cut: function() { checkTextareaOnce(function() { controller.cut(); }); }, + copy: function() { checkTextareaOnce(function() { controller.copy(); }); }, + paste: onPaste + }); + } // -*- export public methods -*- // return { diff --git a/src/services/scrollHoriz.js b/src/services/scrollHoriz.js index 73c8f0170..bc7afb4cb 100644 --- a/src/services/scrollHoriz.js +++ b/src/services/scrollHoriz.js @@ -4,16 +4,36 @@ **********************************************/ Controller.open(function(_) { + _.setOverflowClasses = function () { + var root = this.root.jQ[0]; + var shouldHaveOverflowRight = false; + var shouldHaveOverflowLeft = false; + if (!this.blurred) { + var width = root.getBoundingClientRect().width; + var scrollWidth = root.scrollWidth; + var scroll = root.scrollLeft; + shouldHaveOverflowRight = (scrollWidth > width + scroll); + shouldHaveOverflowLeft = (scroll > 0); + } + if (root.classList.contains('mq-editing-overflow-right') !== shouldHaveOverflowRight) + root.classList.toggle('mq-editing-overflow-right') + if (root.classList.contains('mq-editing-overflow-left') !== shouldHaveOverflowLeft) + root.classList.toggle('mq-editing-overflow-left') + } _.scrollHoriz = function() { var cursor = this.cursor, seln = cursor.selection; var rootRect = this.root.jQ[0].getBoundingClientRect(); - if (!seln) { + if (!cursor.jQ[0] && !seln) { + this.root.jQ.stop().animate({scrollLeft: 0}, 100, function () { + this.setOverflowClasses(); + }.bind(this)); + return; + } else if (!seln) { var x = cursor.jQ[0].getBoundingClientRect().left; if (x > rootRect.right - 20) var scrollBy = x - (rootRect.right - 20); else if (x < rootRect.left + 20) var scrollBy = x - (rootRect.left + 20); else return; - } - else { + } else { var rect = seln.jQ[0].getBoundingClientRect(); var overLeft = rect.left - (rootRect.left + 20); var overRight = rect.right - (rootRect.right - 20); @@ -34,6 +54,12 @@ Controller.open(function(_) { else return; } } - this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100); + + var root = this.root.jQ[0] + if (scrollBy < 0 && root.scrollLeft === 0) return + if (scrollBy > 0 && root.scrollWidth <= root.scrollLeft + rootRect.width) return + this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100, function () { + this.setOverflowClasses(); + }.bind(this)); }; }); diff --git a/src/services/textarea.js b/src/services/textarea.js index 23b2ecab1..78f04d102 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -6,7 +6,7 @@ Controller.open(function(_) { Options.p.substituteTextarea = function() { return $('