@@ -4128,6 +4128,7 @@ var content = (function() {
4128
4128
4129
4129
var zeroWidthSpace = / \u200B / g;
4130
4130
var zeroWidthNonBreakingSpace = / \uFEFF / g;
4131
+ var whitespaceExceptSpace = / [ ^ \S ] / g;
4131
4132
4132
4133
return {
4133
4134
@@ -4183,6 +4184,10 @@ var content = (function() {
4183
4184
element . appendChild ( fragment ) ;
4184
4185
} ,
4185
4186
4187
+ normalizeWhitespace : function ( text ) {
4188
+ return text . replace ( whitespaceExceptSpace , ' ' ) ;
4189
+ } ,
4190
+
4186
4191
/**
4187
4192
* Clean the element from character, tags, etc... added by the plugin logic.
4188
4193
*
@@ -5585,13 +5590,29 @@ var highlightText = (function() {
5585
5590
5586
5591
return {
5587
5592
extractText : function ( element ) {
5588
- var textNode ;
5589
5593
var text = '' ;
5594
+ this . getText ( element , function ( part ) {
5595
+ text += part ;
5596
+ } ) ;
5597
+ return text ;
5598
+ } ,
5599
+
5600
+ // Extract the text of an element.
5601
+ // This has two notable behaviours:
5602
+ // - It uses a NodeIterator which will skip elements
5603
+ // with data-editable="remove"
5604
+ // - It returns a space for <br> elements
5605
+ // (The only block level element allowed inside of editables)
5606
+ getText : function ( element , callback ) {
5590
5607
var iterator = new NodeIterator ( element ) ;
5591
- while ( ( textNode = iterator . getNextTextNode ( ) ) ) {
5592
- text = text + textNode . data ;
5608
+ var next ;
5609
+ while ( ( next = iterator . getNext ( ) ) ) {
5610
+ if ( next . nodeType === nodeType . textNode && next . data !== '' ) {
5611
+ callback ( next . data ) ;
5612
+ } else if ( next . nodeType === nodeType . elementNode && next . nodeName === 'BR' ) {
5613
+ callback ( ' ' ) ;
5614
+ }
5593
5615
}
5594
- return text ;
5595
5616
} ,
5596
5617
5597
5618
highlight : function ( element , regex , stencilElement ) {
@@ -5616,21 +5637,33 @@ var highlightText = (function() {
5616
5637
return ;
5617
5638
}
5618
5639
5619
- var textNode , length , offset , isFirstPortion , isLastPortion ;
5640
+ var next , textNode , length , offset , isFirstPortion , isLastPortion , wordId ;
5620
5641
var currentMatchIndex = 0 ;
5621
5642
var currentMatch = matches [ currentMatchIndex ] ;
5622
5643
var totalOffset = 0 ;
5623
5644
var iterator = new NodeIterator ( element ) ;
5624
5645
var portions = [ ] ;
5625
- while ( ( textNode = iterator . getNextTextNode ( ) ) ) {
5646
+ while ( ( next = iterator . getNext ( ) ) ) {
5647
+
5648
+ // Account for <br> elements
5649
+ if ( next . nodeType === nodeType . textNode && next . data !== '' ) {
5650
+ textNode = next ;
5651
+ } else if ( next . nodeType === nodeType . elementNode && next . nodeName === 'BR' ) {
5652
+ totalOffset = totalOffset + 1 ;
5653
+ continue ;
5654
+ } else {
5655
+ continue ;
5656
+ }
5657
+
5626
5658
var nodeText = textNode . data ;
5627
5659
var nodeEndOffset = totalOffset + nodeText . length ;
5628
- if ( nodeEndOffset > currentMatch . startIndex && totalOffset < currentMatch . endIndex ) {
5660
+ if ( currentMatch . startIndex < nodeEndOffset && totalOffset < currentMatch . endIndex ) {
5629
5661
5630
- // get portion position
5662
+ // get portion position (fist, last or in the middle)
5631
5663
isFirstPortion = isLastPortion = false ;
5632
5664
if ( totalOffset <= currentMatch . startIndex ) {
5633
5665
isFirstPortion = true ;
5666
+ wordId = currentMatch . startIndex ;
5634
5667
}
5635
5668
if ( nodeEndOffset >= currentMatch . endIndex ) {
5636
5669
isLastPortion = true ;
@@ -5646,16 +5679,17 @@ var highlightText = (function() {
5646
5679
if ( isLastPortion ) {
5647
5680
length = ( currentMatch . endIndex - totalOffset ) - offset ;
5648
5681
} else {
5649
- length = textNode . data . length - offset ;
5682
+ length = nodeText . length - offset ;
5650
5683
}
5651
5684
5652
5685
// create portion object
5653
5686
var portion = {
5654
5687
element : textNode ,
5655
- text : textNode . data . substring ( offset , offset + length ) ,
5688
+ text : nodeText . substring ( offset , offset + length ) ,
5656
5689
offset : offset ,
5657
5690
length : length ,
5658
- isLastPortion : isLastPortion
5691
+ isLastPortion : isLastPortion ,
5692
+ wordId : wordId
5659
5693
} ;
5660
5694
5661
5695
portions . push ( portion ) ;
@@ -5701,6 +5735,7 @@ var highlightText = (function() {
5701
5735
range . setStart ( portion . element , portion . offset ) ;
5702
5736
range . setEnd ( portion . element , portion . offset + portion . length ) ;
5703
5737
var node = stencilElement . cloneNode ( true ) ;
5738
+ node . setAttribute ( 'data-word-id' , portion . wordId ) ;
5704
5739
range . surroundContents ( node ) ;
5705
5740
5706
5741
// Fix a weird behaviour where an empty text node is inserted after the range
@@ -6776,11 +6811,12 @@ var Spellcheck = (function() {
6776
6811
*/
6777
6812
var Spellcheck = function ( editable , configuration ) {
6778
6813
var defaultConfig = {
6779
- checkOnChange : true ,
6780
- checkOnFocus : false ,
6781
- spellcheckService : undefined ,
6782
- markerNode : $ ( '<span class="spellcheck"></span>' ) [ 0 ] ,
6783
- throttle : 1000 // delay after changes stop before calling the spellcheck service
6814
+ checkOnFocus : false , // check on focus
6815
+ checkOnChange : true , // check after changes
6816
+ throttle : 1000 , // unbounce rate in ms before calling the spellcheck service after changes
6817
+ removeOnCorrection : true , // remove highlights after a change if the cursor is inside a highlight
6818
+ markerNode : $ ( '<span class="spellcheck"></span>' ) ,
6819
+ spellcheckService : undefined
6784
6820
} ;
6785
6821
6786
6822
this . config = $ . extend ( defaultConfig , configuration ) ;
@@ -6794,7 +6830,7 @@ var Spellcheck = (function() {
6794
6830
this . editable . on ( 'focus' , $ . proxy ( this , 'onFocus' ) ) ;
6795
6831
this . editable . on ( 'blur' , $ . proxy ( this , 'onBlur' ) ) ;
6796
6832
}
6797
- if ( this . config . checkOnChange ) {
6833
+ if ( this . config . checkOnChange || this . config . removeOnCorrection ) {
6798
6834
this . editable . on ( 'change' , $ . proxy ( this , 'onChange' ) ) ;
6799
6835
}
6800
6836
} ;
@@ -6813,7 +6849,12 @@ var Spellcheck = (function() {
6813
6849
} ;
6814
6850
6815
6851
Spellcheck . prototype . onChange = function ( editableHost ) {
6816
- this . editableHasChanged ( editableHost ) ;
6852
+ if ( this . config . checkOnChange ) {
6853
+ this . editableHasChanged ( editableHost , this . config . throttle ) ;
6854
+ }
6855
+ if ( this . config . removeOnCorrection ) {
6856
+ this . removeHighlightsAtCursor ( editableHost ) ;
6857
+ }
6817
6858
} ;
6818
6859
6819
6860
Spellcheck . prototype . prepareMarkerNode = function ( ) {
@@ -6835,6 +6876,33 @@ var Spellcheck = (function() {
6835
6876
} ) ;
6836
6877
} ;
6837
6878
6879
+ Spellcheck . prototype . removeHighlightsAtCursor = function ( editableHost ) {
6880
+ var wordId ;
6881
+ var selection = this . editable . getSelection ( editableHost ) ;
6882
+ if ( selection && selection . isCursor ) {
6883
+ var elementAtCursor = selection . range . startContainer ;
6884
+ if ( elementAtCursor . nodeType === nodeType . textNode ) {
6885
+ elementAtCursor = elementAtCursor . parentNode ;
6886
+ }
6887
+
6888
+ do {
6889
+ if ( elementAtCursor === editableHost ) return ;
6890
+ if ( elementAtCursor . hasAttribute ( 'data-word-id' ) ) {
6891
+ wordId = elementAtCursor . getAttribute ( 'data-word-id' ) ;
6892
+ break ;
6893
+ }
6894
+ } while ( ( elementAtCursor = elementAtCursor . parentNode ) ) ;
6895
+
6896
+ if ( wordId ) {
6897
+ selection . retainVisibleSelection ( function ( ) {
6898
+ $ ( editableHost ) . find ( '[data-word-id=' + wordId + ']' ) . each ( function ( index , elem ) {
6899
+ content . unwrap ( elem ) ;
6900
+ } ) ;
6901
+ } ) ;
6902
+ }
6903
+ }
6904
+ } ;
6905
+
6838
6906
Spellcheck . prototype . createRegex = function ( words ) {
6839
6907
var escapedWords = $ . map ( words , function ( word ) {
6840
6908
return escapeRegEx ( word ) ;
@@ -6861,7 +6929,7 @@ var Spellcheck = (function() {
6861
6929
}
6862
6930
} ;
6863
6931
6864
- Spellcheck . prototype . editableHasChanged = function ( editableHost ) {
6932
+ Spellcheck . prototype . editableHasChanged = function ( editableHost , throttle ) {
6865
6933
if ( this . timeoutId && this . currentEditableHost === editableHost ) {
6866
6934
clearTimeout ( this . timeoutId ) ;
6867
6935
}
@@ -6871,14 +6939,16 @@ var Spellcheck = (function() {
6871
6939
that . checkSpelling ( editableHost ) ;
6872
6940
that . currentEditableHost = undefined ;
6873
6941
that . timeoutId = undefined ;
6874
- } , this . config . throttle ) ;
6942
+ } , throttle || 0 ) ;
6875
6943
6876
6944
this . currentEditableHost = editableHost ;
6877
6945
} ;
6878
6946
6879
6947
Spellcheck . prototype . checkSpelling = function ( editableHost ) {
6880
6948
var that = this ;
6881
6949
var text = highlightText . extractText ( editableHost ) ;
6950
+ text = content . normalizeWhitespace ( text ) ;
6951
+
6882
6952
this . config . spellcheckService ( text , function ( misspelledWords ) {
6883
6953
var selection = that . editable . getSelection ( editableHost ) ;
6884
6954
if ( selection ) {
0 commit comments