@webspellchecker/wproofreader-learnosity 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/LICENSE +190 -0
- package/README.md +172 -0
- package/dist/wproofreader-learnosity.min.js +2 -0
- package/package.json +50 -0
- package/src/index.d.ts +106 -0
- package/src/index.js +502 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@webspellchecker/wproofreader-learnosity`.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [1.2.1]
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **Published to npm.** `@webspellchecker/wproofreader-learnosity` is now available on the public npm registry. v1.2.1 is functionally identical to v1.2.0; the bump exists to mark the npm publication event so the jsDelivr URL documented in the README resolves.
|
|
12
|
+
|
|
13
|
+
## [1.2.0]
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **Two install paths.** Existing ESM via npm, plus a new IIFE bundle (`dist/wproofreader-learnosity.min.js`) served from jsDelivr, exposing `window.LearnosityWProofreader.init` for host apps that load the connector via a `<script>` tag (PHP, Python, Java, ASP.NET, Ruby, classic HTML). About 5 KB minified, no runtime dependencies. Built with esbuild via `npm run build`; produced automatically at `npm publish` time via the `prepack` hook.
|
|
18
|
+
|
|
19
|
+
## [1.1.0]
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
- **Self-hosted WProofreader support.** `init()` accepts self-hosted service options (`serviceProtocol`, `serviceHost`, `servicePort`, `servicePath`) under the `wproofreader` block instead of (or alongside) `serviceId`, so customers running the WProofreader Server on their own infrastructure (Docker, Helm, on-prem) integrate without a Cloud service ID. Either `wproofreader.serviceId` or `wproofreader.serviceHost` activates `init()`.
|
|
24
|
+
|
|
25
|
+
## [1.0.0]
|
|
26
|
+
|
|
27
|
+
Initial public release.
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
- **Drop-in initialisation.** A single `LearnosityWProofreader.init({ wproofreader: { serviceId } })` call from a host app is enough to start proofreading inside Learnosity widgets. Idempotent: subsequent calls update the live configuration.
|
|
32
|
+
- **ESM npm package.** Default export imported as `import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity'`; the call site mirrors the import shape.
|
|
33
|
+
- **All three Learnosity APIs supported.** Questions API (`longtextV2` rich-text responses), Author API (every rich-text field across the authoring UI), and Items API (`assess` rendering type).
|
|
34
|
+
- **Two-tier options.** WProofreader runtime options live under a nested `wproofreader` block and are forwarded to WProofreader unchanged (including unknown future options); connector-only fields (`surfaces`, `enableShorttext`, `customSelectors`, `bundleUrl`) live at the top level. The two groups never collide.
|
|
35
|
+
- **Per-editor surface filter.** The `surfaces` option (default `['author']`) walks each editor's ancestry to detect which Learnosity API placed it (`lrn-author*` → Author, `lrn_player*` → Items, otherwise → Questions) and skips editors whose surface is not allowed. Multi-API pages are handled correctly. Opt in to assessment proofreading explicitly with `['questions', 'items', 'author']`.
|
|
36
|
+
- **Assessment-safe default.** The default `surfaces: ['author']` keeps proofreading off in student-facing assessments unless the host app derives an explicit opt-in from server-side activity metadata.
|
|
37
|
+
- **Locale-aware default.** `wproofreader.lang` reads from the host page's `<html lang>` (with `-` normalised to `_`), so a French page proofreads in French without extra config. Falls back to `en_US` when the page does not set a `lang` or sets a bare un-regioned one like `en`.
|
|
38
|
+
- **TypeScript definitions** at `src/index.d.ts`, exposed via `package.json` `types` and `exports.types`. `WProofreaderOptions` has an `[other: string]: unknown` index signature so unknown WProofreader options still type-check.
|
|
39
|
+
- **Lifecycle cleanup.** A scoped `MutationObserver` watches for editors appearing and disappearing. WProofreader instances are destroyed when their host elements leave the DOM, so single-page-app hosts that rebuild Learnosity widgets do not leak instances.
|
|
40
|
+
- **Targeted attachment, not autoSearch.** Attaches only to editors inside a Learnosity-rendered subtree (any ancestor with a class starting with `lrn`, plus optional caller-supplied selectors). The host app's own editables are left untouched.
|
|
41
|
+
- **`customSelectors` escape hatch.** Caller-supplied ancestor selectors extend the Learnosity scope for unusual host apps without giving up the built-in scoping for the rest.
|
|
42
|
+
- **Optional `shorttext` surface.** Off by default. When `enableShorttext: true`, the connector also attaches to `<input type="text">` inside Learnosity scope.
|
|
43
|
+
- **Configurable WProofreader bundle URL** via `bundleUrl`, for customers running WProofreader from a non-default CDN host.
|
|
44
|
+
- **Apache-2.0 licensed.**
|
|
45
|
+
|
|
46
|
+
[1.2.1]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.2.1
|
|
47
|
+
[1.2.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.2.0
|
|
48
|
+
[1.1.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.1.0
|
|
49
|
+
[1.0.0]: https://github.com/WebSpellChecker/wproofreader-learnosity/releases/tag/v1.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Support. While redistributing the Work or
|
|
166
|
+
Derivative Works thereof, You may choose to offer, and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend,
|
|
172
|
+
and hold each Contributor harmless for any liability incurred by,
|
|
173
|
+
or claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or support.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 WebSpellChecker LLC
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
189
|
+
implied. See the License for the specific language governing permissions
|
|
190
|
+
and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @webspellchecker/wproofreader-learnosity
|
|
2
|
+
|
|
3
|
+
WProofreader for Learnosity. Spelling, grammar and style assistance inside Learnosity rich-text widgets, with one function call.
|
|
4
|
+
|
|
5
|
+
> v1.2.1. Apache-2.0. TypeScript definitions included. Two install paths: npm (ESM) and CDN (IIFE). Cloud and self-hosted WProofreader. Per-editor surface filter, lifecycle cleanup, locale-aware defaults, optional escape hatches for unusual host apps.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The connector offers two install paths. The runtime API is identical, `LearnosityWProofreader.init({...})` in both.
|
|
10
|
+
|
|
11
|
+
### Via npm (Node host apps)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @webspellchecker/wproofreader-learnosity
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity';
|
|
19
|
+
|
|
20
|
+
LearnosityWProofreader.init({
|
|
21
|
+
wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' }
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Via `<script>` tag (non-Node host apps)
|
|
26
|
+
|
|
27
|
+
For host apps where adding a bundler is overkill (classic HTML, PHP, Python, Java, ASP.NET, Ruby), load the connector from jsDelivr:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<script src="https://cdn.jsdelivr.net/npm/@webspellchecker/wproofreader-learnosity@1.2.1/dist/wproofreader-learnosity.min.js"></script>
|
|
31
|
+
<script>
|
|
32
|
+
LearnosityWProofreader.init({
|
|
33
|
+
wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' }
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The bundle exposes a single global, `window.LearnosityWProofreader`, with the same `init` method. About 5 KB minified, no runtime dependencies. jsDelivr serves from npm, so the pinned URL above resolves once the package is published.
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity';
|
|
44
|
+
|
|
45
|
+
// Before Learnosity widgets render.
|
|
46
|
+
LearnosityWProofreader.init({
|
|
47
|
+
wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Initialise Learnosity as normal. The connector attaches to longtextV2 editors automatically.
|
|
51
|
+
LearnosityApp.init(signedRequest, {
|
|
52
|
+
readyListener() { console.log('Learnosity ready'); },
|
|
53
|
+
errorListener(err) { console.error(err); }
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`init()` is idempotent: calling it again updates the live configuration without reloading the WProofreader bundle.
|
|
58
|
+
|
|
59
|
+
## Getting a service ID
|
|
60
|
+
|
|
61
|
+
WProofreader is a commercial service from WebSpellChecker. The service ID passed to `init({ wproofreader: { serviceId } })` is your account's activation key.
|
|
62
|
+
|
|
63
|
+
- **Free trial**: <https://webspellchecker.com/free-trial/> issues a service ID for evaluation.
|
|
64
|
+
- **Paid subscription** converts the trial to a production licence with the domain set you need.
|
|
65
|
+
- **Domain locking**: every service ID is bound to a list of allowed origins. Whitelist your production and development domains in the WebSpellChecker dashboard; otherwise the spell-check API returns 403 for those origins.
|
|
66
|
+
|
|
67
|
+
If you run WProofreader on your own infrastructure (Docker, Helm, on-prem) you skip the service ID entirely. See [Self-hosted WProofreader](#self-hosted-wproofreader) below.
|
|
68
|
+
|
|
69
|
+
## Self-hosted WProofreader
|
|
70
|
+
|
|
71
|
+
For customers running the [WProofreader Server](https://hub.docker.com/r/webspellchecker/wproofreader) on their own infrastructure (Docker, Helm, on-prem), pass the service connection options inside the `wproofreader` block instead of (or alongside) `serviceId`. Every property on `wproofreader` is forwarded to WProofreader unchanged:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
LearnosityWProofreader.init({
|
|
75
|
+
wproofreader: {
|
|
76
|
+
serviceProtocol: 'https',
|
|
77
|
+
serviceHost: 'localhost',
|
|
78
|
+
servicePort: 443,
|
|
79
|
+
servicePath: '/wscservice/api'
|
|
80
|
+
},
|
|
81
|
+
bundleUrl: 'https://localhost/wscservice/wscbundle/wscbundle.js'
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Either `wproofreader.serviceId` or `wproofreader.serviceHost` must be set; otherwise `init()` no-ops with a console warning.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
`init()` takes a single options object with two groups: a nested `wproofreader` sub-object (forwarded to WProofreader unchanged) and connector-only fields at the top level.
|
|
90
|
+
|
|
91
|
+
### WProofreader options (under `wproofreader`)
|
|
92
|
+
|
|
93
|
+
These pass straight through to WProofreader's runtime config. The tables below cover the frequently-used ones; the full surface is documented in [Documentation and support](#documentation-and-support).
|
|
94
|
+
|
|
95
|
+
| Option | Default | Notes |
|
|
96
|
+
|-------------------|--------------------------------------|--------------------------------------------------------|
|
|
97
|
+
| `serviceId` | _none_ | WProofreader Cloud service ID. Domain-locked server-side. Required for the Cloud path. Omit when using a self-hosted deployment. |
|
|
98
|
+
| `serviceProtocol` | _none_ | Self-hosted: `'http'` or `'https'`. |
|
|
99
|
+
| `serviceHost` | _none_ | Self-hosted: WProofreader server hostname. Presence of `serviceHost` (or `serviceId`) is what activates `init()`. |
|
|
100
|
+
| `servicePort` | _none_ | Self-hosted: port. |
|
|
101
|
+
| `servicePath` | _none_ | Self-hosted: URL path to the WProofreader API. |
|
|
102
|
+
| `enableGrammar` | `true` | Grammar checking on top of spell checking. |
|
|
103
|
+
| `autocorrect` | `true` | Suggested replacement on typo accept. |
|
|
104
|
+
| `lang` | host page's `<html lang>`, or `en_US`| Read from `<html lang>` with `-` normalised to `_` (e.g. `en-US` → `en_US`). Falls back to `en_US` if the page does not set a `lang` or sets a bare un-regioned one (`en`, `fr`), because WProofreader expects region-qualified codes. Pass explicitly to override. |
|
|
105
|
+
|
|
106
|
+
If neither `wproofreader.serviceId` nor `wproofreader.serviceHost` is set, `init()` no-ops with a console warning so the host app can call it unconditionally.
|
|
107
|
+
|
|
108
|
+
### Connector options (top level)
|
|
109
|
+
|
|
110
|
+
| Option | Default | Notes |
|
|
111
|
+
|-------------------|---------------|--------------------------------------------------------|
|
|
112
|
+
| `surfaces` | `['author']` | Subset of `['questions','items','author']`. **Per-editor** filter: each editor's ancestry is walked for surface-specific markers (`lrn-author*` → author, `lrn_player*` → items, otherwise → questions); editors whose surface is not in the allowed list are skipped. Default is Author only, for assessment integrity. Opt in to assessment proofreading explicitly with `['questions', 'items', 'author']`. |
|
|
113
|
+
| `enableShorttext` | `false` | Also attach to `shorttext` inputs (`<input type="text">`) inside Learnosity scope. Off by default because plain text inputs are usually short and proofreading them is noisy. |
|
|
114
|
+
| `customSelectors` | `[]` | Extra ancestor CSS selectors that count as "inside Learnosity scope" for unusual host apps. Each entry is matched via `element.matches`. |
|
|
115
|
+
| `bundleUrl` | (default CDN) | Override the WProofreader bundle URL for non-default CDNs or self-hosted servers. |
|
|
116
|
+
|
|
117
|
+
### Examples
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
// Authoring-only (the default). No-ops on assessment pages.
|
|
121
|
+
LearnosityWProofreader.init({
|
|
122
|
+
wproofreader: { serviceId: '...' }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// All three surfaces, including assessment. The host app should derive this
|
|
126
|
+
// from server-side activity metadata so it cannot be flipped from the console.
|
|
127
|
+
LearnosityWProofreader.init({
|
|
128
|
+
wproofreader: { serviceId: '...' },
|
|
129
|
+
surfaces: ['questions', 'items', 'author']
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Unusual host app that wraps Learnosity inside its own container class.
|
|
133
|
+
LearnosityWProofreader.init({
|
|
134
|
+
wproofreader: { serviceId: '...' },
|
|
135
|
+
customSelectors: ['.my-host-app-learnosity-region']
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Supported surfaces
|
|
140
|
+
|
|
141
|
+
| Learnosity API | Surface |
|
|
142
|
+
|----------------|-------------------------------|
|
|
143
|
+
| Questions API | `longtextV2` (rich-text essay) |
|
|
144
|
+
| Author API | All rich-text fields |
|
|
145
|
+
| Items API | `assess` rendering type |
|
|
146
|
+
|
|
147
|
+
`shorttext` (plain text input) is opt-in via `enableShorttext: true`.
|
|
148
|
+
|
|
149
|
+
## Supported Learnosity versions
|
|
150
|
+
|
|
151
|
+
Tested manually against the `latest-lts` channel. The connector tracks editor placement via the `lrn`-prefixed class convention plus a small per-surface marker set; class-rename changes between LTS releases are an isolated update in `src/index.js`.
|
|
152
|
+
|
|
153
|
+
## Documentation and support
|
|
154
|
+
|
|
155
|
+
- **WProofreader product page**: <https://webspellchecker.com/wsc-proofreader/>
|
|
156
|
+
- **Configuration reference** (every WProofreader option beyond what this connector documents): <https://webspellchecker.com/docs/api/wscbundle/Options.html>
|
|
157
|
+
- **WProofreader SDK source** (the underlying library the connector wraps): <https://github.com/WebSpellChecker/wproofreader-sdk-js>
|
|
158
|
+
- **Self-hosted server image**: <https://hub.docker.com/r/webspellchecker/wproofreader>
|
|
159
|
+
- **Contact and commercial enquiries**: <https://webspellchecker.com/contact-us/>
|
|
160
|
+
|
|
161
|
+
## Tests
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npm install
|
|
165
|
+
npm test
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`npm test` runs the unit suite under `node --test`.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
Apache-2.0. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var LearnosityWProofreader=(()=>{var l=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var E=(e,t)=>{for(var n in t)l(e,n,{get:t[n],enumerable:!0})},v=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of S(t))!A.call(e,r)&&r!==n&&l(e,r,{get:()=>t[r],enumerable:!(o=L(t,r))||o.enumerable});return e};var g=e=>v(l({},"__esModule",{value:!0}),e);var k={};E(k,{default:()=>D});var _="https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js",C=["questions","items","author"],O={questions:"LearnosityApp",items:"LearnosityItems",author:"LearnosityAuthor"},I=["author"],x=3e4,T=100,h=!1,p=null,f=new WeakSet,d=new WeakMap;function U(e){e=e||{};let t=e.wproofreader||{};if(!t.serviceId&&!t.serviceHost){typeof console<"u"&&console.warn&&console.warn("[wproofreader-learnosity] No wproofreader.serviceId or wproofreader.serviceHost provided. Proofreading will not activate. Pass serviceId for WProofreader Cloud, or serviceHost (plus serviceProtocol, servicePort, servicePath) for a self-hosted deployment.");return}let n=P(e);if(h){Object.assign(window.WEBSPELLCHECKER_CONFIG,n.wproofreader),Object.assign(p,n);return}window.WEBSPELLCHECKER_CONFIG=n.wproofreader,p=n,q(e.bundleUrl||_),W(n),h=!0}function P(e){let t=e.wproofreader||{};return{wproofreader:{...t,enableGrammar:t.enableGrammar!==!1,autocorrect:t.autocorrect!==!1,lang:t.lang||F()},surfaces:R(e.surfaces),enableShorttext:e.enableShorttext===!0,customSelectors:Array.isArray(e.customSelectors)?e.customSelectors.slice():[]}}function F(){if(typeof document>"u")return"en_US";let e=document.documentElement;if(!e||!e.lang)return"en_US";let t=e.lang;return t.indexOf("-")===-1&&t.indexOf("_")===-1?"en_US":t.replace("-","_")}function R(e){return Array.isArray(e)?e.filter(function(t){return C.indexOf(t)!==-1}):I.slice()}function B(e){if(typeof window>"u"||e.length===0)return!1;for(let t=0;t<e.length;t++){let n=O[e[t]];if(n&&typeof window[n]<"u")return!0}return!1}function N(e){let t=e&&e.parentElement;for(;t;){let n=t.classList;if(n&&n.length)for(let o=0;o<n.length;o++){let r=n[o];if(r.indexOf("lrn-author")===0)return"author";if(r.indexOf("lrn_player")===0)return"items"}t=t.parentElement}return"questions"}function m(e,t){let n=e&&e.parentElement;for(;n;){let o=n.classList;if(o)for(let r=0;r<o.length;r++){let i=o[r];if(i==="learnosity-response"||i.indexOf("lrn")===0)return!0}if(t&&t.length&&n.matches)for(let r=0;r<t.length;r++)try{if(n.matches(t[r]))return!0}catch{}n=n.parentElement}return!1}function q(e){if(typeof document>"u"||document.querySelector("script[data-wproofreader-bundle]"))return;let t=document.createElement("script");t.src=e,t.async=!0,t.dataset.wproofreaderBundle="",document.head.appendChild(t)}function W(e){if(typeof document>"u")return;let t=function(o){G(o,e)};t(document),new MutationObserver(function(o){for(let r=0;r<o.length;r++){let i=o[r];if(i.type==="childList"){let s=i.addedNodes;for(let a=0;a<s.length;a++){let c=s[a];c&&c.nodeType===1&&t(c)}let u=i.removedNodes;for(let a=0;a<u.length;a++){let c=u[a];c&&c.nodeType===1&&H(c)}}else if(i.type==="attributes"&&i.attributeName==="contenteditable"){let s=i.target;s&&y(s,e)&&m(s,e.customSelectors)&&b(s,e)}}}).observe(document.body||document.documentElement,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["contenteditable"]})}function G(e,t){if(!e||!e.querySelectorAll)return;let n=[];y(e,t)&&n.push(e);let o=e.querySelectorAll(j(t));for(let r=0;r<o.length;r++)n.push(o[r]);for(let r=0;r<n.length;r++){let i=n[r];m(i,t.customSelectors)&&b(i,t)}}function j(e){let t='iframe, [contenteditable]:not([contenteditable="false"])';return e.enableShorttext&&(t+=', input[type="text"], input:not([type])'),t}function y(e,t){return!e||e.nodeType!==1?!1:e.tagName==="IFRAME"?!0:e.tagName==="INPUT"&&t.enableShorttext?(e.getAttribute("type")||"text").toLowerCase()==="text":!e.hasAttribute||!e.hasAttribute("contenteditable")?!1:e.getAttribute("contenteditable")!=="false"}function b(e,t){if(!e||f.has(e)||!B(t.surfaces))return;let n=N(e);if(t.surfaces.indexOf(n)===-1)return;f.add(e);let o=function(){M("WEBSPELLCHECKER",function(i){let s=Object.assign({},t.wproofreader,{container:e});try{i.init(s,function(u){u&&d.set(e,u)})}catch{}})};if(e.tagName&&e.tagName.toLowerCase()==="iframe"){let i=null;try{i=e.contentDocument}catch{}i&&i.readyState==="complete"?o():e.addEventListener("load",o,{once:!0})}else o()}function H(e){if(e&&(f.has(e)&&w(e),e.querySelectorAll)){let t=e.querySelectorAll('iframe, [contenteditable], input[type="text"], input:not([type])');for(let n=0;n<t.length;n++)f.has(t[n])&&w(t[n])}}function w(e){let t=d.get(e);if(t){try{typeof t.destroy=="function"&&t.destroy()}catch{}d.delete(e)}f.delete(e)}function M(e,t){if(typeof window>"u")return;if(window[e]){t(window[e]);return}let n=setInterval(function(){window[e]&&(clearInterval(n),clearTimeout(o),t(window[e]))},T),o=setTimeout(function(){clearInterval(n),typeof console<"u"&&console.warn&&console.warn("[wproofreader-learnosity] window."+e+" never appeared. Proofreading will not activate.")},x)}var D={init:U};return g(k);})();
|
|
2
|
+
LearnosityWProofreader=LearnosityWProofreader.default;
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@webspellchecker/wproofreader-learnosity",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "WProofreader for Learnosity. Spelling, grammar and style assistance inside Learnosity rich-text widgets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/",
|
|
16
|
+
"dist/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test tests/*.test.js",
|
|
23
|
+
"build": "esbuild src/index.js --bundle --format=iife --global-name=LearnosityWProofreader '--footer:js=LearnosityWProofreader=LearnosityWProofreader.default;' --minify --outfile=dist/wproofreader-learnosity.min.js",
|
|
24
|
+
"prepack": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"esbuild": "^0.23.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"learnosity",
|
|
31
|
+
"wproofreader",
|
|
32
|
+
"webspellchecker",
|
|
33
|
+
"spell-check",
|
|
34
|
+
"grammar",
|
|
35
|
+
"ckeditor"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/WebSpellChecker/wproofreader-learnosity",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/WebSpellChecker/wproofreader-learnosity.git"
|
|
44
|
+
},
|
|
45
|
+
"private": false,
|
|
46
|
+
"license": "Apache-2.0",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"user": "0.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for @webspellchecker/wproofreader-learnosity.
|
|
3
|
+
* Hand-written to match src/index.js. Keep in sync.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Learnosity API surfaces the connector knows how to scope to. Each editor's
|
|
8
|
+
* ancestry is walked for surface-specific markers; editors whose surface is
|
|
9
|
+
* not in the allowed list are skipped.
|
|
10
|
+
*/
|
|
11
|
+
export type Surface = 'questions' | 'items' | 'author';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runtime options forwarded to WProofreader unchanged. The connector does
|
|
15
|
+
* not enforce any schema beyond `serviceId` / `serviceHost` activation, so
|
|
16
|
+
* any other WProofreader option may also be set.
|
|
17
|
+
*/
|
|
18
|
+
export interface WProofreaderOptions {
|
|
19
|
+
/**
|
|
20
|
+
* WProofreader Cloud service ID. Domain-locked server-side. Required for
|
|
21
|
+
* the Cloud path. Omit when using a self-hosted deployment.
|
|
22
|
+
*/
|
|
23
|
+
serviceId?: string;
|
|
24
|
+
|
|
25
|
+
/** Self-hosted: protocol. */
|
|
26
|
+
serviceProtocol?: 'http' | 'https';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Self-hosted: WProofreader server hostname. Presence of `serviceHost` (or
|
|
30
|
+
* `serviceId`) activates `init()`.
|
|
31
|
+
*/
|
|
32
|
+
serviceHost?: string;
|
|
33
|
+
|
|
34
|
+
/** Self-hosted: port. */
|
|
35
|
+
servicePort?: number | string;
|
|
36
|
+
|
|
37
|
+
/** Self-hosted: URL path to the WProofreader API (e.g. `/wscservice/api`). */
|
|
38
|
+
servicePath?: string;
|
|
39
|
+
|
|
40
|
+
/** Default `true`. */
|
|
41
|
+
enableGrammar?: boolean;
|
|
42
|
+
|
|
43
|
+
/** Default `true`. */
|
|
44
|
+
autocorrect?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default: the host page's `<html lang>` attribute, with `-` normalised to
|
|
48
|
+
* `_` (e.g. `en-US` becomes `en_US`). Falls back to `en_US` if the page
|
|
49
|
+
* does not set a `lang` or sets a bare un-regioned one (WProofreader
|
|
50
|
+
* requires region-qualified codes).
|
|
51
|
+
*/
|
|
52
|
+
lang?: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Forward-compatible escape hatch: any other property is passed through to
|
|
56
|
+
* WProofreader's runtime config unchanged. Useful for WProofreader options
|
|
57
|
+
* the connector does not enumerate explicitly.
|
|
58
|
+
*/
|
|
59
|
+
[other: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface InitOptions {
|
|
63
|
+
/** WProofreader runtime options. Forwarded to WProofreader unchanged. */
|
|
64
|
+
wproofreader?: WProofreaderOptions;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subset of `['questions','items','author']`. Default `['author']` for
|
|
68
|
+
* assessment integrity. The connector walks each editor's ancestry to
|
|
69
|
+
* detect which Learnosity API placed it (`lrn-author*` → author,
|
|
70
|
+
* `lrn_player*` → items, otherwise → questions) and skips editors whose
|
|
71
|
+
* surface is not in the allowed list. Opt in to assessment proofreading
|
|
72
|
+
* explicitly with `['questions', 'items', 'author']`.
|
|
73
|
+
*/
|
|
74
|
+
surfaces?: ReadonlyArray<Surface>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default `false`. Also attach to `shorttext` inputs (`<input type="text">`)
|
|
78
|
+
* inside Learnosity scope.
|
|
79
|
+
*/
|
|
80
|
+
enableShorttext?: boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extra ancestor CSS selectors that count as "inside Learnosity scope" for
|
|
84
|
+
* unusual host apps. Each entry is matched via `element.matches`.
|
|
85
|
+
*/
|
|
86
|
+
customSelectors?: ReadonlyArray<string>;
|
|
87
|
+
|
|
88
|
+
/** Override the WProofreader bundle URL for non-default CDNs or self-hosted servers. */
|
|
89
|
+
bundleUrl?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Public API surface. The default export is the namespace object; the call
|
|
94
|
+
* site mirrors that shape.
|
|
95
|
+
*/
|
|
96
|
+
export interface LearnosityWProofreader {
|
|
97
|
+
/**
|
|
98
|
+
* Initialise the connector. Should be called before Learnosity widgets
|
|
99
|
+
* render. Safe to call multiple times: subsequent calls update the live
|
|
100
|
+
* configuration without reloading the WProofreader bundle.
|
|
101
|
+
*/
|
|
102
|
+
init(options?: InitOptions): void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare const LearnosityWProofreader: LearnosityWProofreader;
|
|
106
|
+
export default LearnosityWProofreader;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WProofreader connector for Learnosity.
|
|
3
|
+
*
|
|
4
|
+
* Targeted attachment to Learnosity-rendered editables, scoped by an ancestry
|
|
5
|
+
* walk for any `lrn`-prefixed class plus optional caller-supplied selectors.
|
|
6
|
+
* Per-editor `surfaces` filter (default `['author']`) covers assessment
|
|
7
|
+
* integrity by excluding Questions and Items by default; the host app opts in
|
|
8
|
+
* explicitly. Cleans up WProofreader instances when their host elements are
|
|
9
|
+
* removed from the DOM (single-page apps that rebuild Learnosity widgets do
|
|
10
|
+
* not leak).
|
|
11
|
+
*
|
|
12
|
+
* Usage in a host app:
|
|
13
|
+
*
|
|
14
|
+
* import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity';
|
|
15
|
+
*
|
|
16
|
+
* LearnosityWProofreader.init({
|
|
17
|
+
* wproofreader: { serviceId: 'YOUR_WPROOFREADER_SERVICE_ID' }
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* LearnosityApp.init(signedRequest, callbacks);
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BUNDLE_URL =
|
|
24
|
+
'https://svc.webspellchecker.net/spellcheck31/wscbundle/wscbundle.js';
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Surfaces the connector recognises. `SURFACE_GLOBALS` maps each to the
|
|
28
|
+
* `window` global Learnosity exposes for that API. Used in two places:
|
|
29
|
+
* - `isAnyAllowedSurfaceLoaded`: cheap page-level gate; skip everything if
|
|
30
|
+
* no allowed-surface global has loaded yet.
|
|
31
|
+
* - `detectSurface`: per-editor classification via DOM-ancestor markers,
|
|
32
|
+
* so multi-API pages are filtered correctly.
|
|
33
|
+
*/
|
|
34
|
+
const ALLOWED_SURFACES = ['questions', 'items', 'author'];
|
|
35
|
+
const SURFACE_GLOBALS = {
|
|
36
|
+
questions: 'LearnosityApp',
|
|
37
|
+
items: 'LearnosityItems',
|
|
38
|
+
author: 'LearnosityAuthor'
|
|
39
|
+
};
|
|
40
|
+
/*
|
|
41
|
+
* Default surfaces. Author only, because Questions and Items render
|
|
42
|
+
* student-facing assessments where spell-check can compromise the activity
|
|
43
|
+
* itself. Host apps that want proofreading in those contexts opt in
|
|
44
|
+
* explicitly with `surfaces: ['questions', 'items', 'author']`.
|
|
45
|
+
*/
|
|
46
|
+
const DEFAULT_SURFACES = ['author'];
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Maximum wait for `window.WEBSPELLCHECKER` to appear after the bundle
|
|
50
|
+
* `<script>` is injected. The bundle loads async, so it may not be on
|
|
51
|
+
* `window` by the time the first editor needs attaching. After this timeout,
|
|
52
|
+
* `whenAvailable` logs a console warning and stops waiting.
|
|
53
|
+
*/
|
|
54
|
+
const GLOBAL_WAIT_TIMEOUT_MS = 30000;
|
|
55
|
+
const GLOBAL_POLL_INTERVAL_MS = 100;
|
|
56
|
+
|
|
57
|
+
let initialized = false;
|
|
58
|
+
let currentConnectorConfig = null;
|
|
59
|
+
const attachedContainers = new WeakSet();
|
|
60
|
+
const containerToInstance = new WeakMap();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialise the connector. Should be called before Learnosity widgets render.
|
|
64
|
+
* Safe to call multiple times: subsequent calls update the live configuration
|
|
65
|
+
* without reloading the WProofreader bundle or re-binding the observer.
|
|
66
|
+
*
|
|
67
|
+
* Options are split into two groups:
|
|
68
|
+
* - `wproofreader`: passed through to WProofreader unchanged (its runtime
|
|
69
|
+
* options surface). `serviceId` for Cloud or `serviceHost` for self-hosted
|
|
70
|
+
* activates the connector; without one of these, init() no-ops with a
|
|
71
|
+
* console warning.
|
|
72
|
+
* - Top-level keys: connector-only configuration (`surfaces`,
|
|
73
|
+
* `enableShorttext`, `customSelectors`, `bundleUrl`).
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} [options]
|
|
76
|
+
* @param {Object} [options.wproofreader] WProofreader runtime options.
|
|
77
|
+
* Forwarded to WProofreader unchanged.
|
|
78
|
+
* @param {string} [options.wproofreader.serviceId] WProofreader Cloud service ID.
|
|
79
|
+
* Required for the Cloud path. Omit
|
|
80
|
+
* when using a self-hosted deployment
|
|
81
|
+
* (see `serviceHost`).
|
|
82
|
+
* @param {string} [options.wproofreader.serviceProtocol] Self-hosted: 'http' or 'https'.
|
|
83
|
+
* @param {string} [options.wproofreader.serviceHost] Self-hosted: hostname of the
|
|
84
|
+
* WProofreader server. Presence of
|
|
85
|
+
* `serviceHost` (or `serviceId`)
|
|
86
|
+
* activates init().
|
|
87
|
+
* @param {number} [options.wproofreader.servicePort] Self-hosted: port.
|
|
88
|
+
* @param {string} [options.wproofreader.servicePath] Self-hosted: URL path to the
|
|
89
|
+
* WProofreader API (e.g.
|
|
90
|
+
* '/wscservice/api').
|
|
91
|
+
* @param {boolean} [options.wproofreader.enableGrammar] Default true.
|
|
92
|
+
* @param {boolean} [options.wproofreader.autocorrect] Default true.
|
|
93
|
+
* @param {string} [options.wproofreader.lang] Default: the host page's
|
|
94
|
+
* `<html lang>` attribute, with `-`
|
|
95
|
+
* normalised to `_` (e.g. `en-US`
|
|
96
|
+
* becomes `en_US`). Falls back to
|
|
97
|
+
* `en_US` if the page does not set a
|
|
98
|
+
* `lang` or sets a bare un-regioned
|
|
99
|
+
* one (WProofreader requires
|
|
100
|
+
* region-qualified codes).
|
|
101
|
+
* @param {string[]} [options.surfaces] Connector-only. Subset of
|
|
102
|
+
* ['questions','items','author'].
|
|
103
|
+
* Default: `['author']`
|
|
104
|
+
* (assessment-safe). The connector
|
|
105
|
+
* walks each editor's ancestry to
|
|
106
|
+
* detect which Learnosity API placed
|
|
107
|
+
* it (`lrn-author*` → author,
|
|
108
|
+
* `lrn_player*` → items, otherwise →
|
|
109
|
+
* questions) and skips editors whose
|
|
110
|
+
* surface is not in the allowed list.
|
|
111
|
+
* @param {boolean} [options.enableShorttext] Connector-only. Default false. When
|
|
112
|
+
* true, also attach to `shorttext`
|
|
113
|
+
* inputs (`<input type="text">`)
|
|
114
|
+
* inside Learnosity scope. Off by
|
|
115
|
+
* default because plain text inputs
|
|
116
|
+
* are often short and proofreading
|
|
117
|
+
* them feels noisy.
|
|
118
|
+
* @param {string[]} [options.customSelectors] Connector-only. Extra ancestor CSS
|
|
119
|
+
* selectors that count as "inside
|
|
120
|
+
* Learnosity scope" for unusual host
|
|
121
|
+
* apps.
|
|
122
|
+
* @param {string} [options.bundleUrl] Connector-only. Override the
|
|
123
|
+
* WProofreader bundle URL.
|
|
124
|
+
*/
|
|
125
|
+
function init(options) {
|
|
126
|
+
options = options || {};
|
|
127
|
+
const wp = options.wproofreader || {};
|
|
128
|
+
|
|
129
|
+
if (!wp.serviceId && !wp.serviceHost) {
|
|
130
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
131
|
+
console.warn(
|
|
132
|
+
'[wproofreader-learnosity] No wproofreader.serviceId or wproofreader.serviceHost ' +
|
|
133
|
+
'provided. Proofreading will not activate. Pass serviceId for WProofreader Cloud, or ' +
|
|
134
|
+
'serviceHost (plus serviceProtocol, servicePort, servicePath) for a self-hosted deployment.'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const config = buildConfig(options);
|
|
141
|
+
|
|
142
|
+
if (initialized) {
|
|
143
|
+
// Idempotent: update the live WProofreader config in place so the observer's
|
|
144
|
+
// captured config keeps the same identity, while WProofreader's bundle picks
|
|
145
|
+
// up the new values next time it reads `window.WEBSPELLCHECKER_CONFIG`.
|
|
146
|
+
Object.assign(window.WEBSPELLCHECKER_CONFIG, config.wproofreader);
|
|
147
|
+
Object.assign(currentConnectorConfig, config);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* WProofreader's bundle reads its runtime config from
|
|
153
|
+
* `window.WEBSPELLCHECKER_CONFIG` when it boots, so the object must exist
|
|
154
|
+
* before the bundle `<script>` runs. `autoSearch` is intentionally absent
|
|
155
|
+
* from the config: we attach explicitly via `observeLearnosity` below so
|
|
156
|
+
* attachment stays scoped to Learnosity-rendered editors only.
|
|
157
|
+
*/
|
|
158
|
+
window.WEBSPELLCHECKER_CONFIG = config.wproofreader;
|
|
159
|
+
currentConnectorConfig = config;
|
|
160
|
+
loadBundle(options.bundleUrl || DEFAULT_BUNDLE_URL);
|
|
161
|
+
observeLearnosity(config);
|
|
162
|
+
initialized = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* Two-tier config:
|
|
167
|
+
* - `wproofreader`: everything WProofreader's bundle reads from
|
|
168
|
+
* `window.WEBSPELLCHECKER_CONFIG`. Spread-then-default so any unknown
|
|
169
|
+
* option (self-hosted service*, future WProofreader knobs) flows through
|
|
170
|
+
* unchanged, while our defaults (enableGrammar, autocorrect, lang) apply
|
|
171
|
+
* only when the caller did not supply them.
|
|
172
|
+
* - Top-level: connector-only fields the observer reads (surfaces,
|
|
173
|
+
* enableShorttext, customSelectors). `bundleUrl` is consumed by
|
|
174
|
+
* `loadBundle()` directly from `options` and is not stored here.
|
|
175
|
+
*/
|
|
176
|
+
function buildConfig(options) {
|
|
177
|
+
const wp = options.wproofreader || {};
|
|
178
|
+
return {
|
|
179
|
+
wproofreader: {
|
|
180
|
+
...wp,
|
|
181
|
+
enableGrammar: wp.enableGrammar !== false,
|
|
182
|
+
autocorrect: wp.autocorrect !== false,
|
|
183
|
+
lang: wp.lang || defaultLang()
|
|
184
|
+
},
|
|
185
|
+
surfaces: normalizeSurfaces(options.surfaces),
|
|
186
|
+
enableShorttext: options.enableShorttext === true,
|
|
187
|
+
customSelectors: Array.isArray(options.customSelectors) ? options.customSelectors.slice() : []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Default WProofreader language. Read from the host page's `<html lang>` so a
|
|
193
|
+
* Learnosity activity rendered into a French page proofreads in French without
|
|
194
|
+
* the host app having to pass `lang` explicitly. Normalise `en-US` style to
|
|
195
|
+
* `en_US` (WProofreader's expected separator).
|
|
196
|
+
*
|
|
197
|
+
* WProofreader requires a region-qualified code: bare `en` is rejected with
|
|
198
|
+
* "Language 'en' is unsupported or doesn't exist." So we only use the host
|
|
199
|
+
* page's lang when it includes a region; otherwise we fall back to `en_US`.
|
|
200
|
+
*/
|
|
201
|
+
function defaultLang() {
|
|
202
|
+
if (typeof document === 'undefined') return 'en_US';
|
|
203
|
+
const html = document.documentElement;
|
|
204
|
+
if (!html || !html.lang) return 'en_US';
|
|
205
|
+
const lang = html.lang;
|
|
206
|
+
if (lang.indexOf('-') === -1 && lang.indexOf('_') === -1) return 'en_US';
|
|
207
|
+
return lang.replace('-', '_');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
* Coerce caller input into a valid subset of ALLOWED_SURFACES. Non-arrays
|
|
212
|
+
* fall back to DEFAULT_SURFACES; unrecognised entries are dropped silently.
|
|
213
|
+
*/
|
|
214
|
+
function normalizeSurfaces(input) {
|
|
215
|
+
if (!Array.isArray(input)) return DEFAULT_SURFACES.slice();
|
|
216
|
+
return input.filter(function (s) { return ALLOWED_SURFACES.indexOf(s) !== -1; });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/*
|
|
220
|
+
* Cheap page-level pre-check: is any allowed-surface Learnosity global on the
|
|
221
|
+
* page at all? If not, no point doing any per-editor work.
|
|
222
|
+
*/
|
|
223
|
+
function isAnyAllowedSurfaceLoaded(surfaces) {
|
|
224
|
+
if (typeof window === 'undefined') return false;
|
|
225
|
+
if (surfaces.length === 0) return false;
|
|
226
|
+
for (let i = 0; i < surfaces.length; i++) {
|
|
227
|
+
const g = SURFACE_GLOBALS[surfaces[i]];
|
|
228
|
+
if (g && typeof window[g] !== 'undefined') return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* Per-editor surface detection. Walks the editor's ancestry looking for the
|
|
235
|
+
* smallest set of class names that distinguish each Learnosity API in the
|
|
236
|
+
* rendered DOM. Markers confirmed from `Learnosity/learnosity-apis-css`:
|
|
237
|
+
*
|
|
238
|
+
* - Author API uses the hyphenated `lrn-author` / `lrn-author-*` prefix
|
|
239
|
+
* exclusively (no other API does).
|
|
240
|
+
* - Items API (Assess) wraps its player in `.lrn_player`, also unique.
|
|
241
|
+
* - Everything else `lrn_*` falls through to Questions API as the catch-all.
|
|
242
|
+
*
|
|
243
|
+
* Best effort. If Learnosity renames these between LTS versions, the update
|
|
244
|
+
* is isolated to this function. The `customSelectors` option is the escape
|
|
245
|
+
* hatch for unusual host apps.
|
|
246
|
+
*/
|
|
247
|
+
function detectSurface(el) {
|
|
248
|
+
let cur = el && el.parentElement;
|
|
249
|
+
while (cur) {
|
|
250
|
+
const list = cur.classList;
|
|
251
|
+
if (list && list.length) {
|
|
252
|
+
for (let i = 0; i < list.length; i++) {
|
|
253
|
+
const cls = list[i];
|
|
254
|
+
if (cls.indexOf('lrn-author') === 0) return 'author';
|
|
255
|
+
if (cls.indexOf('lrn_player') === 0) return 'items';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
cur = cur.parentElement;
|
|
259
|
+
}
|
|
260
|
+
return 'questions';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
* Learnosity rewrites the host-page placeholder span during render and does
|
|
265
|
+
* not preserve our `learnosity-response` class on the result. So instead of
|
|
266
|
+
* scoping by a single anchor class, we walk up the DOM from each candidate
|
|
267
|
+
* editable and check for any ancestor whose class matches Learnosity's
|
|
268
|
+
* conventions ("learnosity-response" exact, or any class starting with "lrn").
|
|
269
|
+
* Caller-supplied selectors (customSelectors) extend the scope for unusual
|
|
270
|
+
* host apps without giving up the Learnosity-internal scoping for the rest.
|
|
271
|
+
*/
|
|
272
|
+
function hasLearnosityAncestor(el, customSelectors) {
|
|
273
|
+
let cur = el && el.parentElement;
|
|
274
|
+
while (cur) {
|
|
275
|
+
const list = cur.classList;
|
|
276
|
+
if (list) {
|
|
277
|
+
for (let i = 0; i < list.length; i++) {
|
|
278
|
+
const cls = list[i];
|
|
279
|
+
if (cls === 'learnosity-response' || cls.indexOf('lrn') === 0) return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (customSelectors && customSelectors.length && cur.matches) {
|
|
283
|
+
for (let j = 0; j < customSelectors.length; j++) {
|
|
284
|
+
try { if (cur.matches(customSelectors[j])) return true; } catch (_) { /* bad selector */ }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
cur = cur.parentElement;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/*
|
|
293
|
+
* Inject the WProofreader bundle `<script>` exactly once per page. The
|
|
294
|
+
* `data-wproofreader-bundle` attribute serves as the idempotency marker.
|
|
295
|
+
*/
|
|
296
|
+
function loadBundle(url) {
|
|
297
|
+
if (typeof document === 'undefined') return;
|
|
298
|
+
if (document.querySelector('script[data-wproofreader-bundle]')) return;
|
|
299
|
+
const script = document.createElement('script');
|
|
300
|
+
script.src = url;
|
|
301
|
+
script.async = true;
|
|
302
|
+
script.dataset.wproofreaderBundle = '';
|
|
303
|
+
document.head.appendChild(script);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*
|
|
307
|
+
* Watch the document for editables appearing or disappearing inside Learnosity
|
|
308
|
+
* scope. On addition, attach. On removal, detach so SPA hosts that rebuild
|
|
309
|
+
* Learnosity widgets do not leak WProofreader instances.
|
|
310
|
+
*/
|
|
311
|
+
function observeLearnosity(config) {
|
|
312
|
+
if (typeof document === 'undefined') return;
|
|
313
|
+
|
|
314
|
+
const scanRoot = function (root) { scanForEditables(root, config); };
|
|
315
|
+
scanRoot(document);
|
|
316
|
+
|
|
317
|
+
const observer = new MutationObserver(function (mutations) {
|
|
318
|
+
for (let i = 0; i < mutations.length; i++) {
|
|
319
|
+
const m = mutations[i];
|
|
320
|
+
if (m.type === 'childList') {
|
|
321
|
+
const added = m.addedNodes;
|
|
322
|
+
for (let j = 0; j < added.length; j++) {
|
|
323
|
+
const node = added[j];
|
|
324
|
+
if (node && node.nodeType === 1 /* ELEMENT_NODE */) scanRoot(node);
|
|
325
|
+
}
|
|
326
|
+
const removed = m.removedNodes;
|
|
327
|
+
for (let k = 0; k < removed.length; k++) {
|
|
328
|
+
const node = removed[k];
|
|
329
|
+
if (node && node.nodeType === 1) detachInSubtree(node);
|
|
330
|
+
}
|
|
331
|
+
} else if (m.type === 'attributes' && m.attributeName === 'contenteditable') {
|
|
332
|
+
const el = m.target;
|
|
333
|
+
if (el && matchesCandidate(el, config) && hasLearnosityAncestor(el, config.customSelectors)) {
|
|
334
|
+
attachToContainer(el, config);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
observer.observe(document.body || document.documentElement, {
|
|
340
|
+
childList: true,
|
|
341
|
+
subtree: true,
|
|
342
|
+
// Catch editors whose `contenteditable` flips to truthy after insertion.
|
|
343
|
+
attributes: true,
|
|
344
|
+
attributeFilter: ['contenteditable']
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
* Find every editable under `root` that sits inside a Learnosity-rendered
|
|
350
|
+
* subtree, and attach. `root` can be the document or a freshly-added subtree.
|
|
351
|
+
*/
|
|
352
|
+
function scanForEditables(root, config) {
|
|
353
|
+
if (!root || !root.querySelectorAll) return;
|
|
354
|
+
const candidates = [];
|
|
355
|
+
if (matchesCandidate(root, config)) candidates.push(root);
|
|
356
|
+
const inside = root.querySelectorAll(candidateSelector(config));
|
|
357
|
+
for (let i = 0; i < inside.length; i++) candidates.push(inside[i]);
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
360
|
+
const el = candidates[i];
|
|
361
|
+
if (hasLearnosityAncestor(el, config.customSelectors)) attachToContainer(el, config);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/*
|
|
366
|
+
* Selector for "could plausibly be an editable WProofreader should attach to."
|
|
367
|
+
* Always includes iframes (CKEditor classic) and truthy contenteditables.
|
|
368
|
+
* Includes plain text inputs only when enableShorttext is on.
|
|
369
|
+
*/
|
|
370
|
+
function candidateSelector(config) {
|
|
371
|
+
let s = 'iframe, [contenteditable]:not([contenteditable="false"])';
|
|
372
|
+
if (config.enableShorttext) s += ', input[type="text"], input:not([type])';
|
|
373
|
+
return s;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/*
|
|
377
|
+
* Element-level equivalent of `candidateSelector`. Used by the
|
|
378
|
+
* attribute-mutation branch of `observeLearnosity`, where we already have the
|
|
379
|
+
* target element and don't want to re-query the DOM.
|
|
380
|
+
*/
|
|
381
|
+
function matchesCandidate(el, config) {
|
|
382
|
+
if (!el || el.nodeType !== 1) return false;
|
|
383
|
+
if (el.tagName === 'IFRAME') return true;
|
|
384
|
+
if (el.tagName === 'INPUT' && config.enableShorttext) {
|
|
385
|
+
const t = (el.getAttribute('type') || 'text').toLowerCase();
|
|
386
|
+
return t === 'text';
|
|
387
|
+
}
|
|
388
|
+
if (!el.hasAttribute || !el.hasAttribute('contenteditable')) return false;
|
|
389
|
+
return el.getAttribute('contenteditable') !== 'false';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/*
|
|
393
|
+
* Initialise WProofreader against a container element. Three gates before we
|
|
394
|
+
* actually attach:
|
|
395
|
+
* - dedupe against `attachedContainers` (no double-attaches across observer
|
|
396
|
+
* events).
|
|
397
|
+
* - page-level pre-check: at least one allowed-surface Learnosity global
|
|
398
|
+
* must be loaded.
|
|
399
|
+
* - per-editor surface filter: `detectSurface(container)` must be in
|
|
400
|
+
* `config.surfaces`.
|
|
401
|
+
* Then, for an iframe whose document is not yet loaded, defer until the
|
|
402
|
+
* `load` event so WProofreader sees the editable body. WProofreader's bundle
|
|
403
|
+
* may still be loading, so we also wait for `window.WEBSPELLCHECKER` to
|
|
404
|
+
* appear. The returned instance is tracked so we can destroy it later when
|
|
405
|
+
* the host element is removed.
|
|
406
|
+
*/
|
|
407
|
+
function attachToContainer(container, config) {
|
|
408
|
+
if (!container || attachedContainers.has(container)) return;
|
|
409
|
+
if (!isAnyAllowedSurfaceLoaded(config.surfaces)) return;
|
|
410
|
+
const surface = detectSurface(container);
|
|
411
|
+
if (config.surfaces.indexOf(surface) === -1) return;
|
|
412
|
+
attachedContainers.add(container);
|
|
413
|
+
|
|
414
|
+
const go = function () {
|
|
415
|
+
whenAvailable('WEBSPELLCHECKER', function (WSC) {
|
|
416
|
+
const initArgs = Object.assign({}, config.wproofreader, { container: container });
|
|
417
|
+
try {
|
|
418
|
+
WSC.init(initArgs, function (instance) {
|
|
419
|
+
if (instance) containerToInstance.set(container, instance);
|
|
420
|
+
});
|
|
421
|
+
} catch (_) { /* WProofreader handled the failure internally */ }
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const isIframe = container.tagName && container.tagName.toLowerCase() === 'iframe';
|
|
426
|
+
if (isIframe) {
|
|
427
|
+
let doc = null;
|
|
428
|
+
try { doc = container.contentDocument; } catch (_) { /* cross-origin */ }
|
|
429
|
+
if (doc && doc.readyState === 'complete') {
|
|
430
|
+
go();
|
|
431
|
+
} else {
|
|
432
|
+
container.addEventListener('load', go, { once: true });
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
go();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/*
|
|
440
|
+
* Walk a removed subtree, find any container we previously attached, destroy
|
|
441
|
+
* its WProofreader instance, and forget it. WeakSet/WeakMap don't enumerate,
|
|
442
|
+
* so we re-walk the DOM with `has()` checks instead.
|
|
443
|
+
*/
|
|
444
|
+
function detachInSubtree(root) {
|
|
445
|
+
if (!root) return;
|
|
446
|
+
if (attachedContainers.has(root)) detachFromContainer(root);
|
|
447
|
+
if (root.querySelectorAll) {
|
|
448
|
+
// Match the broadest possible candidate selector so we catch anything we
|
|
449
|
+
// could have attached to under any config.
|
|
450
|
+
const candidates = root.querySelectorAll('iframe, [contenteditable], input[type="text"], input:not([type])');
|
|
451
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
452
|
+
if (attachedContainers.has(candidates[i])) detachFromContainer(candidates[i]);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/*
|
|
458
|
+
* Destroy the WProofreader instance attached to `container` and forget the
|
|
459
|
+
* mapping. Safe to call on containers we never attached to.
|
|
460
|
+
*/
|
|
461
|
+
function detachFromContainer(container) {
|
|
462
|
+
const inst = containerToInstance.get(container);
|
|
463
|
+
if (inst) {
|
|
464
|
+
try {
|
|
465
|
+
if (typeof inst.destroy === 'function') inst.destroy();
|
|
466
|
+
} catch (_) { /* swallow */ }
|
|
467
|
+
containerToInstance.delete(container);
|
|
468
|
+
}
|
|
469
|
+
attachedContainers.delete(container);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/*
|
|
473
|
+
* Resolve a global once it appears on `window`, with a hard timeout so a
|
|
474
|
+
* missing script does not leak an interval forever.
|
|
475
|
+
*/
|
|
476
|
+
function whenAvailable(globalName, callback) {
|
|
477
|
+
if (typeof window === 'undefined') return;
|
|
478
|
+
if (window[globalName]) { callback(window[globalName]); return; }
|
|
479
|
+
const interval = setInterval(function () {
|
|
480
|
+
if (window[globalName]) {
|
|
481
|
+
clearInterval(interval);
|
|
482
|
+
clearTimeout(timeout);
|
|
483
|
+
callback(window[globalName]);
|
|
484
|
+
}
|
|
485
|
+
}, GLOBAL_POLL_INTERVAL_MS);
|
|
486
|
+
const timeout = setTimeout(function () {
|
|
487
|
+
clearInterval(interval);
|
|
488
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
489
|
+
console.warn('[wproofreader-learnosity] window.' + globalName +
|
|
490
|
+
' never appeared. Proofreading will not activate.');
|
|
491
|
+
}
|
|
492
|
+
}, GLOBAL_WAIT_TIMEOUT_MS);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/*
|
|
496
|
+
* Public API. Exported as a default object so the import shape mirrors the
|
|
497
|
+
* call site:
|
|
498
|
+
*
|
|
499
|
+
* import LearnosityWProofreader from '@webspellchecker/wproofreader-learnosity';
|
|
500
|
+
* LearnosityWProofreader.init({ wproofreader: { serviceId: '...' } });
|
|
501
|
+
*/
|
|
502
|
+
export default { init };
|