├── .gitignore ├── src ├── interfaces │ ├── ILanguage.ts │ ├── IServiceSettings.ts │ ├── IGrammarCheckerSettings.ts │ └── editable-wrapper.ts ├── utils │ ├── uuid.ts │ └── dom.ts ├── highlight-spec.ts ├── utils.ts ├── prosemirror.html └── beyond-grammar-prosemirror-plugin.ts ├── prosemirror.d.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── appveyor.yml ├── rangy.d.ts ├── conf ├── webpack-demo.conf.js └── webpack-demo.CI.conf.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | build/ 4 | 5 | 6 | npm-debug.log 7 | *.orig 8 | dist/ 9 | -------------------------------------------------------------------------------- /src/interfaces/ILanguage.ts: -------------------------------------------------------------------------------- 1 | export interface ILanguage{ 2 | displayName: string; 3 | isoCode: string; 4 | isEnabled : boolean; 5 | } -------------------------------------------------------------------------------- /src/interfaces/IServiceSettings.ts: -------------------------------------------------------------------------------- 1 | export interface IServiceSettings{ 2 | sourcePath ?: string; 3 | serviceUrl ?: string; 4 | userId ?: string; 5 | apiKey ?: string; 6 | } -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates unique id 3 | * @return {string} 4 | */ 5 | export function uuid_() { 6 | let res = ''; 7 | 8 | for (let i = 0; i < 32; i++) { 9 | res += Math.floor(Math.random() * 16).toString(16).toUpperCase(); 10 | } 11 | 12 | return res; 13 | } -------------------------------------------------------------------------------- /prosemirror.d.ts: -------------------------------------------------------------------------------- 1 | import * as PRModel from "@types/prosemirror-model"; 2 | import * as PRState from "@types/prosemirror-state"; 3 | import * as PRView from "@types/prosemirror-view"; 4 | 5 | declare type ExternalProseMirror = { 6 | view : typeof PRView; 7 | state : typeof PRState; 8 | model : typeof PRModel; 9 | } -------------------------------------------------------------------------------- /src/interfaces/IGrammarCheckerSettings.ts: -------------------------------------------------------------------------------- 1 | export interface IGrammarCheckerSettings { 2 | languageIsoCode ?: string; 3 | checkGrammar ?: boolean; 4 | checkSpelling ?: boolean; 5 | checkStyle ?: boolean; 6 | showThesaurusByDoubleClick ?: boolean; 7 | checkerIsEnabled ?: boolean; 8 | heavyGrammar ?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "inlineSourceMap": false, 8 | "experimentalDecorators": true, 9 | "typeRoots": ["./node_modules/@types"] 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/highlight-spec.ts: -------------------------------------------------------------------------------- 1 | import {HighlightInfo, Tag} from "./interfaces/editable-wrapper"; 2 | import {uuid_} from "./utils/uuid"; 3 | 4 | export class HighlightSpec{ 5 | id: string; 6 | highlightInfo: HighlightInfo; 7 | inclusiveStart: boolean = true; 8 | inclusiveEnd: boolean = true; 9 | 10 | constructor(public tag: Tag, public word : string, public ignored : boolean = false){ 11 | this.id = 'pwa-' + uuid_(); 12 | 13 | this.highlightInfo = new HighlightInfo(); 14 | this.highlightInfo.category=tag.category; 15 | this.highlightInfo.hint=tag.hint; 16 | this.highlightInfo.suggestions=tag.suggestions; 17 | this.highlightInfo.word = word; 18 | this.highlightInfo.ruleId = tag.ruleId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import {BeyondGrammarModule} from "../interfaces/editable-wrapper"; 2 | 3 | export function getWindow_(el: Node){ 4 | if( document == el.ownerDocument ) { 5 | return window 6 | } 7 | let doc = el.ownerDocument || document; 8 | return doc.defaultView || (doc).parentWindow; 9 | } 10 | 11 | export function loadBeyondGrammarModule_(src : string, onLoad ?: (module:BeyondGrammarModule)=> void){ 12 | let script = document.createElement("script"); 13 | script.src = src;//"bundle.js?r=" + Math.ceil(Math.random() * 1e6); 14 | (document.head || document.body).appendChild(script); 15 | 16 | let r = false; 17 | script.onload = script["onreadystatechange"] = function(){ 18 | if ( !r && (!this.readyState || this.readyState == 'complete') ) 19 | { 20 | r = true; 21 | onLoad && onLoad( module = window["BeyondGrammar"]); 22 | } 23 | } 24 | } 25 | 26 | export class DocRange_ { 27 | constructor(from: number, to: number){ 28 | this.from=from; 29 | this.to=to; 30 | } 31 | from: number; 32 | to: number; 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beyondgrammar-prosemirror", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --port 8082 --config ./conf/webpack-demo.conf.js --content-base build/", 8 | "startLocal:": "webpack-dev-server --port 8082 --config ./conf/webpack-demo.conf.js --content-base build/", 9 | "build": "./node_modules/.bin/webpack --config ./conf/webpack-demo.conf.js ", 10 | "build:CI": "./node_modules/.bin/webpack --config ./conf/webpack-demo.CI.conf.js " 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/jquery": "2.0.48", 16 | "@types/node": "8.0.15", 17 | "@types/prosemirror-model": "1.0.0", 18 | "@types/prosemirror-state": "1.0.0", 19 | "@types/prosemirror-view": "1.0.0", 20 | "@types/prosemirror-transform": "1.0.0", 21 | "copy-webpack-plugin": "^4.0.1", 22 | "css-loader": "^0.28.1", 23 | "expose-loader": "^0.7.1", 24 | "graceful-fs": "^4.1.10", 25 | "style-loader": "^0.17.0", 26 | "ts-loader": "^1.1.0", 27 | "typescript": "2.2.2", 28 | "url-loader": "^0.5.8", 29 | "webpack": "^1.13.2", 30 | "webpack-dev-server": "^1.16.2" 31 | }, 32 | "dependencies": { 33 | "core-js": "2.4.1", 34 | "jquery": "^2.2.4", 35 | "prosemirror-model": "1.0.1", 36 | "prosemirror-state": "1.0.1", 37 | "prosemirror-transform": "1.0.1", 38 | "prosemirror-view": "1.0.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "6" 4 | 5 | # Install scripts. (runs after repo cloning) 6 | install: 7 | # Get the latest stable version of Node.js or io.js 8 | - ps: Install-Product node $env:nodejs_version 9 | # install modules 10 | - npm install 11 | 12 | # Post-install test scripts. 13 | test_script: 14 | # Output useful info for debugging. 15 | # - node --version 16 | # - npm --version 17 | # run tests 18 | # npm test -- singleRun 19 | 20 | 21 | # Don't actually build. 22 | #build: off 23 | build_script: 24 | - node --version 25 | - npm --version 26 | - npm run build:CI 27 | 28 | artifacts: 29 | - path: 'dist/*.js' 30 | name: prosemirror 31 | 32 | notifications: 33 | - provider: Slack 34 | incoming_webhook: 35 | secure: Ayc6czyM66EzMrNgNMuWGV6m5ElTa7uzvin0/f6QBfttfkg1gfWy0lkddqkN9APXC4GCl5ekQIyZSpfZheCP7uHEmQC0Zi5Y2hzrMXLrmvE= 36 | channel: web-build 37 | on_build_success: true 38 | on_build_failure: true 39 | on_build_status_changed: false 40 | 41 | deploy: 42 | - provider: AzureBlob 43 | storage_account_name: prowritingaid 44 | storage_access_key: 45 | secure: gzyqglVhkink16D5LDmANUnHDoe/yvOCDWr79JlBDX5pSf6ewKBwGr1ttNd0a08+J/rQbyEmXw8mLxDLoBJwrFxYqsZQIMEhcR7chDy5p2aNK+wA5IxmI2NFwtW8LaWb 46 | container: cdn 47 | folder: $(APPVEYOR_PROJECT_SLUG)\$(APPVEYOR_BUILD_VERSION) 48 | artifact: prosemirror 49 | unzip: false 50 | set_content_type: true 51 | -------------------------------------------------------------------------------- /rangy.d.ts: -------------------------------------------------------------------------------- 1 | type NodePosition = {node: Node, start: number, end: number} 2 | 3 | type TextAndMap = { 4 | text: string; 5 | map: Array 6 | }; 7 | 8 | type CharacterOptions = { 9 | includeBlockContentTrailingSpace?: boolean; 10 | includeSpaceBeforeBr? : boolean; 11 | includePreLineTrailingSpace? : boolean; 12 | includeTrailingSpace?: boolean; 13 | ignoreCharacters? : boolean; 14 | ignoreTagNames? : {[key: string] : boolean}; 15 | rootElement? : HTMLElement; 16 | } 17 | 18 | interface TextRange { 19 | textAndMap(characterOptions?: CharacterOptions): TextAndMap; 20 | text(characterOptions?: CharacterOptions): string; 21 | pasteHtml(html : string); 22 | } 23 | 24 | interface ClassApplier{ 25 | elementAttributes : {[key: string] : string | number | boolean}; 26 | elementProperties : {[key: string] : any}; 27 | applyToRange(range: Range); 28 | undoToRange(range : Range); 29 | isAppliedToRange(range : Range): boolean; 30 | } 31 | 32 | interface RangyRange extends Range, TextRange { 33 | selectCharacters(containerNode: Node, startIndex:number, endIndex:number); 34 | setStartAndEnd(startNode: Node, startOffset: number, endNode?: Node, endOffset?: number): any; 35 | setStartAndEnd(startNode: Node, startOffset: number, endOffset: number): any; 36 | getNodes(nodeTypes: number[]): Node[]; 37 | selectNodeContents(element: HTMLElement); 38 | moveEnd(unit : "word"|"character", count : number, opts ?:any ); 39 | moveStart(unit : "word"|"character", count : number, opts ?:any ); 40 | cloneRange():RangyRange; 41 | isValid() : boolean; 42 | nativeRange : Range; 43 | isValid():boolean; 44 | } 45 | 46 | interface RangySelection extends Selection{ 47 | setSingleRange(range: Range); 48 | } 49 | 50 | interface RangyStatic extends TextRange { 51 | init(); 52 | createRange(win?:Window): RangyRange; 53 | createClassApplier(theClass : string, options? : any): ClassApplier; 54 | getSelection(win?: Window): RangySelection; 55 | saveSelection(win?: Window): Object; 56 | restoreSelection(sel: Object); 57 | } 58 | 59 | declare var rangy: RangyStatic; 60 | 61 | declare module "rangy" { 62 | export = rangy; 63 | } -------------------------------------------------------------------------------- /conf/webpack-demo.conf.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gracefulFs = require('graceful-fs'); 3 | gracefulFs.gracefulify(fs); 4 | 5 | var webpack = require('webpack'), 6 | CopyWebpackPlugin = require('copy-webpack-plugin'), 7 | path = require('path'); 8 | 9 | const ROOT = path.resolve('.'); 10 | 11 | module.exports = { 12 | watch : true, 13 | stats: { colors: true, reasons: true }, 14 | debug: true, 15 | 16 | output: { 17 | path : path.resolve('dist'), 18 | filename: '[name].js', 19 | chunkFilename: '[id].chunk.js' 20 | }, 21 | 22 | entry: { 23 | 'beyond-grammar-plugin' : "./src/beyond-grammar-prosemirror-plugin.ts" 24 | }, 25 | 26 | plugins: [ 27 | new CopyWebpackPlugin([ 28 | { from: './src/prosemirror.html', to: './' }, 29 | { context : './src', from: {glob : './icons/**/*'}, to:'./' } 30 | ]), 31 | 32 | /*new webpack.optimize.UglifyJsPlugin({ 33 | toplevel: true, 34 | mangle: 35 | { 36 | regex: /_$/, 37 | props: { 38 | regex: /_$/, 39 | toplevel: true 40 | } 41 | }, 42 | compress: { 43 | warnings: false, 44 | drop_console: true 45 | } 46 | })*/ 47 | ], 48 | 49 | resolve: { 50 | extensions: [ '', '.ts', '.es6', '.js', '.json' ], 51 | modules: [ 52 | path.join(ROOT, "modules"), 53 | path.join(ROOT, 'node_modules'), 54 | 'node_modules' 55 | ] 56 | }, 57 | module: { 58 | loaders: [ 59 | {test: /\.ts$/, loader: 'ts-loader?project=./tsconfig.json'}, 60 | {test : /\.png$/, loader : "url-loader"} 61 | ] 62 | }, 63 | 64 | devServer: { 65 | contentBase: './', 66 | quite: false, 67 | proxy: { 68 | "/api/v1": { 69 | target: "http://rtgrammarapi.azurewebsites.net/", 70 | changeOrigin: true 71 | }, 72 | "/api/language": { 73 | target: "http://rtgrammarapi.azurewebsites.net/", 74 | changeOrigin: true 75 | } 76 | } 77 | } 78 | }; 79 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BeyondGrammar plugin for ProseMirror editor 2 | 3 | Bring real-time spelling, grammar and style checking into your ProseMirror editor. Perfect for CMSs, help desk systems and blogs. 4 | 5 | To add real-time grammar, spell and style checking to your ProseMirror editor you just need to install this add-in. It's free for individuals and reasonably proced for multi-user licenses. 6 | 7 | ## Why choose our plugin? 8 | 9 | Our state-of-the-art grammar checker is used by over 500,000 users. It contains many unique features that you won't find in other solutions. These include: 10 | 11 | - Entity spellchecking. We check over 2 million people, places, teams, towns and other terms. It highlights where you may have incorrectly spelled a name. e.g. Andy Murrey => Andy Murray 12 | 13 | - Contextual spelling. We use artificial intelligence to highlight where you may have used a word in the incorrect context. It's easy to mistype a word, or slip a homonym in the wrong place, but we will highlight these, e.g. He is my best fiend in all the world. or I love my knew shoes. Most other grammar checkers just use simple rules than only catch a fraction of possible mistakes. 14 | 15 | - Style checking. We include over 15,000 potential style improvements for you text to ensure that it's not only grammatically correct, but also well-written and punchy. 16 | 17 | - Contextual thesaurus. Our contextual thesaurus looks at the context of the word you want to look up. It then limits the suggestions to just those so you can quickly pick the right synonym. There's also a full thesaurus option if you prefer that. 18 | 19 | ## How to build and see an example 20 | 21 | - Navigate to the root directory 22 | 23 | - Run `npm update` 24 | 25 | - Run `npm start` 26 | 27 | - Navigate to [http://localhost:8082/webpack-dev-server/prosemirror.html](http://localhost:8082/webpack-dev-server/prosemirror.html) 28 | 29 | 30 | ## Example usage 31 | 32 | NOTE: You will need to [register](https://prowritingaid.com/en/App/BeyondGrammar) to get a API key first (FREE for Individuals)! 33 | 34 | You can see an example of how to use the plugin in the [prosemirror.html](https://github.com/prowriting/beyondgrammar-prosemirror/blob/master/src/prosemirror.html) file in this repository. 35 | 36 | You can use our CDB version to load the plugin code: 37 | 38 | https://prowriting.azureedge.net/beyondgrammar-prosemirror/1.0.10/dist/beyond-grammar-plugin.js 39 | 40 | JSFiddle Integration Demo 41 | -------------------------------------------------------------------------------- /conf/webpack-demo.CI.conf.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gracefulFs = require('graceful-fs'); 3 | gracefulFs.gracefulify(fs); 4 | 5 | var webpack = require('webpack'), 6 | CopyWebpackPlugin = require('copy-webpack-plugin'), 7 | path = require('path'); 8 | 9 | const ROOT = path.resolve('.'); 10 | 11 | module.exports = { 12 | stats: { colors: true, reasons: true }, 13 | debug: true, 14 | 15 | output: { 16 | path : path.resolve('dist'), 17 | filename: '[name].js', 18 | chunkFilename: '[id].chunk.js' 19 | }, 20 | 21 | entry: { 22 | 'beyond-grammar-plugin' : "./src/beyond-grammar-prosemirror-plugin.ts" 23 | }, 24 | 25 | plugins: [ 26 | function() 27 | { 28 | this.plugin("done", function(stats) 29 | { 30 | if (stats.compilation.errors && stats.compilation.errors.length) 31 | { 32 | console.log(stats.compilation.errors); 33 | process.exit(1); 34 | } 35 | // ... 36 | }); 37 | }, 38 | new CopyWebpackPlugin([ 39 | { from: './src/prosemirror.html', to: './' }, 40 | { context : './src', from: {glob : './icons/**/*'}, to:'./' }, 41 | ]), 42 | 43 | new webpack.optimize.UglifyJsPlugin({ 44 | toplevel: true, 45 | mangle: 46 | { 47 | regex: /_$/, 48 | props: { 49 | regex: /_$/, 50 | toplevel: true 51 | } 52 | }, 53 | compress: { 54 | warnings: false, 55 | drop_console: true 56 | } 57 | }) 58 | ], 59 | 60 | resolve: { 61 | extensions: [ '', '.ts', '.es6', '.js', '.json' ], 62 | modules: [ 63 | path.join(ROOT, "modules"), 64 | path.join(ROOT, 'node_modules'), 65 | 'node_modules' 66 | ] 67 | }, 68 | module: { 69 | loaders: [ 70 | {test: /\.ts$/, loader: 'ts-loader?project=./tsconfig.json'}, 71 | {test : /\.png$/, loader : "url-loader"} 72 | ] 73 | }, 74 | 75 | devServer: { 76 | contentBase: './', 77 | quite: false, 78 | proxy: { 79 | "/api/v1": { 80 | target: "http://rtgrammarapi.azurewebsites.net/", 81 | changeOrigin: true 82 | }, 83 | "/api/language": { 84 | target: "http://rtgrammarapi.azurewebsites.net/", 85 | changeOrigin: true 86 | } 87 | } 88 | } 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | //[pavel] Object assigning was stolen from : 3 | //https://github.com/sindresorhus/object-assign/blob/master/index.js 4 | import {Tag} from "./interfaces/editable-wrapper"; 5 | import {HighlightSpec} from "./highlight-spec"; 6 | import {getWindow_} from "./utils/dom"; 7 | 8 | export function objectAssign(target, source, ...attr) { 9 | let getOwnPropertySymbols = Object['getOwnPropertySymbols']; 10 | let hasOwnProperty = Object.prototype.hasOwnProperty; 11 | let propIsEnumerable = Object.prototype.propertyIsEnumerable; 12 | 13 | let from; 14 | let to = toObject(target); 15 | let symbols; 16 | 17 | for (let s = 1; s < arguments.length; s++) { 18 | from = Object(arguments[s]); 19 | 20 | for (let key in from) { 21 | if (hasOwnProperty.call(from, key)) { 22 | to[key] = from[key]; 23 | } 24 | } 25 | 26 | if (getOwnPropertySymbols) { 27 | symbols = getOwnPropertySymbols(from); 28 | for (let i = 0; i < symbols.length; i++) { 29 | if (propIsEnumerable.call(from, symbols[i])) { 30 | to[symbols[i]] = from[symbols[i]]; 31 | } 32 | } 33 | } 34 | } 35 | 36 | return to; 37 | } 38 | 39 | function toObject(val) { 40 | if (val === null || val === undefined) { 41 | throw new TypeError('Object.assign cannot be called with null or undefined'); 42 | } 43 | 44 | return Object(val); 45 | } 46 | 47 | export function createDecorationAttributesFromSpec(spec : HighlightSpec) { 48 | // noinspection ReservedWordAsName 49 | return { 50 | class: `pwa-mark pwa-mark-done${spec.ignored?" pwa-mark-ignored":""}`, 51 | nodeName : "span", 52 | "data-pwa-id" : spec.id, 53 | 'data-pwa-category': spec.tag.category.toLowerCase(), 54 | 'data-pwa-hint': spec.tag.hint, 55 | 'data-pwa-suggestions': spec.tag.suggestions.join("~"), 56 | 'data-pwa-dictionary-word' : spec.tag.text, 57 | 'data-pwa-rule-id' : spec.tag.ruleId, 58 | tabindex : '0' 59 | } 60 | } 61 | 62 | export function nodeAfterRange( srcRange : Range, nodes : Node[], container ?: Node, cycle : boolean = true ) : Node { 63 | container = container || document.body; 64 | 65 | let doc = getWindow_(container).document; 66 | for(let i = 0; i < nodes.length; i++){ 67 | let range = doc.createRange(); 68 | let node = nodes[i]; 69 | range.selectNode(node); 70 | if ( srcRange.compareBoundaryPoints(Range.START_TO_START, range) <= 0) { 71 | return node; 72 | } 73 | } 74 | 75 | //case when we have at the end of all highlight and we can start from first node 76 | if( nodes.length && cycle){ 77 | let range = doc.createRange(); 78 | range.selectNode(container); 79 | range.collapse(true); 80 | return nodeAfterRange(range, nodes, container, cycle); 81 | } 82 | return null 83 | } 84 | 85 | export function nodeSetCursor(node : Node, after : boolean = true) { 86 | let doc = getWindow_(node).document; 87 | 88 | let range = doc.createRange(); 89 | range.selectNodeContents(node); 90 | range.collapse(!after); 91 | 92 | let selection = doc.getSelection(); 93 | selection.removeAllRanges(); 94 | selection.addRange(range); 95 | } 96 | -------------------------------------------------------------------------------- /src/prosemirror.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Beyond Grammar - Prosemirror Test 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 44 | 45 |
46 | 47 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/interfaces/editable-wrapper.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {ILanguage} from "./ILanguage"; 4 | import {IGrammarCheckerSettings} from "./IGrammarCheckerSettings"; 5 | import {IServiceSettings} from "./IServiceSettings"; 6 | import {Node as PMNode} from "@types/prosemirror-model"; 7 | 8 | //Elements 9 | export const ELEMENT_TEXTAREA_UNDERLAY = "pwa-container"; 10 | 11 | //Class names 12 | export const CLASS_NAME_HIGHLIGHT = "pwa-mark"; 13 | export const CLASS_NAME_IGNORED_HIGHLIGHT = "pwa-mark-ignored"; 14 | 15 | //Class names with dots 16 | export const CLASS_HIGHLIGHT = ".pwa-mark"; 17 | export const CLASS_IGNORED_HIGHLIGHT = ".pwa-mark-ignored"; 18 | 19 | //Selector ready for use in jQuery 20 | export const SELECTOR_ALL_HIGHLIGHTS = CLASS_HIGHLIGHT; 21 | export const SELECTOR_EXCLUDE_HIGHLIGHTS = `:not(${CLASS_IGNORED_HIGHLIGHT})`; 22 | export const SELECTOR_ALL_HIGHLIGHTS_EXCLUDE_IGNORED = `${CLASS_HIGHLIGHT}:not(${CLASS_IGNORED_HIGHLIGHT})`; 23 | 24 | export interface BGOptions { 25 | service ?: IServiceSettings; 26 | grammar?: IGrammarCheckerSettings; 27 | } 28 | 29 | export interface GrammarCheckerConstuctor{ 30 | new (element: HTMLElement, serviceSettings : IServiceSettings, grammarCheckerSettings?: IGrammarCheckerSettings, editorWrapper?: IEditableWrapper):IGrammarChecker; 31 | } 32 | 33 | export interface ThrottledFunction { 34 | (...args:any[]):any; 35 | _name : string; 36 | cancel(); 37 | } 38 | 39 | export interface BeyondGrammarModule{ 40 | GrammarChecker : GrammarCheckerConstuctor; 41 | getThesaurusData(contextWindow : Window, $container : JQuery, $target:JQuery, isContextual : boolean ) : ThesaurusData; 42 | loadPwaMarkStyles(win : Window); 43 | } 44 | 45 | export interface INodeWrapper{ 46 | node : PMNode; 47 | textContent : string; 48 | } 49 | 50 | //Interfaces from BeyondGrammar Core : 51 | 52 | 53 | export interface DictionaryEntry { 54 | Id : string; 55 | Word : string; 56 | Replacement ?: string; 57 | } 58 | 59 | export interface IGrammarCheckerConstructor{ 60 | new ( element : HTMLElement, serviceSettings : IServiceSettings, grammarCheckerSettings ?: IGrammarCheckerSettings ): IGrammarChecker; 61 | } 62 | 63 | export interface IGrammarChecker { 64 | init() : Promise; 65 | 66 | activate(); 67 | deactivate(); 68 | isActivated(); 69 | 70 | checkAll(forceClearCache ?: boolean) : void; 71 | 72 | clearMarks(): void; 73 | reloadMarks(): void; 74 | 75 | setSettings(settings: IGrammarCheckerSettings): void; 76 | getSettings(): IGrammarCheckerSettings; 77 | 78 | getAvailableLanguages(): ILanguage[]; 79 | getApplicationName() : string; 80 | getApplicationVersion() : string; 81 | getVersionedApplicationName() : string; 82 | getCopyrightUrl() : string; 83 | getBrandImageUrl() : string; 84 | 85 | addToDictionary( word : string, replacement ?: string ) : Promise; 86 | removeFromDictionary( id : string ) : Promise; 87 | getDictionaryEntries() : Promise; 88 | } 89 | 90 | /*export class GrammarCheckerSettings { 91 | languageFilter ?: string[]; 92 | languageIsoCode ?: string; 93 | checkGrammar ?: boolean; 94 | checkSpelling ?: boolean; 95 | checkStyle ?: boolean; 96 | showThesaurusByDoubleClick ?: boolean; 97 | showContextThesaurus ?: boolean; 98 | checkerIsEnabled ?: boolean; 99 | disableDictionary ?:boolean; 100 | }*/ 101 | 102 | /*export interface ServiceSettings{ 103 | sourcePath?: string; 104 | serviceUrl ?: string; 105 | userId ?: string; 106 | apiKey ?: string; 107 | }*/ 108 | 109 | export interface Tag{ 110 | startPos : number; 111 | endPos : number; 112 | hint : string; 113 | suggestions : string[]; 114 | category : string; 115 | ruleId : string; 116 | text: string; 117 | } 118 | 119 | export interface TextAreaRange { 120 | underlay : HTMLElement; 121 | $underlay : JQuery; 122 | textarea : HTMLElement; 123 | $textarea : JQuery; 124 | 125 | selectionStart : number; 126 | selectionEnd : number 127 | } 128 | 129 | export interface ThesaurusData{ 130 | textAreaRange ?: TextAreaRange; 131 | wordRange : RangyRange; 132 | isContextual : boolean; 133 | word : string; 134 | context ?: string; 135 | start ?: number; 136 | end ?: number; 137 | } 138 | 139 | /*export enum UnbindEditableReason { 140 | RemovedFromDOM = 'removed-from-dom', 141 | NotVisibleAnyMore = 'not-visible-any-more', 142 | EditableMonitorStopped = 'editable-monitor-stopped', 143 | FrameControllerDestroyed = 'frame-controller-destroyed', 144 | EditorsSwitchedOff = 'editors-switched-off' 145 | }*/ 146 | 147 | export type MouseXY = [ number, number ]; 148 | 149 | export interface Position { 150 | top : number; 151 | left : number; 152 | } 153 | 154 | export interface Size { 155 | width : number; 156 | height : number; 157 | } 158 | 159 | export interface Rectangle extends Position, Size {} 160 | 161 | /* 162 | Implement this interface if you want to support a different type of editor, e.g. ProseMirror or CodeMirror or something else 163 | */ 164 | export interface IEditableWrapper { 165 | onShowThesaurus: (thesaurusData: ThesaurusData, mouseXY : MouseXY, contextWindow: Window)=>boolean; 166 | /* 167 | get the text from a specific element 168 | */ 169 | getText(blockElement: HTMLElement):string; 170 | /* 171 | Clear all the marks from the text 172 | */ 173 | clearMarks(skipIgnored : boolean): void; 174 | /* 175 | start bindings 176 | */ 177 | bindEditable(): void; 178 | /* 179 | end bindings 180 | */ 181 | unbindEditable(reason?: string): void; //UnbindEditableReason 182 | 183 | /* 184 | start change events being logged 185 | */ 186 | bindChangeEvents(): void; 187 | 188 | /* 189 | Stop change events being raised 190 | */ 191 | unbindChangeEvents() : void; 192 | 193 | /* 194 | Get all block elements 195 | */ 196 | getAllElements(): HTMLElement[]; 197 | 198 | /* 199 | Apply the highlights to the specified block 200 | */ 201 | applyHighlightsSingleBlock(elem: HTMLElement, text: string, tags: Tag[], ignoreSelectors:string[], removeExisting: boolean): void; 202 | 203 | /* 204 | Remove all highlights that match this uid 205 | */ 206 | onAddToDictionary(uid: string): void; 207 | 208 | /* 209 | Get the info for the specified highlight 210 | */ 211 | getHighlightInfo(uid: string): HighlightInfo; 212 | 213 | updateAfterPaste(): void; 214 | /* 215 | This should be called when a block of text is changed. Usually a paragraph. 216 | */ 217 | onBlockChanged: (block: HTMLElement)=> void; 218 | onPopupClose: (immediate?:boolean)=> void; 219 | onPopupDeferClose: ()=>void; 220 | onCheckRequired: ()=> void; 221 | onShowPopup: (uid:string, elem: Element, mouseXY : MouseXY,preventCloseByMouseLeave ?: boolean)=>void; 222 | getActiveHighlightUid: () => string | null; 223 | 224 | onPopupClosed : ()=>void; 225 | 226 | notifyCursorPositionChanged : ThrottledFunction; 227 | 228 | resetSpellCheck():void; 229 | restoreSpellCheck():void; 230 | /* 231 | Count the number of errors in the document 232 | */ 233 | getCurrentErrorCount(): number; 234 | 235 | /** 236 | * Get absolute position of text cursor on screen 237 | * @returns {{top: number; left: number}} 238 | */ 239 | getCursorScreenPosition():Rectangle; 240 | 241 | //applying user's choices 242 | /* 243 | Ignore the specified highlight 244 | */ 245 | ignore(uid: string): void; 246 | /* 247 | Omit the specified highlight 248 | */ 249 | omit(uid: string): void; 250 | /* 251 | Accept the specified highlight 252 | */ 253 | accept(uid: string, suggestion: string):void; 254 | /* 255 | Apply the specified replacement to the word selected for the thesaurus 256 | */ 257 | applyThesaurus( replacement : string ) : void; 258 | 259 | /* 260 | Gets the HTML 261 | */ 262 | getHtml(): string; 263 | /* 264 | Sets the HTML 265 | */ 266 | setHtml( html: string ): void; 267 | 268 | getAllMarks() : HTMLElement[]; 269 | getContainer():HTMLElement; 270 | 271 | updateActiveSelection(); 272 | 273 | scrollToHighlight(elem : HTMLElement); 274 | nextHighlight() : HTMLElement; 275 | prevHighlight() : HTMLElement; 276 | jumpToNextHighlight() : void; 277 | 278 | addHoveredClass($highlight : JQuery); 279 | removeHoveredClass($highlight : JQuery); 280 | } 281 | 282 | export class HighlightInfo{ 283 | public word: string; 284 | public category: string; 285 | public hint: string; 286 | public suggestions: string[]; 287 | public ruleId : string; 288 | } 289 | 290 | -------------------------------------------------------------------------------- /src/beyond-grammar-prosemirror-plugin.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {ExternalProseMirror} from "../prosemirror"; 3 | 4 | import { 5 | BGOptions, 6 | IEditableWrapper, 7 | HighlightInfo, 8 | Tag, 9 | ThesaurusData, 10 | BeyondGrammarModule, 11 | INodeWrapper, 12 | MouseXY, 13 | ThrottledFunction, 14 | Rectangle, 15 | SELECTOR_ALL_HIGHLIGHTS_EXCLUDE_IGNORED 16 | } from "./interfaces/editable-wrapper"; 17 | 18 | import {Node as PMNode} from "@types/prosemirror-model"; 19 | import {EditorState, PluginSpec, StateField, Transaction} from "@types/prosemirror-state"; 20 | import {EditorView, EditorProps, DecorationSet, Decoration} from "@types/prosemirror-view"; 21 | 22 | import * as $ from "jquery"; 23 | import {HighlightSpec} from "./highlight-spec"; 24 | import {createDecorationAttributesFromSpec, nodeAfterRange, nodeSetCursor, objectAssign} from "./utils"; 25 | import {DocRange_, getWindow_, loadBeyondGrammarModule_} from "./utils/dom"; 26 | 27 | 28 | const CSS_IGNORED_ = 'pwa-mark-ignored'; 29 | const PWA_DECO_UPDATE_META_ = 'pwa-deco-update'; 30 | 31 | export function createBeyondGrammarPluginSpec_(PM : ExternalProseMirror, element : HTMLElement, bgOptions ?: BGOptions ) { 32 | const DEFAULT_SETTINGS : BGOptions = { 33 | grammar : { 34 | heavyGrammar:true 35 | }, 36 | service : { 37 | sourcePath : "//cdn.prowritingaid.com/beyondgrammar/2.0.2893/dist/hayt/bundle.js" 38 | } 39 | }; 40 | 41 | bgOptions.grammar = objectAssign( DEFAULT_SETTINGS.grammar, bgOptions.grammar); 42 | bgOptions.service = objectAssign( DEFAULT_SETTINGS.service, bgOptions.service); 43 | bgOptions.grammar = { ...bgOptions.grammar, heavyGrammar: true }; 44 | 45 | let $element = $(element); 46 | let plugin = new BeyondGrammarProseMirrorPlugin($element, PM); 47 | 48 | loadBeyondGrammarModule_(bgOptions.service.sourcePath, (bgModule : BeyondGrammarModule)=>{ 49 | bgModule.loadPwaMarkStyles(window); 50 | let $contentEditable = $element.find("[contenteditable]"); 51 | let grammarChecker = new bgModule.GrammarChecker($contentEditable[0], bgOptions.service, bgOptions.grammar, plugin); 52 | grammarChecker.init().then(()=>{ 53 | grammarChecker.activate(); 54 | plugin.bgModule_ = bgModule; 55 | }); 56 | }); 57 | 58 | return plugin; 59 | } 60 | 61 | //idea to combine in one class two implementations: bg wrapper + pm plugin, so we can work in one scope 62 | export class BeyondGrammarProseMirrorPlugin implements PluginSpec, IEditableWrapper{ 63 | //Outside set data 64 | bgModule_:BeyondGrammarModule; 65 | editorView: EditorView; 66 | 67 | //Wrapper Callbacks 68 | onShowThesaurus: (thesaurusData: ThesaurusData, mouse:MouseXY, contextWindow: Window) => boolean; 69 | onBlockChanged: (block: HTMLElement) => void; 70 | onPopupClose: () => void; 71 | onPopupDeferClose: () => void; 72 | onCheckRequired: () => void; 73 | onShowPopup: (uid: string, elem: Element, mouseXY : MouseXY, preventCloseByMouseLeave ?: boolean)=>void; 74 | 75 | 76 | private isBound_ : boolean = false; 77 | private state_ : StateField; 78 | private props_ : EditorProps; 79 | private decos_ : DecorationSet; 80 | private doc_ : PMNode; 81 | private lastThesaurusData_ : ThesaurusData; 82 | 83 | constructor( private $element_ : JQuery, private PM_ : ExternalProseMirror){ 84 | this.initState_(); 85 | this.initProps_(); 86 | this.bindEditableEvents_(); 87 | } 88 | 89 | bindEditableEvents_() { 90 | this.$element_.on('scroll', ()=> { 91 | // close the popup, otherwise it moves away from the word 92 | if (this.onPopupClose){ 93 | this.onPopupClose(); 94 | } 95 | }); 96 | 97 | this.$element_.on('keydown', (evt)=> { 98 | // if we press a button in the text then close the popup 99 | if(evt.keyCode == 17){ 100 | //temporally solution for awhile we use ctrlKey for opening contextual thesaurus 101 | return; 102 | } 103 | if (this.onPopupClose){ 104 | this.onPopupClose(); 105 | } 106 | }); 107 | 108 | this.$element_.on('mouseover touchend', `.pwa-mark:not(.${CSS_IGNORED_})`, (evt: JQueryEventObject) => { 109 | let elem = evt.target, 110 | uid = elem.getAttribute('data-pwa-id'); 111 | 112 | if (this.onShowPopup){ 113 | let mouse : MouseXY = [evt.clientX, evt.clientY]; 114 | this.onShowPopup(uid, elem, mouse); 115 | } 116 | }); 117 | 118 | this.$element_.on('mouseleave', '.pwa-mark', () => { 119 | if (this.onPopupDeferClose){ 120 | this.onPopupDeferClose(); 121 | } 122 | }); 123 | } 124 | 125 | /** 126 | * Implementation of ProseMirror plugin interface 127 | */ 128 | 129 | initState_() { 130 | let self = this; 131 | this.decos_ = self.PM_.view.DecorationSet.empty; 132 | 133 | // noinspection JSUnusedLocalSymbols 134 | this.state_ = { 135 | init(config, state){ 136 | // we should start the checker 137 | //self.onCheckRequired(); 138 | // we do nothing here 139 | self.doc_= state.doc; 140 | //console.log(self.doc); 141 | return {decos : self.decos_}; 142 | }, 143 | 144 | apply( tr : Transaction, pluginState, old : EditorState, newState : EditorState ) { 145 | 146 | //console.log("apply value=", pluginState); 147 | 148 | //storing new doc, as it was changed after transactions 149 | self.doc_ = newState.doc; 150 | 151 | if (tr.docChanged) { 152 | // I think we need to update our decos using the mapping of 153 | // the transaction. This should update all the from and tos 154 | 155 | //As I understand it makes sense only if content was changed, in other case it is not necessary 156 | self.decos_ = self.decos_.map(tr.mapping, self.doc_); 157 | self.decos_ = self.invalidateDecorations_(self.decos_); 158 | } 159 | 160 | if (tr.docChanged) { 161 | self.onDocChangedTransaction_(tr); 162 | } 163 | 164 | return { decos : self.decos_ }; 165 | } 166 | } 167 | } 168 | 169 | onDocChangedTransaction_(tr : Transaction ){ 170 | // get the range that is affected by the transformation 171 | let range = this.rangeFromTransform_(tr); 172 | 173 | // update all the blocks that have been affected by the transformation 174 | this.doc_.nodesBetween(range.from, range.to, (elem, pos) => { 175 | if (elem.isTextblock) { 176 | if (this.onBlockChanged) { 177 | //console.info("onBlockChanged", elem); 178 | this.onBlockChanged(this.wrapNode_(elem, pos)); 179 | } 180 | return false; 181 | } 182 | return true; 183 | }); 184 | 185 | // set off a check 186 | if (this.onCheckRequired) { 187 | this.onCheckRequired(); 188 | } 189 | } 190 | 191 | initProps_() { 192 | let self = this; 193 | this.props_ = { 194 | decorations() { return this.spec.decos_ }, 195 | attributes : { spellcheck : "false" }, 196 | handleDoubleClick(){ 197 | if( !self.isBound_ ){ 198 | return true; 199 | } 200 | 201 | setTimeout(()=>{ 202 | self.processShowContextualThesaurus_(null); 203 | }, 10); 204 | 205 | return false; 206 | } 207 | } 208 | } 209 | 210 | protected processShowContextualThesaurus_($target : JQuery ) : boolean{ 211 | if( !this.onShowThesaurus ) return false; 212 | 213 | let thesaurusData = this.bgModule_.getThesaurusData(getWindow_(this.$element_[0]), this.$element_, $target, true); 214 | 215 | this.lastThesaurusData_ = thesaurusData; 216 | 217 | //TODO MouseXY 218 | return this.onShowThesaurus( thesaurusData, [0, 0], getWindow_(this.$element_[0])) 219 | } 220 | 221 | get state(): StateField{ 222 | return this.state_; 223 | } 224 | 225 | get props() : EditorProps{ 226 | return this.props_; 227 | } 228 | 229 | invalidateDecorations_(decos : DecorationSet) : DecorationSet{ 230 | let changed = decos 231 | .find() 232 | .filter((deco:Decoration)=> this.doc_.textBetween(deco.from, deco.to) != (deco.spec).word); 233 | return changed.length == 0 ? decos : decos.remove(changed); 234 | } 235 | 236 | applyHighlightsSingleBlock(elem:HTMLElement | INodeWrapper, text:string, tags:Tag[], ignoreSelectors : string[], removeExisting:boolean):void { 237 | 238 | // problem is PM is in most cases immutable, so if we started checking on one node, will type something 239 | // in dom we will have another node, but as we have it returned from closure this node can be 240 | // incorrect(removed from dom structure). So we should make checking by existing checked element 241 | // not it's content or text, as it is in dom, that means it is not changed, if it is not in dom, that 242 | // means it is not actual and we can skip check result. 243 | 244 | //unwrapping element, as elem is result of wrapNode method 245 | //store textContent as tag positions related to it 246 | let {node, textContent} = elem; 247 | 248 | let found = false; 249 | this.doc_.descendants((n:PMNode)=>{ 250 | if( node == n ) { 251 | found = true; 252 | } 253 | return !found; 254 | }); 255 | 256 | if(!found) return; //nothing to do 257 | 258 | let start = this.getPositionInDocument_(node); 259 | let length = text.length; 260 | // find the decos from the start and end of this element and remove them 261 | let decosForBlock = this.decos_.find(start,start + length); 262 | let newDecos = []; 263 | 264 | for(let i = 0; i < tags.length; i++){ 265 | let tag = tags[i]; 266 | let tagPos = { from : tag.startPos + start, to : tag.endPos + start + 1 }; 267 | let existing : Decoration = null; 268 | 269 | for(let k = 0; k < decosForBlock.length; k++){ 270 | let deco = decosForBlock[k]; 271 | let spec = deco.spec; 272 | if (deco.from===tagPos.from && deco.to===tagPos.to){ 273 | //update tag item with new tag instance 274 | spec.tag=tag; 275 | 276 | // As I understand we should make step backward, as if we've removed on k, k+1 in next iteration 277 | // skips, as it was shifted 278 | decosForBlock.splice(k--,1); 279 | 280 | existing=deco; 281 | break; 282 | } 283 | } 284 | 285 | // no existing, so we can say it is new??? 286 | if (existing===null) { 287 | // check for an existing decoration 288 | // 289 | let word = textContent.substring(tag.startPos, tag.endPos+1); 290 | let spec = new HighlightSpec(tag, word); 291 | let attributes = createDecorationAttributesFromSpec(spec); 292 | 293 | let deco = this.PM_.view.Decoration.inline(tagPos.from, tagPos.to, attributes, spec); 294 | 295 | newDecos.push(deco); 296 | } 297 | } 298 | 299 | this.decos_ = this.decos_.remove(decosForBlock).add(this.doc_, newDecos); 300 | 301 | this.applyDecoUpdateTransaction_(); 302 | } 303 | 304 | getHighlightInfo(uid:string):HighlightInfo { 305 | let decos = this.decos_; 306 | if (decos) { 307 | let deco = this.getDecoById_(uid); 308 | if (deco){ 309 | return deco.spec.highlightInfo; 310 | } 311 | } 312 | return null; 313 | } 314 | 315 | clearMarks(skipIgnored:boolean):void { 316 | if (skipIgnored) { 317 | //find not ignored decos and remove only it 318 | let notIgnoredDecos = this.decos_.find(undefined, undefined, (spec:HighlightSpec)=>!spec.ignored); 319 | this.decos_ = this.decos_.remove(notIgnoredDecos); 320 | }else { 321 | this.decos_ = this.PM_.view.DecorationSet.empty; 322 | } 323 | this.applyDecoUpdateTransaction_(); 324 | } 325 | 326 | ignore(uid:string):void { 327 | let deco = this.getDecoById_(uid); 328 | 329 | if( deco ) { 330 | this.applyDecoUpdateTransaction_(()=>{ 331 | //getting old spec, marking it as ignored and creating from it new ignored deco 332 | let spec = deco.spec; 333 | spec.ignored = true; 334 | 335 | let new_deco = this.PM_.view.Decoration.inline(deco.from, deco.to, createDecorationAttributesFromSpec(spec), spec); 336 | 337 | this.decos_ = this.decos_.remove([deco]).add(this.doc_, [new_deco]); 338 | return deco.to; 339 | }); 340 | } 341 | } 342 | 343 | omit(uid:string):void { 344 | let deco = this.getDecoById_(uid); 345 | if (deco){ 346 | this.applyDecoUpdateTransaction_((tr:Transaction)=>{ 347 | tr.delete(deco.from, deco.to); 348 | this.decos_ = this.decos_.remove([deco]); 349 | return deco.from; 350 | }); 351 | } 352 | } 353 | 354 | accept(uid:string, suggestion:string):void { 355 | let deco = this.getDecoById_(uid); 356 | if (deco){ 357 | this.applyDecoUpdateTransaction_((tr:Transaction)=>{ 358 | //let tr = this.editorView.state.tr; 359 | this.decos_ = this.decos_.remove([deco]); 360 | tr 361 | .replace(deco.from, deco.to) 362 | .insertText(suggestion, deco.from); 363 | return deco.from + suggestion.length; 364 | }); 365 | } 366 | } 367 | 368 | onAddToDictionary(uid:string):void { 369 | let deco = this.getDecoById_(uid); 370 | if (deco) { 371 | this.applyDecoUpdateTransaction_(()=>{ 372 | let specToAdd: HighlightSpec = deco.spec; 373 | let decosToRemove = this.decos_.find(null, null, (spec : HighlightSpec) => { 374 | return spec.tag.category == specToAdd.tag.category && spec.word == specToAdd.word; 375 | }); 376 | this.decos_ = this.decos_.remove(decosToRemove); 377 | return deco.to; 378 | }); 379 | } 380 | } 381 | 382 | applyThesaurus(replacement:string):void { 383 | this.applyDecoUpdateTransaction_((tr : Transaction)=>{ 384 | tr.insertText(replacement, tr.selection.from, tr.selection.from + this.lastThesaurusData_.word.length); 385 | return tr.selection.from + this.lastThesaurusData_.word.length; 386 | }); 387 | } 388 | 389 | getText(blockElement:HTMLElement):string { 390 | let node = (blockElement); 391 | return node.textContent; 392 | } 393 | 394 | getAllElements():HTMLElement[] { 395 | let result = []; 396 | this.doc_.descendants((node, pos)=>{ 397 | if (node.isTextblock){ 398 | result.push( this.wrapNode_( node, pos) ); 399 | return false; 400 | } 401 | return true; 402 | }); 403 | return result; 404 | } 405 | 406 | private wrapNode_(node : PMNode, pos : number) : INodeWrapper{ 407 | // we should re-write text content, as in real case textContent of DOM can't contains images and unsized elements 408 | // but PM can do this. So it's text block can contains images, so when we getting textContent we skips images 409 | // and broken indexed after image when adding highlights 410 | return { 411 | node : node, 412 | textContent : this.doc_.textBetween( pos, pos + node.nodeSize, "\n", "\n" ) 413 | } 414 | } 415 | 416 | getCurrentErrorCount():number { 417 | return this.decos_.find().length; 418 | } 419 | 420 | private applyDecoUpdateTransaction_(process ?: (tr:Transaction)=>number ){ 421 | let state = this.editorView.state; 422 | const tr = state.tr.setMeta(PWA_DECO_UPDATE_META_, true); 423 | 424 | const cursorPosition = process ? process(tr) : -1; 425 | 426 | // Update state (doc) before setting selection, otherwise ProseMirror complains: 427 | // - Selection passed to setSelection must point at the current document 428 | state = state.apply(tr); 429 | 430 | if( cursorPosition != -1 ){ 431 | state = state.apply( 432 | state.tr.setSelection(this.PM_.state.TextSelection.create(this.doc_, cursorPosition)) 433 | ); 434 | } 435 | 436 | this.editorView.updateState(state); 437 | 438 | if( cursorPosition != -1 ){ 439 | this.editorView.focus(); 440 | } 441 | } 442 | 443 | private getPositionInDocument_(theNode: PMNode): number{ 444 | let pos = 0; 445 | let finished = false; 446 | this.doc_.descendants((node, p)=>{ 447 | if (finished){ 448 | return false; 449 | } 450 | 451 | if( node.eq( theNode )) { 452 | pos = p + 1; 453 | finished = true; 454 | return false; 455 | } 456 | 457 | return true; 458 | }); 459 | return pos; 460 | } 461 | 462 | // noinspection JSMethodCanBeStatic 463 | private rangeFromTransform_(tr: Transaction): DocRange_ { 464 | let from, to; 465 | for (let i = 0; i < tr.steps.length; i++) { 466 | let step = tr.steps[i]; 467 | 468 | let stepMapping = step.getMap(); 469 | 470 | //new position after step 471 | let stepFrom = stepMapping.map(step.from || step.pos, -1); 472 | let stepTo = stepMapping.map(step.to || step.pos, 1); 473 | 474 | if( from ) { 475 | from = Math.min( stepMapping.map( from, -1 ), stepFrom ); 476 | } else { 477 | from = stepFrom; 478 | } 479 | 480 | if( to ) { 481 | to = Math.max( stepMapping.map(to, 1), stepTo ); 482 | } else { 483 | to = stepTo; 484 | } 485 | } 486 | 487 | return new DocRange_( from, to ); 488 | } 489 | 490 | private getDecoById_(uuid: string):Decoration{ 491 | let decos = this.decos_.find(null,null,spec=>(spec).id == uuid); 492 | return decos[0]; 493 | } 494 | 495 | jumpToNextHighlight() { 496 | let elem = this.nextHighlight(); 497 | 498 | if( elem ){ 499 | let uid = elem.getAttribute('data-pwa-id'); 500 | this.scrollToHighlight(elem); 501 | 502 | if (this.onShowPopup) { 503 | this.onShowPopup(uid, elem, [1, 1], true); 504 | } 505 | } 506 | } 507 | 508 | nextHighlight(): HTMLElement { 509 | let doc = getWindow_(this.$element_[0]).document; 510 | let selection = doc.getSelection(); 511 | if( !selection || selection.rangeCount == 0) { 512 | return; 513 | } 514 | 515 | let selectionRange = selection.getRangeAt(0).cloneRange(); 516 | 517 | return nodeAfterRange( 518 | selectionRange, 519 | this.$element_ 520 | .find(SELECTOR_ALL_HIGHLIGHTS_EXCLUDE_IGNORED) 521 | .toArray(), 522 | this.$element_[0] 523 | ); 524 | } 525 | 526 | scrollToHighlight(elem: HTMLElement) { 527 | nodeSetCursor(elem, true); 528 | elem.blur(); 529 | elem.focus(); 530 | } 531 | 532 | /** 533 | * Methods stubbed for awhile 534 | */ 535 | 536 | bindEditable():void { 537 | this.isBound_ = true; 538 | } 539 | 540 | unbindEditable():void { 541 | this.isBound_ = false; 542 | } 543 | 544 | updateActiveSelection() {}// ???? 545 | 546 | prevHighlight(): HTMLElement { return undefined;} 547 | 548 | getAllMarks(): HTMLElement[] { return []; } 549 | 550 | getContainer(): HTMLElement { return undefined; } 551 | 552 | bindChangeEvents():void { } 553 | 554 | unbindChangeEvents():void { } 555 | 556 | updateAfterPaste():void { } 557 | 558 | resetSpellCheck():void { } 559 | 560 | restoreSpellCheck():void { } 561 | 562 | addHoveredClass($highlight : JQuery){} 563 | removeHoveredClass($highlight : JQuery){} 564 | 565 | getActiveHighlightUid: () => (string | null);//skip 566 | notifyCursorPositionChanged: ThrottledFunction;//skip 567 | onPopupClosed: () => void; //skip 568 | getHtml(): string {return "";} //skip 569 | setHtml(html: string): void {} //skip 570 | getCursorScreenPosition(): Rectangle {return {left : 0, top : 0, width : 0, height : 0};} //skip 571 | } 572 | 573 | //Extending BeyondGrammar namespace 574 | window["BeyondGrammar"] = window["BeyondGrammar"] || {}; 575 | window["BeyondGrammar"].createBeyondGrammarPluginSpec = createBeyondGrammarPluginSpec_; 576 | --------------------------------------------------------------------------------