├── Changelog.md ├── README.md ├── css ├── spellcheck.css └── spellcheck.min.css ├── js ├── spellcheck.js └── spellcheck.min.js └── spellcheck.php /Changelog.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ============================ 3 | ### Version 1.6.1 ### 4 | spellcheck.js: 5 | * Updated `sc.trim()` method to use native trim if available 6 | * Clicking background overlay will now close the spell check window 7 | 8 | ### Version 1.5.3 ### 9 | spellcheck.css: 10 | * Added `box-sizing: content-box;` rule to elements within spell check box 11 | 12 | ### Version 1.5.2 ### 13 | spellcheck.js: 14 | * Overhauled undo functionality for improved performance and support for reversal of multiple, consecutive changes 15 | * Combined `_showReviewer()` with `_begin()` to eliminate an unnecessary function call 16 | * Removed some unnecessary variable copying 17 | 18 | ### Version 1.5.1 ### 19 | spellcheck.js: 20 | * Added `debug` option to view progress messages and server response in the console 21 | * Switched to a better method of handling server responses -- more reliable, improved error handling 22 | * XHR responses are now only handled if the spell checker is open -- closing before a request is completed effectively abandons the request 23 | 24 | spellcheck.css: 25 | * Added 1px #AAA border to spell check box 26 | 27 | ### Version 1.5 ### 28 | spellcheck.js: 29 | * Added `onOpen()` and `onClose()` callback methods 30 | * Added `destroy()` method for removing spell check functionality 31 | * The `name` option is now properly URI encoded prior to server request 32 | * Event message "OK" button is now given focus when displayed to allow closing with "Enter" button 33 | 34 | spellcheck.css: 35 | * Cleaned up formatting and organization, removed duplicate and unnecessary CSS styles 36 | * Added several IE7-specific enhancements 37 | 38 | spellcheck.php: 39 | * Added `/u` modifier to `preg_split()` regex pattern to properly handle words with accents - #1 (special thanks to tssk for this) 40 | 41 | ### Version 1.4 ### 42 | * Plugin is now wrapped in an IIFE 43 | * Regular expressions are now pre-compiled and cached for better performance 44 | * Made several IE-specific CSS fixes (for IE7-IE9) 45 | * Cleaned up CSS and removed a number of duplicate/redundant rules 46 | * Switched to a unique ID function that is RFC 4122 version 4 compliant 47 | * Any 2xx status code is now handled as a successful response (previously, only `200` was successful) 48 | * Added `"use strict";` to every function 49 | * Cleaned up some messy code -- organization, unnecessary variable copying, etc. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Javascript/PHP Spell Checker 2 | ============================ 3 | 4 | Word processor style spell check functionality for web applications. 5 | 6 | Live Demo: https://www.lpology.com/code/spellcheck/ 7 | 8 | ### Overview ### 9 | Javascript/PHP Spell Checker makes it easy to add an MS Word-style spell checker to any web application with almost no configuration. It's fast, lightweight, and works in all major browsers. 10 | 11 | ### Features ### 12 | 13 | * Designed to mimic the appearance and feel of desktop word processor spell checkers. 14 | * Provides a list of suggestions for misspelled words. 15 | * Add spell check to any ` 46 | 47 | ``` 48 | 49 | ### Installing Pspell ### 50 | You'll need to install aspell, a dictionary, and the php-pspell module if not already installed: 51 | 52 | ``` 53 | sudo yum install aspell aspell-en 54 | sudo yum install php-pspell 55 | ``` 56 | 57 | Then restart Apache: 58 | 59 | ``` 60 | sudo service httpd restart 61 | ``` 62 | 63 | ### API Reference ### 64 | 65 | #### Settings #### 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
NameTypeDescription
actionString
Default: ""
Location of spellcheck.php on the server.
buttonMixed
Default: ""
Button that opens spell checker. Accepts an element ID string, element, or jQuery object.
textInputMixed
Default: ""
Text input to spell check. Accepts an element ID string, element, or jQuery object.
nameString
Default: ""
Parameter name of text sent to server.
dataObject
Default: {}
Additional data to send to the server.
debugBoolean
Default: false
Set to true to log progress messages and server response in the console.
114 | 115 | #### Callback Functions #### 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 |
NameArgumentsDescription
onOpenbutton (Element),
text (String)
Function to be called when spell checker is opened, after successful server response.

The function gets passed two arguments: (1) a reference to the spell check button; (2) a string containing the text that is to be spell checked.
onClosebutton (Element),
text (String)
Function to be called after the spell checker is closed.

The function gets passed two arguments: (1) a reference to the spell check button; (2) a string containing the spell checked text, including any changes.
139 | 140 | #### Instance Methods #### 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
NameArgumentsDescription
destroynoneCompletely removes spell check functionality. All event listeners, CSS classes, and DOM elements added by the plugin are removed.
158 | 159 | ### License ### 160 | Released under the MIT license. 161 | 162 | [![githalytics.com alpha](https://cruel-carlota.pagodabox.com/c658e3ccc513c56cc253223a42274cb7 "githalytics.com")](http://githalytics.com/LPology/Javascript-PHP-Spell-Checker) -------------------------------------------------------------------------------- /css/spellcheck.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript/PHP Spell Checker 3 | * Version 1.6 4 | * https://github.com/LPology/Javascript-PHP-Spell-Checker 5 | * 6 | * Copyright 2012-2015 LPology, LLC 7 | * Released under the MIT license 8 | */ 9 | 10 | div.spell-wrap, 11 | div.spell-msg, 12 | div.spell-wrap div, 13 | div.spell-msg div, 14 | div.spell-wrap input, 15 | div.spell-wrap input[type="text"], 16 | div.spell-wrap hr { 17 | -webkit-box-sizing: content-box; 18 | -moz-box-sizing: content-box; 19 | box-sizing: content-box; 20 | } 21 | 22 | div.spell-wrap div { 23 | display: block; 24 | } 25 | 26 | .spellcheck-trigger:hover { 27 | cursor: pointer; 28 | } 29 | 30 | div.spell-wrap button, 31 | div.spell-wrap input[type="button"], 32 | div.spell-wrap input, 33 | div.spell-wrap input[type="text"], 34 | div.spell-wrap select { 35 | margin: 0; 36 | vertical-align: middle; 37 | display: inline-block; 38 | line-height: inherit; 39 | font-size: inherit; 40 | font-family: inherit; 41 | font-weight: inherit; 42 | *display: inline; 43 | *zoom: 1; 44 | } 45 | 46 | div.spell-wrap button::-moz-focus-inner, 47 | div.spell-wrap input::-moz-focus-inner { 48 | padding: 0; 49 | border: 0; 50 | } 51 | 52 | div.spell-wrap input, 53 | div.spell-wrap input[type="text"] { 54 | height: 18px; 55 | line-height: 18px; 56 | padding: 3px; 57 | color: #404040; 58 | border: 1px solid #bbbbbb; 59 | -webkit-border-radius: 1px; 60 | -moz-border-radius: 1px; 61 | border-radius: 1px; 62 | } 63 | 64 | div.spell-wrap button, 65 | div.spell-wrap input[type="button"] { 66 | line-height: 20px; 67 | cursor: pointer; 68 | width: 130px; 69 | -webkit-appearance: button; 70 | -moz-appearance: button; 71 | } 72 | 73 | div.spell-wrap div.clearleft { 74 | clear: both; 75 | float: left; 76 | width: 100%; 77 | margin-top: 8px; 78 | } 79 | 80 | div.spell-wrap span.word-highlight { 81 | color: #ff0000; 82 | } 83 | 84 | div.spell-wrap hr { 85 | clear: both; 86 | border: none; 87 | height: 1px; 88 | background: #cccccc; 89 | color: #cccccc; 90 | margin: 15px 0; 91 | float: left; 92 | width: 100%; 93 | } 94 | 95 | div.spell-check-overlay { 96 | display: none; 97 | position: fixed; 98 | top: 0; 99 | left: 0; 100 | width: 200%; 101 | height: 200%; 102 | background-color: #111; 103 | opacity: 0.22; 104 | filter: alpha(opacity=22); 105 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=22)"; 106 | z-index: 5000; 107 | } 108 | 109 | div.spell-header { 110 | clear: both; 111 | position: relative; 112 | padding: 3px; 113 | } 114 | 115 | div.spell-header > div { 116 | vertical-align:middle; 117 | padding: 8px 12px 6px; 118 | font-weight: bold; 119 | background-color: #F5F5F5; 120 | -webkit-box-shadow: inset 0 1px 0 #ffffff; 121 | -moz-box-shadow: inset 0 1px 0 #ffffff; 122 | box-shadow: inset 0 1px 0 #ffffff; 123 | -webkit-border-radius: 4px; /* Safari 4 */ 124 | -moz-border-radius: 4px; /* Firefox 3.6 */ 125 | border-radius: 4px; 126 | border: 0 0 0 1px solid; 127 | border-color: white; 128 | text-decoration: none; 129 | -webkit-border-bottom-right-radius: 0; /* Safari 4 */ 130 | -moz-border-radius-bottomright: 0; /* Firefox 3.6 */ 131 | border-bottom-right-radius: 0; 132 | -webkit-border-bottom-left-radius: 0; /* Safari 4 */ 133 | -moz-border-radius-bottomleft: 0; /* Firefox 3.6 */ 134 | border-bottom-left-radius: 0; 135 | border-bottom: 1px solid #ddd; 136 | } 137 | 138 | div.spell-msg div.spell-header { 139 | margin-bottom: 8px; 140 | text-align:left; 141 | } 142 | 143 | div.spell-wrap, 144 | div.spell-msg { 145 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 146 | background-color: #ffffff; 147 | display: none; 148 | position: fixed; 149 | color: #454545; 150 | font-size: 15px; 151 | font-weight: normal; 152 | font-weight: 200; 153 | top: 50%; 154 | left: 50%; 155 | height: auto; 156 | line-height: 23px; 157 | -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); /* Safari 4 */ 158 | -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); /* Firefox 3.6 */ 159 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 160 | } 161 | 162 | div.spell-wrap { 163 | padding: 0; 164 | width: 520px; 165 | margin-left: -260px; 166 | margin-top: -218px; 167 | z-index: 4999; 168 | } 169 | 170 | div.spell-wrap, 171 | div.spell-msg { 172 | border: 1px solid #cccccc; 173 | border: 1px solid rgba(0, 0, 0, 0.4); 174 | border-radius: 6px 6px 6px 6px; 175 | } 176 | 177 | div.spelling-inner { 178 | padding: 2px 15px 8px; 179 | } 180 | 181 | div.spell-wrap div.spell-header { 182 | font-size: 17px; 183 | line-height: 17px; 184 | } 185 | 186 | div.spell-wrap div.spell-header div { 187 | padding: 12px 15px 11px; 188 | } 189 | 190 | div.spell-wrap input.current { 191 | width: 100%; 192 | margin-bottom: 5px; 193 | } 194 | 195 | div.spell-wrap div.context { 196 | clear: both; 197 | float: left; 198 | color: #333; 199 | width: 100%; 200 | height: 70px; 201 | line-height: 20px; 202 | background-color: #fff; 203 | padding: 1px 3px; 204 | -webkit-border-radius: 1px; 205 | -moz-border-radius: 1px; 206 | border-radius: 1px; 207 | border: 1px solid; 208 | border-color: #bbbbbb #bbbbbb #bbbbbb #cccccc; 209 | -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05); 210 | -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05); 211 | box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05); 212 | overflow: auto; 213 | } 214 | 215 | div.spell-msg { 216 | width: 300px; 217 | min-height: 70px; 218 | margin-left: -150px; 219 | margin-top: -55px; 220 | padding-bottom: 8px; 221 | text-align: center; 222 | z-index: 5002; 223 | } 224 | 225 | div.close-box button { 226 | cursor: pointer; 227 | display: inline-block; 228 | padding: 4px 8px; 229 | width: auto; 230 | margin-bottom: 0; 231 | font-size: 14px; 232 | font-weight: normal; 233 | line-height: 1.428571429; 234 | text-align: center; 235 | white-space: nowrap; 236 | vertical-align: middle; 237 | cursor: pointer; 238 | border: 1px solid #ccc; 239 | -webkit-user-select: none; 240 | -moz-user-select: none; 241 | -ms-user-select: none; 242 | -o-user-select: none; 243 | user-select: none; 244 | color: #ffffff; 245 | background-color:#3276b1;border-color:#2c699d; 246 | -webkit-border-radius: 3px; 247 | -moz-border-radius: 3px; 248 | border-radius: 3px; 249 | } 250 | div.close-box button:hover, 251 | div.close-box button:focus, 252 | div.close-box button:active { 253 | color: #ffffff; 254 | background-color:#296191; 255 | border-color:#1f496d; 256 | text-decoration: none; 257 | } 258 | 259 | div.close-box button:focus { 260 | outline: thin dotted #333; 261 | outline: 5px auto -webkit-focus-ring-color; 262 | outline-offset: -2px; 263 | } 264 | 265 | div.spell-msg div.spell-msg-inner { 266 | margin: 0 auto; 267 | height: auto; 268 | clear: both; 269 | padding: 6px 0; 270 | text-align: center; 271 | } 272 | 273 | div.spell-msg div.spell-msg-inner button { 274 | margin: 0; 275 | width: 70px; 276 | margin-top: 2px; 277 | margin-bottom: 2px; 278 | } 279 | 280 | div.spell-wrap div.spell-suggest, 281 | div.spell-wrap div.spell-nf { 282 | clear: both; 283 | float: left; 284 | width: 328px; 285 | height: auto; 286 | } 287 | 288 | div.spell-wrap div.spell-nf { 289 | width: 320px; 290 | } 291 | 292 | div.spell-wrap div.spell-ignorebtns, 293 | div.spell-wrap div.spell-changebtns { 294 | float: right; 295 | height: auto; 296 | } 297 | 298 | div.spell-wrap div.spell-changebtns button, 299 | div.spell-wrap div.spell-ignorebtns button { 300 | display: block; 301 | margin-bottom: 8px; 302 | clear: both; 303 | } 304 | 305 | div.spell-wrap div.spell-changebtns button:last-child, 306 | div.spell-wrap div.spell-ignorebtns button:last-child { 307 | margin-bottom: 0; 308 | } 309 | 310 | div.spell-wrap div.spell-suggest select { 311 | background-color: #ffffff; 312 | border: 1px solid #bbbbbb; 313 | -webkit-border-radius: 3px; 314 | -moz-border-radius: 3px; 315 | border-radius: 3px; 316 | font-family: inherit; 317 | display: inline-block; 318 | font-size: inherit; 319 | line-height: inherit; 320 | font-weight: inherit; 321 | padding: 0; 322 | margin: 0; 323 | width: 100%; 324 | height: auto !important; 325 | *display: inline; 326 | *zoom: 1; 327 | } 328 | 329 | div.spell-wrap div.spell-suggest select option { 330 | font-size: inherit; 331 | line-height: inherit; 332 | } 333 | 334 | div.spell-wrap div.close-box { 335 | clear: both; 336 | width: 100%; 337 | text-align: right; 338 | padding-bottom: 6px; 339 | } 340 | 341 | div.spell-wrap, 342 | div.spell-wrap div, 343 | div.spell-msg, 344 | div.spell-msg div, 345 | div.spell-check-overlay { 346 | *zoom: 1; 347 | } -------------------------------------------------------------------------------- /css/spellcheck.min.css: -------------------------------------------------------------------------------- 1 | /** Javascript/PHP Spell Checker v1.6 https://github.com/LPology/Javascript-PHP-Spell-Checker Copyright 2012-2015 LPology, LLC Released under the MIT license */ 2 | div.spell-wrap,div.spell-msg,div.spell-wrap div,div.spell-msg div,div.spell-wrap input,div.spell-wrap input[type="text"],div.spell-wrap hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}div.spell-wrap div{display:block}.spellcheck-trigger:hover{cursor:pointer}div.spell-wrap button,div.spell-wrap input[type="button"],div.spell-wrap input,div.spell-wrap input[type="text"],div.spell-wrap select{margin:0;vertical-align:middle;display:inline-block;line-height:inherit;font-size:inherit;font-family:inherit;font-weight:inherit;*display:inline;*zoom:1}div.spell-wrap button::-moz-focus-inner,div.spell-wrap input::-moz-focus-inner{padding:0;border:0}div.spell-wrap input,div.spell-wrap input[type="text"]{height:18px;line-height:18px;padding:3px;color:#404040;border:1px solid #bbb;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px}div.spell-wrap button,div.spell-wrap input[type="button"]{line-height:20px;cursor:pointer;width:130px;-webkit-appearance:button;-moz-appearance:button}div.spell-wrap div.clearleft{clear:both;float:left;width:100%;margin-top:8px}div.spell-wrap span.word-highlight{color:red}div.spell-wrap hr{clear:both;border:0;height:1px;background:#ccc;color:#ccc;margin:15px 0;float:left;width:100%}div.spell-check-overlay{display:none;position:fixed;top:0;left:0;width:200%;height:200%;background-color:#111;opacity:.22;filter:alpha(opacity=22);-ms-filter:"alpha(opacity=22)";z-index:5000}div.spell-header{clear:both;position:relative;padding:3px}div.spell-header>div{vertical-align:middle;padding:8px 12px 6px;font-weight:bold;background-color:#f5f5f5;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0 0 0 1px solid;border-color:white;text-decoration:none;-webkit-border-bottom-right-radius:0;-moz-border-radius-bottomright:0;border-bottom-right-radius:0;-webkit-border-bottom-left-radius:0;-moz-border-radius-bottomleft:0;border-bottom-left-radius:0;border-bottom:1px solid #ddd}div.spell-msg div.spell-header{margin-bottom:8px;text-align:left}div.spell-wrap,div.spell-msg{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;background-color:#fff;display:none;position:fixed;color:#454545;font-size:15px;font-weight:normal;font-weight:200;top:50%;left:50%;height:auto;line-height:23px;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3)}div.spell-wrap{padding:0;width:520px;margin-left:-260px;margin-top:-218px;z-index:4999}div.spell-wrap,div.spell-msg{border:1px solid #ccc;border:1px solid rgba(0,0,0,0.4);border-radius:6px 6px 6px 6px}div.spelling-inner{padding:2px 15px 8px}div.spell-wrap div.spell-header{font-size:17px;line-height:17px}div.spell-wrap div.spell-header div{padding:12px 15px 11px}div.spell-wrap input.current{width:100%;margin-bottom:5px}div.spell-wrap div.context{clear:both;float:left;color:#333;width:100%;height:70px;line-height:20px;background-color:#fff;padding:1px 3px;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;border:1px solid;border-color:#bbb #bbb #bbb #ccc;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);overflow:auto}div.spell-msg{width:300px;min-height:70px;margin-left:-150px;margin-top:-55px;padding-bottom:8px;text-align:center;z-index:5002}div.close-box button{cursor:pointer;display:inline-block;padding:4px 8px;width:auto;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;border:1px solid #ccc;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;color:#fff;background-color:#3276b1;border-color:#2c699d;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}div.close-box button:hover,div.close-box button:focus,div.close-box button:active{color:#fff;background-color:#296191;border-color:#1f496d;text-decoration:none}div.close-box button:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}div.spell-msg div.spell-msg-inner{margin:0 auto;height:auto;clear:both;padding:6px 0;text-align:center}div.spell-msg div.spell-msg-inner button{margin:0;width:70px;margin-top:2px;margin-bottom:2px}div.spell-wrap div.spell-suggest,div.spell-wrap div.spell-nf{clear:both;float:left;width:328px;height:auto}div.spell-wrap div.spell-nf{width:320px}div.spell-wrap div.spell-ignorebtns,div.spell-wrap div.spell-changebtns{float:right;height:auto}div.spell-wrap div.spell-changebtns button,div.spell-wrap div.spell-ignorebtns button{display:block;margin-bottom:8px;clear:both}div.spell-wrap div.spell-changebtns button:last-child,div.spell-wrap div.spell-ignorebtns button:last-child{margin-bottom:0}div.spell-wrap div.spell-suggest select{background-color:#fff;border:1px solid #bbb;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;font-family:inherit;display:inline-block;font-size:inherit;line-height:inherit;font-weight:inherit;padding:0;margin:0;width:100%;height:auto !important;*display:inline;*zoom:1}div.spell-wrap div.spell-suggest select option{font-size:inherit;line-height:inherit}div.spell-wrap div.close-box{clear:both;width:100%;text-align:right;padding-bottom:6px}div.spell-wrap,div.spell-wrap div,div.spell-msg,div.spell-msg div,div.spell-check-overlay{*zoom:1} 3 | -------------------------------------------------------------------------------- /js/spellcheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Javascript/PHP Spell Checker 3 | * Version 1.6.1 4 | * https://github.com/LPology/Javascript-PHP-Spell-Checker 5 | * 6 | * Copyright 2012-2015 LPology, LLC 7 | * Released under the MIT license 8 | */ 9 | 10 | ;(function( window, document, undefined ) { 11 | 12 | var sc = window.sc || {}, 13 | 14 | // Pre-compile and cache our regular expressions 15 | rLWhitespace = /^\s+/, 16 | rTWhitespace = /\s+$/, 17 | rLNonWhitespace = /[^\s]+/, 18 | rRNonWhitespace = /[^\s]+$/, 19 | 20 | // sc.getUID 21 | uidReplace = /[xy]/g, 22 | 23 | //sc.encodeHTML() 24 | rAmp = /&/g, 25 | rQuot = /"/g, 26 | rQuot2 = /'/g, 27 | rLt = //g, 29 | 30 | rAlphaNum = /^\w+$/, 31 | 32 | // Holds cached regular expressions for _getRegex() 33 | regexCache = {}, 34 | 35 | _ = function( elem ) { 36 | return document.getElementById( elem ); 37 | }; 38 | 39 | /** 40 | * Accepts an object and returns an array of its property names 41 | */ 42 | sc.objectKeys = function( obj ) { 43 | "use strict"; 44 | 45 | var keys = []; 46 | for ( var prop in obj ) { 47 | if ( obj.hasOwnProperty( prop ) ) { 48 | keys.push( prop ); 49 | } 50 | } 51 | return keys; 52 | }; 53 | 54 | /** 55 | * Converts object to query string 56 | */ 57 | sc.obj2string = function( obj, prefix ) { 58 | "use strict"; 59 | 60 | var str = []; 61 | 62 | for ( var prop in obj ) { 63 | if ( obj.hasOwnProperty( prop ) ) { 64 | var k = prefix ? prefix + '[' + prop + ']' : prop, v = obj[prop]; 65 | str.push( typeof v === 'object' ? 66 | sc.obj2string( v, k ) : 67 | encodeURIComponent( k ) + '=' + encodeURIComponent( v ) ); 68 | } 69 | } 70 | 71 | return str.join( '&' ); 72 | }; 73 | 74 | /** 75 | * Copies all missing properties from second object to first object 76 | */ 77 | sc.extendObj = function( first, second ) { 78 | "use strict"; 79 | 80 | for ( var prop in second ) { 81 | if ( second.hasOwnProperty( prop ) ) { 82 | first[prop] = second[prop]; 83 | } 84 | } 85 | }; 86 | 87 | /** 88 | * Returns true if an object has no properties of its own 89 | */ 90 | sc.isEmpty = function( obj ) { 91 | "use strict"; 92 | 93 | for ( var prop in obj ) { 94 | if ( obj.hasOwnProperty( prop ) ) { 95 | return false; 96 | } 97 | } 98 | return true; 99 | }; 100 | 101 | sc.contains = function( array, item ) { 102 | "use strict"; 103 | 104 | var i = array.length; 105 | while ( i-- ) { 106 | if ( array[i] === item ) { 107 | return true; 108 | } 109 | } 110 | return false; 111 | }; 112 | 113 | /** 114 | * Nulls out event handlers to prevent memory leaks in IE6/IE7 115 | * http://javascript.crockford.com/memory/leak.html 116 | * @param {Element} d 117 | * @return void 118 | */ 119 | sc.purge = function( d ) { 120 | "use strict"; 121 | 122 | var a = d.attributes, i, l, n; 123 | 124 | if ( a ) { 125 | for ( i = a.length - 1; i >= 0; i -= 1 ) { 126 | n = a[i].name; 127 | 128 | if ( typeof d[n] === 'function' ) { 129 | d[n] = null; 130 | } 131 | } 132 | } 133 | 134 | a = d.childNodes; 135 | 136 | if ( a ) { 137 | l = a.length; 138 | for ( i = 0; i < l; i += 1 ) { 139 | sc.purge( d.childNodes[i] ); 140 | } 141 | } 142 | }; 143 | 144 | /** 145 | * Removes element from the DOM 146 | */ 147 | sc.remove = function( elem ) { 148 | "use strict"; 149 | 150 | if ( elem && elem.parentNode ) { 151 | // null out event handlers for IE 152 | sc.purge( elem ); 153 | elem.parentNode.removeChild( elem ); 154 | } 155 | elem = null; 156 | }; 157 | 158 | /** 159 | * Removes whtie space from left and right of string 160 | */ 161 | var trim = "".trim; 162 | 163 | sc.trim = trim && !trim.call("\uFEFF\xA0") ? 164 | function( text ) { 165 | return text === null ? 166 | "" : 167 | trim.call( text ); 168 | } : 169 | function( text ) { 170 | return text === null ? 171 | "" : 172 | text.toString().replace( rLWhitespace, '' ).replace( rTWhitespace, '' ); 173 | }; 174 | 175 | /** 176 | * Generates unique ID 177 | * Complies with RFC 4122 version 4 178 | * http://stackoverflow.com/a/2117523/1091949 179 | */ 180 | sc.getId = function() { 181 | "use strict"; 182 | 183 | /*jslint bitwise: true*/ 184 | return 'axxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(uidReplace, function(c) { 185 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 186 | return v.toString(16); 187 | }); 188 | }; 189 | 190 | sc.addEvent = function( elem, type, fn ) { 191 | "use strict"; 192 | 193 | if ( typeof elem === 'string' ) { 194 | elem = document.getElementById( elem ); 195 | } 196 | 197 | if ( elem.addEventListener ) { 198 | elem.addEventListener( type, fn, false ); 199 | 200 | } else { 201 | elem.attachEvent( 'on' + type, fn ); 202 | } 203 | 204 | return function() { 205 | sc.removeEvent( elem, type, fn ); 206 | }; 207 | }; 208 | 209 | sc.removeEvent = function(elem, type, fn) { 210 | "use strict"; 211 | 212 | if (typeof elem === 'string') { 213 | elem = document.getElementById( elem ); 214 | } 215 | 216 | if ( elem.removeEventListener ) { 217 | elem.removeEventListener( type, fn, false ); 218 | 219 | } else { 220 | elem.detachEvent( 'on' + type, fn ); 221 | } 222 | }; 223 | 224 | sc.newXHR = function() { 225 | "use strict"; 226 | 227 | if ( typeof XMLHttpRequest !== 'undefined' ) { 228 | return new window.XMLHttpRequest(); 229 | 230 | } else if ( window.ActiveXObject ) { 231 | try { 232 | return new window.ActiveXObject( 'Microsoft.XMLHTTP' ); 233 | } catch ( err ) { 234 | return false; 235 | } 236 | } 237 | }; 238 | 239 | sc.encodeHTML = function( str ) { 240 | "use strict"; 241 | 242 | return String( str ) 243 | .replace( rAmp, '&' ) 244 | .replace( rQuot, '"' ) 245 | .replace( rQuot2, ''' ) 246 | .replace( rLt, '<' ) 247 | .replace( rGt, '>' ); 248 | }; 249 | 250 | /** 251 | * Parses a JSON string and returns a Javascript object 252 | * Parts borrowed from www.jquery.com 253 | */ 254 | sc.parseJSON = function( data ) { 255 | "use strict"; 256 | 257 | if ( !data ) { 258 | return false; 259 | } 260 | 261 | data = sc.trim( data + '' ); 262 | 263 | if ( window.JSON && window.JSON.parse ) { 264 | try { 265 | // Support: Android 2.3 266 | // Workaround failure to string-cast null input 267 | return window.JSON.parse( data + '' ); 268 | } catch ( err ) { 269 | return false; 270 | } 271 | } 272 | 273 | var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g, 274 | depth = null, 275 | requireNonComma; 276 | 277 | // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains 278 | // after removing valid tokens 279 | return data && !sc.trim( data.replace( rvalidtokens, function( token, comma, open, close ) { 280 | 281 | // Force termination if we see a misplaced comma 282 | if ( requireNonComma && comma ) { 283 | depth = 0; 284 | } 285 | 286 | // Perform no more replacements after returning to outermost depth 287 | if ( depth === 0 ) { 288 | return token; 289 | } 290 | 291 | // Commas must not follow "[", "{", or "," 292 | requireNonComma = open || comma; 293 | 294 | // Determine new depth 295 | // array/object open ("[" or "{"): depth += true - false (increment) 296 | // array/object close ("]" or "}"): depth += false - true (decrement) 297 | // other cases ("," or primitive): depth += true - true (numeric cast) 298 | depth += !close - !open; 299 | 300 | // Remove this token 301 | return ''; 302 | }) ) ? 303 | ( Function( 'return ' + data ) )() : 304 | false; 305 | }; 306 | 307 | /** 308 | * Accepts a jquery object, a string containing an element ID, or an element, 309 | * verifies that it exists, and returns the element. 310 | * @param {Mixed} elem 311 | * @return {Element} 312 | */ 313 | sc.verifyElem = function( elem ) { 314 | "use strict"; 315 | 316 | if ( typeof jQuery !== 'undefined' && elem instanceof jQuery ) { 317 | elem = elem[0]; 318 | 319 | } else if ( typeof elem === 'string' ) { 320 | if ( elem.charAt( 0 ) == '#' ) { 321 | elem = elem.substr( 1 ); 322 | } 323 | elem = document.getElementById( elem ); 324 | } 325 | 326 | if ( !elem || elem.nodeType !== 1 ) { 327 | return false; 328 | } 329 | 330 | if ( elem.nodeName.toUpperCase() == 'A' ) { 331 | elem.style.cursor = 'pointer'; 332 | 333 | sc.addEvent( elem, 'click', function( e ) { 334 | if ( e && e.preventDefault ) { 335 | e.preventDefault(); 336 | 337 | } else if ( window.event ) { 338 | window.event.returnValue = false; 339 | } 340 | }); 341 | } 342 | 343 | return elem; 344 | }; 345 | 346 | 347 | /** 348 | * @constructor 349 | * @param {Object} options 350 | */ 351 | sc.SpellChecker = function( options ) { 352 | 353 | var self = this; 354 | 355 | this._settings = { 356 | action: '', // URL of server script 357 | button: '', // Button that opens spell checker 358 | textInput: '', // Text input to spell check 359 | name: 'text', // Parameter name of text sent to server 360 | data: {}, // Additional data to send to the server (optional) 361 | debug: false, 362 | onOpen: function( button, text ) {}, // Callback to be executed when spell checker is opened 363 | onClose: function( button, text ) {} // Callback to be executed after spell checker is closed 364 | }; 365 | 366 | sc.extendObj( this._settings, options ); 367 | 368 | this._button = sc.verifyElem( this._settings.button ); 369 | this._textInput = sc.verifyElem( this._settings.textInput ); 370 | 371 | delete this._settings.button; 372 | 373 | if ( this._button === false ) { 374 | throw new Error( "Invalid button. Make sure the element you're passing exists." ); 375 | } 376 | 377 | if ( this._textInput === false ) { 378 | throw new Error( "Invalid text field. Make sure the element you're passing exists." ); 379 | } 380 | 381 | this._closeOnEsc = function( event ) { 382 | if ( event.keyCode === 27 ) { 383 | self._closeChecker(); 384 | } 385 | }; 386 | 387 | this._uId = sc.getId(); 388 | this._createHTML(); 389 | 390 | this._currentBox = _( 'spell-current' + this._uId ); 391 | this._contextBox = _( 'spell-context' + this._uId ); 392 | this._undoBtn = _( 'spell-undo' + this._uId ); 393 | this._select = _( 'spelling-suggestions' + this._uId ); 394 | 395 | this._isOpen = false; 396 | 397 | // Add CSS class to button for pointer cursor when hovering 398 | this._button.className += ' spellcheck-trigger'; 399 | 400 | this.enable(); 401 | }; 402 | 403 | sc.SpellChecker.prototype = { 404 | 405 | enable: function() { 406 | "use strict"; 407 | 408 | var self = this; 409 | 410 | this._button.off = sc.addEvent( this._button, 'click', function() { 411 | self._openChecker(); 412 | }); 413 | 414 | sc.addEvent( 'spelling-ignore' + this._uId, 'click', function() { 415 | self._ignore(); 416 | }); 417 | 418 | sc.addEvent( 'spelling-ignore-all' + this._uId, 'click', function() { 419 | self._ignore( true ); 420 | }); 421 | 422 | sc.addEvent( 'spell-change' + this._uId, 'click', function() { 423 | self._makeChange(); 424 | }); 425 | 426 | sc.addEvent( 'spell-change-all' + this._uId, 'click', function() { 427 | self._makeChange( true ); 428 | }); 429 | 430 | sc.addEvent( 'spell-close' + this._uId, 'click', function() { 431 | self._closeChecker(); 432 | }); 433 | 434 | sc.addEvent( 'spell-msg-close' + this._uId, 'click', function() { 435 | self._closeChecker(); 436 | }); 437 | 438 | sc.addEvent( 'spell-check-overlay' + this._uId, 'click', function() { 439 | self._closeChecker(); 440 | }); 441 | 442 | sc.addEvent( this._undoBtn, 'click', function() { 443 | self._undoChange(); 444 | }); 445 | 446 | // Unselect any suggestion if user clicks either input so that word is 447 | // changed to correct spelling 448 | sc.addEvent( this._currentBox, 'click', function() { 449 | _( 'spelling-suggestions' + self._uId ).selectedIndex = -1; 450 | }); 451 | 452 | sc.addEvent( this._contextBox, 'click', function() { 453 | _( 'spelling-suggestions' + self._uId ).selectedIndex = -1; 454 | }); 455 | 456 | // Change "Not found in dictionary" if user edits within word context box 457 | sc.addEvent( this._currentBox, 'keyup', function() { 458 | var span = this._contextBox.getElementsByTagName( 'span' )[0]; 459 | if ( span && span.firstChild ) { 460 | span.firstChild.nodeValue = this.value; 461 | } 462 | }); 463 | 464 | // Change word context also if user edits "Not found in dictionary" box 465 | sc.addEvent( this._contextBox, 'keyup', function() { 466 | var span = this.getElementsByTagName( 'span' )[0]; 467 | if ( span && span.firstChild ) { 468 | this._currentBox.value = span.firstChild.nodeValue; 469 | } 470 | }); 471 | }, 472 | 473 | /** 474 | * Completely removes spell check functionality 475 | */ 476 | destroy: function() { 477 | "use strict"; 478 | 479 | // Close the checker if it's open 480 | if ( this._isOpen ) { 481 | this._closeChecker(); 482 | } 483 | 484 | // Remove event listener from button 485 | if ( this._button.off ) { 486 | this._button.off(); 487 | } 488 | 489 | // Remove .spellcheck-trigger CSS class from button 490 | this._button.className = this._button.className.replace( /(?:^|\s)spellcheck-trigger(?!\S)/ , '' ); 491 | 492 | // Remove all of the HTML we created 493 | sc.remove( this._msgBox ); 494 | sc.remove( this._modal ); 495 | sc.remove( this._overlay ); 496 | 497 | // Now burn it all down 498 | for ( var prop in this ) { 499 | if ( this.hasOwnProperty( prop ) ) { 500 | delete this.prop; 501 | } 502 | } 503 | }, 504 | 505 | /** 506 | * Send data to browser console if debug is set to true 507 | */ 508 | log: function( str ) { 509 | "use strict"; 510 | 511 | if ( this._settings.debug && window.console ) { 512 | window.console.log( '[spell checker] ' + str ); 513 | } 514 | }, 515 | 516 | _getRegex: function( word ) { 517 | "use strict"; 518 | 519 | if ( !regexCache[word] ) { 520 | regexCache[word] = new RegExp( word, 'g' ); 521 | } 522 | return regexCache[word]; 523 | }, 524 | 525 | /** 526 | * Begins the spell check function. 527 | */ 528 | _openChecker: function() { 529 | "use strict"; 530 | 531 | if ( this._isOpen ) { 532 | return; 533 | } 534 | 535 | this._undoBtn.disabled = true; 536 | this._overlay.style.display = 'block'; 537 | this._modal.style.display = 'block'; 538 | 539 | // Get the text that we're going to spell check 540 | this._text = this._textInput.value; 541 | this._isOpen = true; 542 | 543 | // Array of objects containing change history for "Undo" 544 | this._undo = []; 545 | 546 | // Add listener for escape key to close checker 547 | sc.addEvent( document, 'keyup', this._closeOnEsc ); 548 | 549 | // Show "Checking..." message 550 | this._notifyMsg( 'a' ); 551 | 552 | // Send the text to the server 553 | this._sendData(); 554 | }, 555 | 556 | /** 557 | * Closes the spell check box and cleans up. 558 | */ 559 | _closeChecker: function() { 560 | "use strict"; 561 | 562 | if ( !this._isOpen ) { 563 | return; 564 | } 565 | 566 | // Close all dialog boxes 567 | this._msgBox.style.display = 'none'; 568 | this._modal.style.cssText = 'display:none; z-index:4999;'; 569 | this._overlay.style.display = 'none'; 570 | this._currentBox.value = ''; 571 | this._contextBox.innerHTML = ''; 572 | this._select.options.length = 0; 573 | this._undo.length = 0; 574 | 575 | // Reset everything after finishing 576 | this._text = this._wordObject = this._wordKeys = this._currentWord = this._wordMatches = this._matchOffset = this._undo = this._isOpen = null; 577 | 578 | // Removes listener for escape key 579 | sc.removeEvent( document, 'keyup', this._closeOnEsc ); 580 | 581 | this._settings.onClose.call( this, this._button, this._textInput.value ); 582 | }, 583 | 584 | /** 585 | * Provides user with status messages. 586 | */ 587 | _notifyMsg: function( type ) { 588 | "use strict"; 589 | 590 | var msg, 591 | closeBox = _( 'spell-msg-close-box' + this._uId ); 592 | 593 | if ( type == 'a' ) { 594 | msg = 'Checking...'; 595 | closeBox.style.display = 'none'; 596 | } else { 597 | closeBox.style.display = 'block'; 598 | } 599 | 600 | if ( type == 'b' ) { 601 | msg = 'We experienced an error and were unable to complete the spell check.'; 602 | } 603 | 604 | if ( type == 'c' ) { 605 | msg = 'Spell check completed. No errors found.'; 606 | } 607 | 608 | if ( type == 'd' ) { 609 | msg = 'Spell check completed.'; 610 | } 611 | 612 | // Put the spell check box behind the message box 613 | this._modal.style.zIndex = 4999; 614 | 615 | // Inject the correct message 616 | _( 'spell-msg-text' + this._uId ).innerHTML = msg; 617 | 618 | // Make the message box visible 619 | this._msgBox.style.display = 'block'; 620 | 621 | // Focus on "OK" button if anything but "Checking..." message 622 | if ( type != 'a' ) { 623 | _( 'spell-msg-close' + this._uId ).focus(); 624 | } 625 | }, 626 | 627 | /** 628 | * Ignores the potentially misspelled word currently in review 629 | */ 630 | _ignore: function( ignoreAll ) { 631 | "use strict"; 632 | 633 | var moreMatches; 634 | 635 | if ( ignoreAll === true || 636 | this._wordMatches <= 1 || 637 | this._matchOffset === this._wordMatches ) 638 | { 639 | this._wordKeys.splice( 0, 1 ); 640 | this._matchOffset = 1; // Reset to 1 in case there is another word to review 641 | moreMatches = false; 642 | } else { 643 | // Increment the match counter because we're using the same word next round 644 | // This prevents us from reviewing the same occurrence of this word 645 | this._matchOffset++; 646 | moreMatches = true; // There are remaining duplicates of this word to review 647 | } 648 | 649 | // Disable "Undo" in case the prior action was a change 650 | this._undoBtn.disabled = true; 651 | 652 | // Empty the change history array to help keep it under control 653 | this._undo.length = 0; 654 | 655 | if ( this._wordKeys.length > 0 || moreMatches === true ) { 656 | // Continue working if that wasn't the last word 657 | this._reviewWord(); 658 | } else { 659 | this._notifyMsg( 'd' ); 660 | } 661 | }, 662 | 663 | /** 664 | * Changes the misspelled word currently in review 665 | */ 666 | _makeChange: function( changeAll ) { 667 | "use strict"; 668 | 669 | var self = this, 670 | regex = this._getRegex( this._currentWord ), 671 | selected_option = this._select.selectedIndex, 672 | m = 0, 673 | new_word, 674 | new_text, 675 | moreMatches; 676 | 677 | // Save the current state before we change anything 678 | this._undo.unshift({ 679 | text: this._text, 680 | word: this._currentWord, 681 | numMatches: this._wordMatches, 682 | matchOffset: this._matchOffset 683 | }); 684 | 685 | // Enable the "Undo" button 686 | this._undoBtn.disabled = false; 687 | 688 | if ( selected_option > -1 ) { 689 | new_word = this._select.options[selected_option].text; // Use suggestion if one is selected 690 | } else { 691 | new_word = this._currentBox.value; 692 | } 693 | 694 | // Replace misspelled word with new word 695 | new_text = this._text.replace( regex, function( match ) { 696 | m++; 697 | 698 | // Replace if we've landed on the right occurrence or it's "Change All" 699 | if ( changeAll === true || self._matchOffset === m ) { 700 | return new_word; 701 | } 702 | 703 | // Otherwise don't change this occurrence 704 | return match; 705 | }); 706 | 707 | // Only remove the replaced word if we won't need it again 708 | if ( changeAll === true || 709 | self._wordMatches <= 1 || 710 | self._matchOffset === self._wordMatches ) 711 | { 712 | // Remove word from our list b/c we're finished with it 713 | this._wordKeys.splice( 0, 1 ); 714 | 715 | // Reset to 1 in case there is another word to review 716 | this._matchOffset = 1; 717 | 718 | // No remaining duplicates of this word 719 | moreMatches = false; 720 | 721 | // There are remaining duplicates of this word to review 722 | } else { 723 | moreMatches = true; 724 | } 725 | 726 | // Update text with new version 727 | this._textInput.value = this._text = new_text; 728 | 729 | // Keep going if there are more words to review 730 | if ( this._wordKeys.length > 0 || moreMatches === true ) { 731 | this._reviewWord(); 732 | 733 | // Otherwise do "Spell check completed" 734 | } else { 735 | this._notifyMsg( 'd' ); 736 | } 737 | }, 738 | 739 | /** 740 | * Undo the previous change action 741 | */ 742 | _undoChange: function() { 743 | "use strict"; 744 | 745 | var prevData = this._undo[0]; 746 | 747 | // Restore text to pre-change state 748 | this._textInput.value = this._text = prevData.text; 749 | 750 | // Return previous word to the "Not found in dictionary" field 751 | this._currentBox.value = prevData.word; 752 | 753 | // Add previous word back to beginning of array if it was removed 754 | if ( !sc.contains( this._wordKeys, prevData.word ) ) { 755 | this._wordKeys.unshift( prevData.word ); 756 | } 757 | 758 | // Restore variables to their value prior to change 759 | this._currentWord = prevData.word; 760 | this._wordMatches = prevData.numMatches; 761 | this._matchOffset = prevData.matchOffset; 762 | 763 | // Populate suggestion box with options 764 | this._setSuggestionOptions(); 765 | 766 | // Reset the word context box 767 | this._setContextBox(); 768 | 769 | // Remove from change history array 770 | this._undo.splice( 0, 1 ); 771 | 772 | // Disable "Undo" button if no more changes to undo 773 | if ( this._undo.length < 1 ) { 774 | this._undoBtn.disabled = true; 775 | } 776 | }, 777 | 778 | /** 779 | * Populates the spelling suggestions select box with options 780 | */ 781 | _setSuggestionOptions: function() { 782 | "use strict"; 783 | 784 | var suggestions = this._wordObject[this._currentWord], 785 | num = suggestions.length, 786 | i; 787 | 788 | // Clear out any existing options 789 | this._select.options.length = 0; 790 | 791 | for ( i = 0; i < num; i++ ) { 792 | this._select.options[i] = new Option( suggestions[i], suggestions[i] ); 793 | } 794 | 795 | // Select the first suggestion option 796 | this._select.selectedIndex = 0; 797 | }, 798 | 799 | /** 800 | * Places the misspelled word in the review box along with surrounding words for context 801 | */ 802 | _setContextBox: function() { 803 | "use strict"; 804 | 805 | var self = this, 806 | wordLength = this._currentWord.length, 807 | regex = this._getRegex( this._currentWord ), 808 | textLength = this._text.length, 809 | i = 0; 810 | 811 | this._text.replace( regex, function( match, index ) { 812 | // Prevents false matches for substring of a word. Ex: 'pre' matching 'previous' 813 | // Text is split by alphanumeric chars, so if the next char is alphanumeric, it's a false match 814 | if ( rAlphaNum.test( self._text.substr( index + wordLength, 1 ) ) ) { 815 | return match; 816 | } 817 | 818 | i++; 819 | 820 | if ( i === self._matchOffset ) { 821 | var firstHalf, 822 | secondHalf, 823 | startFirstHalf = index - 20, 824 | startSecondHalf = index + wordLength; 825 | 826 | if ( startFirstHalf < 0 ) { 827 | firstHalf = self._text.substr( 0, index ); 828 | } else { 829 | firstHalf = self._text.substr( startFirstHalf, 20 ); 830 | } 831 | 832 | if ( startSecondHalf + 50 > textLength ) { 833 | secondHalf = self._text.substr( startSecondHalf ); 834 | } else { 835 | secondHalf = self._text.substr( startSecondHalf, 50 ); 836 | } 837 | 838 | // This prevents broken words from going into the sentence context box by 839 | // trimming whitespace, trimming non-white space, then trimming white space again. 840 | firstHalf = firstHalf.replace( rLWhitespace, '' ) 841 | .replace( rLNonWhitespace, '' ) 842 | .replace( rLWhitespace, '' ); 843 | 844 | secondHalf = secondHalf.replace( rTWhitespace, '' ) 845 | .replace( rRNonWhitespace, '' ) 846 | .replace( rTWhitespace, '' ); 847 | 848 | self._contextBox.innerHTML = sc.encodeHTML( firstHalf ) + 849 | '' + 850 | sc.encodeHTML( self._currentWord ) + 851 | '' + 852 | sc.encodeHTML( secondHalf ); 853 | } 854 | 855 | return match; 856 | }); 857 | }, 858 | 859 | /** 860 | * Begin resolving a potentially misspelled word 861 | * 862 | * Executes at beginning of spell check if the server reports spelling errors or 863 | * after resolving the last word and moving to the next. 864 | */ 865 | _reviewWord: function() { 866 | "use strict"; 867 | 868 | // The misspelled word currently being reviewed 869 | // (always the first element of the keys array) 870 | this._currentWord = this._wordKeys[0]; 871 | 872 | this._currentBox.value = this._currentWord; 873 | 874 | // Find how many occurrences of the misspelled word so each one is reviewed 875 | this._wordMatches = this._getTotalWordMatches(); 876 | 877 | // Populate select field with spelling suggestion options 878 | this._setSuggestionOptions(); 879 | 880 | // Place misspelled word in review box with leading and trailing words for context 881 | this._setContextBox(); 882 | }, 883 | 884 | /** 885 | * Counts number of occurrences of the misspelled word so each will be reviewed 886 | */ 887 | _getTotalWordMatches: function() { 888 | "use strict"; 889 | 890 | var regex = this._getRegex( this._currentWord ), 891 | wordLength = this._currentWord.length, 892 | matches = 0, 893 | text = this._text; 894 | 895 | // Search through text for each occurrence of the misspelled word 896 | // Only count matches where next character is NOT alphanumeric 897 | // Prevents false matches for substring of a word. Ex: 'pre' matching 'previous' 898 | this._text.replace( regex, function( match, index ) { 899 | if ( !rAlphaNum.test( text.substr( index + wordLength, 1 ) ) ) { 900 | matches++; 901 | } 902 | return match; 903 | }); 904 | 905 | return matches; 906 | }, 907 | 908 | /** 909 | * Begins spell check process after data has been received from server 910 | */ 911 | _begin: function( response ) { 912 | "use strict"; 913 | 914 | if ( response.success && response.success === true ) { 915 | 916 | // Open the review box if there were spelling errors found 917 | if ( response.errors && response.errors === true ) { 918 | this._wordObject = response.words; 919 | this._wordKeys = sc.objectKeys( this._wordObject ); 920 | this._matchOffset = 1; 921 | 922 | this._msgBox.style.display = 'none'; 923 | this._modal.style.zIndex = 5001; 924 | this._reviewWord(); 925 | 926 | // Otherwise do "Spell check completed. No errors found." message 927 | } else { 928 | this._notifyMsg( 'c' ); 929 | } 930 | } 931 | }, 932 | 933 | /** 934 | * Sends text to the server for spell review 935 | */ 936 | _sendData: function() { 937 | "use strict"; 938 | 939 | var self = this, 940 | xhr = sc.newXHR(), 941 | data, 942 | callback; 943 | 944 | // Don't waste a server request for less than 2 characters 945 | if ( this._text.length < 2 ) { 946 | // Do "Spell check completed. No errors found" message 947 | this._notifyMsg( 'c' ); 948 | return; 949 | } 950 | 951 | data = encodeURIComponent( this._settings.name ) + '='; 952 | data += encodeURIComponent( this._text ); 953 | 954 | // Add any additional data 955 | if ( !sc.isEmpty( this._settings.data ) ) { 956 | data += '&'; 957 | data += sc.obj2string( this._settings.data ); 958 | } 959 | 960 | callback = function() { 961 | var response, 962 | status, 963 | statusText; 964 | 965 | try { 966 | if ( callback && xhr.readyState === 4 ) { 967 | callback = undefined; 968 | xhr.onreadystatechange = function() {}; 969 | 970 | // Only continue if the spell checker is open. This way, closing the checker 971 | // before the request is completed effectively aborts the request 972 | if ( !self._isOpen ) { 973 | return; 974 | } 975 | 976 | status = xhr.status; 977 | 978 | try { 979 | statusText = xhr.statusText; 980 | } catch( e ) { 981 | statusText = ''; 982 | } 983 | 984 | self.log( 'Request completed. Status: ' + status + ' ' + statusText ); 985 | 986 | if ( status >= 200 && status < 300 ) { 987 | response = sc.parseJSON( xhr.responseText ); 988 | 989 | if ( response !== false ) { 990 | self._settings.onOpen.call( self, self._button, self._text ); 991 | self._begin( response ); 992 | 993 | // There was an error parsing the server response 994 | } else { 995 | self.log( 'Error parsing server response' ); 996 | self._notifyMsg( 'b' ); 997 | } 998 | 999 | xhr = response = null; 1000 | 1001 | // We didn't get a 2xx status 1002 | } else { 1003 | self._notifyMsg( 'b' ); 1004 | } 1005 | 1006 | } 1007 | 1008 | } catch( e ) { 1009 | self.log( 'Error: ' + e.message ); 1010 | self._notifyMsg( 'b' ); 1011 | } 1012 | }; 1013 | 1014 | xhr.onreadystatechange = callback; 1015 | xhr.open( 'POST', this._settings.action, true ); 1016 | xhr.setRequestHeader( 'Accept', 'application/json, text/javascript, */*; q=0.01' ); 1017 | xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' ); 1018 | xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' ); 1019 | self.log( 'Sending data...' ); 1020 | xhr.send( data ); 1021 | }, 1022 | 1023 | /** 1024 | * Creates HTML for spell checker 1025 | */ 1026 | _createHTML: function() { 1027 | this._overlay = document.createElement( 'div' ); 1028 | this._modal = document.createElement( 'div' ); 1029 | this._msgBox = document.createElement( 'div' ); 1030 | 1031 | // Screen overlay 1032 | this._overlay.className = 'spell-check-overlay'; 1033 | this._overlay.id = 'spell-check-overlay' + this._uId; 1034 | document.body.appendChild( this._overlay ); 1035 | 1036 | // Spell check box 1037 | this._modal.className = 'spell-wrap'; 1038 | this._modal.innerHTML = '
Spell Check
Not found in dictionary:
Suggestions:

'; 1039 | document.body.appendChild( this._modal ); 1040 | 1041 | // Popup message box 1042 | this._msgBox.className = 'spell-msg'; 1043 | this._msgBox.innerHTML = '
Spell Check
'; 1044 | document.body.appendChild( this._msgBox ); 1045 | } 1046 | }; 1047 | 1048 | // Expose to the global window object 1049 | window.sc = sc; 1050 | 1051 | })( window, document ); -------------------------------------------------------------------------------- /js/spellcheck.min.js: -------------------------------------------------------------------------------- 1 | /** Javascript/PHP Spell Checker v1.6.1 https://github.com/LPology/Javascript-PHP-Spell-Checker Copyright 2012-2015 LPology, LLC Released under the MIT license */ 2 | !function(t,e,s){var n=t.sc||{},i=/^\s+/,o=/\s+$/,r=/[^\s]+/,c=/[^\s]+$/,l=/[xy]/g,u=/&/g,h=/"/g,d=/'/g,a=//g,p=/^\w+$/,g={},f=function(t){return e.getElementById(t)};n.objectKeys=function(t){"use strict";var e=[];for(var s in t)t.hasOwnProperty(s)&&e.push(s);return e},n.obj2string=function(t,e){"use strict";var s=[];for(var i in t)if(t.hasOwnProperty(i)){var o=e?e+"["+i+"]":i,r=t[i];s.push("object"==typeof r?n.obj2string(r,o):encodeURIComponent(o)+"="+encodeURIComponent(r))}return s.join("&")},n.extendObj=function(t,e){"use strict";for(var s in e)e.hasOwnProperty(s)&&(t[s]=e[s])},n.isEmpty=function(t){"use strict";for(var e in t)if(t.hasOwnProperty(e))return!1;return!0},n.contains=function(t,e){"use strict";for(var s=t.length;s--;)if(t[s]===e)return!0;return!1},n.purge=function(t){"use strict";var e,s,i,o=t.attributes;if(o)for(e=o.length-1;e>=0;e-=1)i=o[e].name,"function"==typeof t[i]&&(t[i]=null);if(o=t.childNodes)for(s=o.length,e=0;s>e;e+=1)n.purge(t.childNodes[e])},n.remove=function(t){"use strict";t&&t.parentNode&&(n.purge(t),t.parentNode.removeChild(t)),t=null};var v="".trim;n.trim=v&&!v.call("\ufeff ")?function(t){return null===t?"":v.call(t)}:function(t){return null===t?"":t.toString().replace(i,"").replace(o,"")},n.getId=function(){"use strict";return"axxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(l,function(t){var e=16*Math.random()|0,s="x"==t?e:3&e|8;return s.toString(16)})},n.addEvent=function(t,s,i){"use strict";return"string"==typeof t&&(t=e.getElementById(t)),t.addEventListener?t.addEventListener(s,i,!1):t.attachEvent("on"+s,i),function(){n.removeEvent(t,s,i)}},n.removeEvent=function(t,s,n){"use strict";"string"==typeof t&&(t=e.getElementById(t)),t.removeEventListener?t.removeEventListener(s,n,!1):t.detachEvent("on"+s,n)},n.newXHR=function(){"use strict";if("undefined"!=typeof XMLHttpRequest)return new t.XMLHttpRequest;if(t.ActiveXObject)try{return new t.ActiveXObject("Microsoft.XMLHTTP")}catch(e){return!1}},n.encodeHTML=function(t){"use strict";return String(t).replace(u,"&").replace(h,""").replace(d,"'").replace(a,"<").replace(_,">")},n.parseJSON=function(e){"use strict";if(!e)return!1;if(e=n.trim(e+""),t.JSON&&t.JSON.parse)try{return t.JSON.parse(e+"")}catch(s){return!1}var i,o=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g,r=null;return e&&!n.trim(e.replace(o,function(t,e,s,n){return i&&e&&(r=0),0===r?t:(i=s||e,r+=!n-!s,"")}))?Function("return "+e)():!1},n.verifyElem=function(s){"use strict";return"undefined"!=typeof jQuery&&s instanceof jQuery?s=s[0]:"string"==typeof s&&("#"==s.charAt(0)&&(s=s.substr(1)),s=e.getElementById(s)),s&&1===s.nodeType?("A"==s.nodeName.toUpperCase()&&(s.style.cursor="pointer",n.addEvent(s,"click",function(e){e&&e.preventDefault?e.preventDefault():t.event&&(t.event.returnValue=!1)})),s):!1},n.SpellChecker=function(t){var e=this;if(this._settings={action:"",button:"",textInput:"",name:"text",data:{},debug:!1,onOpen:function(){},onClose:function(){}},n.extendObj(this._settings,t),this._button=n.verifyElem(this._settings.button),this._textInput=n.verifyElem(this._settings.textInput),delete this._settings.button,this._button===!1)throw new Error("Invalid button. Make sure the element you're passing exists.");if(this._textInput===!1)throw new Error("Invalid text field. Make sure the element you're passing exists.");this._closeOnEsc=function(t){27===t.keyCode&&e._closeChecker()},this._uId=n.getId(),this._createHTML(),this._currentBox=f("spell-current"+this._uId),this._contextBox=f("spell-context"+this._uId),this._undoBtn=f("spell-undo"+this._uId),this._select=f("spelling-suggestions"+this._uId),this._isOpen=!1,this._button.className+=" spellcheck-trigger",this.enable()},n.SpellChecker.prototype={enable:function(){"use strict";var t=this;this._button.off=n.addEvent(this._button,"click",function(){t._openChecker()}),n.addEvent("spelling-ignore"+this._uId,"click",function(){t._ignore()}),n.addEvent("spelling-ignore-all"+this._uId,"click",function(){t._ignore(!0)}),n.addEvent("spell-change"+this._uId,"click",function(){t._makeChange()}),n.addEvent("spell-change-all"+this._uId,"click",function(){t._makeChange(!0)}),n.addEvent("spell-close"+this._uId,"click",function(){t._closeChecker()}),n.addEvent("spell-msg-close"+this._uId,"click",function(){t._closeChecker()}),n.addEvent("spell-check-overlay"+this._uId,"click",function(){t._closeChecker()}),n.addEvent(this._undoBtn,"click",function(){t._undoChange()}),n.addEvent(this._currentBox,"click",function(){f("spelling-suggestions"+t._uId).selectedIndex=-1}),n.addEvent(this._contextBox,"click",function(){f("spelling-suggestions"+t._uId).selectedIndex=-1}),n.addEvent(this._currentBox,"keyup",function(){var t=this._contextBox.getElementsByTagName("span")[0];t&&t.firstChild&&(t.firstChild.nodeValue=this.value)}),n.addEvent(this._contextBox,"keyup",function(){var t=this.getElementsByTagName("span")[0];t&&t.firstChild&&(this._currentBox.value=t.firstChild.nodeValue)})},destroy:function(){"use strict";this._isOpen&&this._closeChecker(),this._button.off&&this._button.off(),this._button.className=this._button.className.replace(/(?:^|\s)spellcheck-trigger(?!\S)/,""),n.remove(this._msgBox),n.remove(this._modal),n.remove(this._overlay);for(var t in this)this.hasOwnProperty(t)&&delete this.prop},log:function(e){"use strict";this._settings.debug&&t.console&&t.console.log("[spell checker] "+e)},_getRegex:function(t){"use strict";return g[t]||(g[t]=new RegExp(t,"g")),g[t]},_openChecker:function(){"use strict";this._isOpen||(this._undoBtn.disabled=!0,this._overlay.style.display="block",this._modal.style.display="block",this._text=this._textInput.value,this._isOpen=!0,this._undo=[],n.addEvent(e,"keyup",this._closeOnEsc),this._notifyMsg("a"),this._sendData())},_closeChecker:function(){"use strict";this._isOpen&&(this._msgBox.style.display="none",this._modal.style.cssText="display:none; z-index:4999;",this._overlay.style.display="none",this._currentBox.value="",this._contextBox.innerHTML="",this._select.options.length=0,this._undo.length=0,this._text=this._wordObject=this._wordKeys=this._currentWord=this._wordMatches=this._matchOffset=this._undo=this._isOpen=null,n.removeEvent(e,"keyup",this._closeOnEsc),this._settings.onClose.call(this,this._button,this._textInput.value))},_notifyMsg:function(t){"use strict";var e,s=f("spell-msg-close-box"+this._uId);"a"==t?(e="Checking...",s.style.display="none"):s.style.display="block","b"==t&&(e="We experienced an error and were unable to complete the spell check."),"c"==t&&(e="Spell check completed. No errors found."),"d"==t&&(e="Spell check completed."),this._modal.style.zIndex=4999,f("spell-msg-text"+this._uId).innerHTML=e,this._msgBox.style.display="block","a"!=t&&f("spell-msg-close"+this._uId).focus()},_ignore:function(t){"use strict";var e;t===!0||this._wordMatches<=1||this._matchOffset===this._wordMatches?(this._wordKeys.splice(0,1),this._matchOffset=1,e=!1):(this._matchOffset++,e=!0),this._undoBtn.disabled=!0,this._undo.length=0,this._wordKeys.length>0||e===!0?this._reviewWord():this._notifyMsg("d")},_makeChange:function(t){"use strict";var e,s,n,i=this,o=this._getRegex(this._currentWord),r=this._select.selectedIndex,c=0;this._undo.unshift({text:this._text,word:this._currentWord,numMatches:this._wordMatches,matchOffset:this._matchOffset}),this._undoBtn.disabled=!1,e=r>-1?this._select.options[r].text:this._currentBox.value,s=this._text.replace(o,function(s){return c++,t===!0||i._matchOffset===c?e:s}),t===!0||i._wordMatches<=1||i._matchOffset===i._wordMatches?(this._wordKeys.splice(0,1),this._matchOffset=1,n=!1):n=!0,this._textInput.value=this._text=s,this._wordKeys.length>0||n===!0?this._reviewWord():this._notifyMsg("d")},_undoChange:function(){"use strict";var t=this._undo[0];this._textInput.value=this._text=t.text,this._currentBox.value=t.word,n.contains(this._wordKeys,t.word)||this._wordKeys.unshift(t.word),this._currentWord=t.word,this._wordMatches=t.numMatches,this._matchOffset=t.matchOffset,this._setSuggestionOptions(),this._setContextBox(),this._undo.splice(0,1),this._undo.length<1&&(this._undoBtn.disabled=!0)},_setSuggestionOptions:function(){"use strict";var t,e=this._wordObject[this._currentWord],s=e.length;for(this._select.options.length=0,t=0;s>t;t++)this._select.options[t]=new Option(e[t],e[t]);this._select.selectedIndex=0},_setContextBox:function(){"use strict";var t=this,e=this._currentWord.length,s=this._getRegex(this._currentWord),l=this._text.length,u=0;this._text.replace(s,function(s,h){if(p.test(t._text.substr(h+e,1)))return s;if(u++,u===t._matchOffset){var d,a,_=h-20,g=h+e;d=0>_?t._text.substr(0,h):t._text.substr(_,20),a=g+50>l?t._text.substr(g):t._text.substr(g,50),d=d.replace(i,"").replace(r,"").replace(i,""),a=a.replace(o,"").replace(c,"").replace(o,""),t._contextBox.innerHTML=n.encodeHTML(d)+''+n.encodeHTML(t._currentWord)+""+n.encodeHTML(a)}return s})},_reviewWord:function(){"use strict";this._currentWord=this._wordKeys[0],this._currentBox.value=this._currentWord,this._wordMatches=this._getTotalWordMatches(),this._setSuggestionOptions(),this._setContextBox()},_getTotalWordMatches:function(){"use strict";var t=this._getRegex(this._currentWord),e=this._currentWord.length,s=0,n=this._text;return this._text.replace(t,function(t,i){return p.test(n.substr(i+e,1))||s++,t}),s},_begin:function(t){"use strict";t.success&&t.success===!0&&(t.errors&&t.errors===!0?(this._wordObject=t.words,this._wordKeys=n.objectKeys(this._wordObject),this._matchOffset=1,this._msgBox.style.display="none",this._modal.style.zIndex=5001,this._reviewWord()):this._notifyMsg("c"))},_sendData:function(){"use strict";var t,e,i=this,o=n.newXHR();return this._text.length<2?void this._notifyMsg("c"):(t=encodeURIComponent(this._settings.name)+"=",t+=encodeURIComponent(this._text),n.isEmpty(this._settings.data)||(t+="&",t+=n.obj2string(this._settings.data)),e=function(){var t,r,c;try{if(e&&4===o.readyState){if(e=s,o.onreadystatechange=function(){},!i._isOpen)return;r=o.status;try{c=o.statusText}catch(l){c=""}i.log("Request completed. Status: "+r+" "+c),r>=200&&300>r?(t=n.parseJSON(o.responseText),t!==!1?(i._settings.onOpen.call(i,i._button,i._text),i._begin(t)):(i.log("Error parsing server response"),i._notifyMsg("b")),o=t=null):i._notifyMsg("b")}}catch(l){i.log("Error: "+l.message),i._notifyMsg("b")}},o.onreadystatechange=e,o.open("POST",this._settings.action,!0),o.setRequestHeader("Accept","application/json, text/javascript, */*; q=0.01"),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),i.log("Sending data..."),void o.send(t))},_createHTML:function(){this._overlay=e.createElement("div"),this._modal=e.createElement("div"),this._msgBox=e.createElement("div"),this._overlay.className="spell-check-overlay",this._overlay.id="spell-check-overlay"+this._uId,e.body.appendChild(this._overlay),this._modal.className="spell-wrap",this._modal.innerHTML='
Spell Check
Not found in dictionary:
Suggestions:

',e.body.appendChild(this._modal),this._msgBox.className="spell-msg",this._msgBox.innerHTML='
Spell Check
',e.body.appendChild(this._msgBox)}},t.sc=n}(window,document); 3 | -------------------------------------------------------------------------------- /spellcheck.php: -------------------------------------------------------------------------------- 1 | false))); 19 | } 20 | 21 | if (!$pspell = pspell_new('en', '', '', '', PSPELL_FAST)) { 22 | exit(json_encode(array('success' => false))); 23 | } 24 | 25 | $words = preg_split('/[\W]+/u', $text, -1, PREG_SPLIT_NO_EMPTY); 26 | $misspelled = array(); 27 | $return = array(); 28 | 29 | foreach ($words as $w) { 30 | if (!pspell_check($pspell, $w) && !is_numeric($w)) { 31 | $misspelled[] = $w; 32 | } 33 | } 34 | 35 | if (sizeof($misspelled) < 1) { 36 | exit(json_encode(array('success' => true, 'errors' => false))); 37 | } 38 | 39 | foreach ($misspelled as $m) { 40 | $return[$m] = pspell_suggest($pspell, $m); 41 | } 42 | 43 | echo json_encode(array('success' => true, 'errors' => true, 'words' => $return)); 44 | --------------------------------------------------------------------------------