@@ -13,6 +13,8 @@ import Range from '../range';
1313import { isInsideSurrogatePair , isInsideCombinedSymbol } from '@ckeditor/ckeditor5-utils/src/unicode' ;
1414import DocumentSelection from '../documentselection' ;
1515
16+ const wordBoundaryCharacters = ' ,.?!:;"-()' ;
17+
1618/**
1719 * Modifies the selection. Currently, the supported modifications are:
1820 *
@@ -31,6 +33,7 @@ import DocumentSelection from '../documentselection';
3133 * For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
3234 * outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
3335 * extension will include whole "surrogate pair".
36+ * * `'word'` - moves selection by a whole word.
3437 *
3538 * **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
3639 *
@@ -39,7 +42,7 @@ import DocumentSelection from '../documentselection';
3942 * @param {module:engine/model/selection~Selection } selection The selection to modify.
4043 * @param {Object } [options]
4144 * @param {'forward'|'backward' } [options.direction='forward'] The direction in which the selection should be modified.
42- * @param {'character'|'codePoint' } [options.unit='character'] The unit by which selection should be modified.
45+ * @param {'character'|'codePoint'|'word' } [options.unit='character'] The unit by which selection should be modified.
4346 */
4447export default function modifySelection ( model , selection , options = { } ) {
4548 const schema = model . schema ;
@@ -79,11 +82,17 @@ export default function modifySelection( model, selection, options = {} ) {
7982}
8083
8184// Checks whether the selection can be extended to the the walker's next value (next position).
85+ // @param {{ walker, unit, isForward, schema } } data
86+ // @param {module:engine/view/treewalker~TreeWalkerValue } value
8287function tryExtendingTo ( data , value ) {
8388 // If found text, we can certainly put the focus in it. Let's just find a correct position
8489 // based on the unit.
8590 if ( value . type == 'text' ) {
86- return getCorrectPosition ( data . walker , data . unit ) ;
91+ if ( data . unit === 'word' ) {
92+ return getCorrectWordBreakPosition ( data . walker , data . isForward ) ;
93+ }
94+
95+ return getCorrectPosition ( data . walker , data . unit , data . isForward ) ;
8796 }
8897
8998 // Entering an element.
@@ -117,6 +126,9 @@ function tryExtendingTo( data, value ) {
117126
118127// Finds a correct position by walking in a text node and checking whether selection can be extended to given position
119128// or should be extended further.
129+ //
130+ // @param {module:engine/model/treewalker~TreeWalker } walker
131+ // @param {String } unit The unit by which selection should be modified.
120132function getCorrectPosition ( walker , unit ) {
121133 const textNode = walker . position . textNode ;
122134
@@ -134,6 +146,45 @@ function getCorrectPosition( walker, unit ) {
134146 return walker . position ;
135147}
136148
149+ // Finds a correct position of a word break by walking in a text node and checking whether selection can be extended to given position
150+ // or should be extended further.
151+ //
152+ // @param {module:engine/model/treewalker~TreeWalker } walker
153+ // @param {Boolean } isForward Is the direction in which the selection should be modified is forward.
154+ function getCorrectWordBreakPosition ( walker , isForward ) {
155+ let textNode = walker . position . textNode ;
156+
157+ if ( textNode ) {
158+ let offset = walker . position . offset - textNode . startOffset ;
159+
160+ while ( ! isAtWordBoundary ( textNode . data , offset , isForward ) && ! isAtNodeBoundary ( textNode , offset , isForward ) ) {
161+ walker . next ( ) ;
162+
163+ // Check of adjacent text nodes with different attributes (like BOLD).
164+ // Example : 'foofoo []bar<$text bold="true">bar</$text> bazbaz'
165+ // should expand to : 'foofoo [bar<$text bold="true">bar</$text>] bazbaz'.
166+ const nextNode = isForward ? walker . position . nodeAfter : walker . position . nodeBefore ;
167+
168+ if ( nextNode ) {
169+ // Check boundary char of an adjacent text node.
170+ const boundaryChar = nextNode . data . charAt ( isForward ? 0 : nextNode . data . length - 1 ) ;
171+
172+ // Go to the next node if the character at the boundary of that node belongs to the same word.
173+ if ( ! wordBoundaryCharacters . includes ( boundaryChar ) ) {
174+ // If adjacent text node belongs to the same word go to it & reset values.
175+ walker . next ( ) ;
176+
177+ textNode = walker . position . textNode ;
178+ }
179+ }
180+
181+ offset = walker . position . offset - textNode . startOffset ;
182+ }
183+ }
184+
185+ return walker . position ;
186+ }
187+
137188function getSearchRange ( start , isForward ) {
138189 const root = start . root ;
139190 const searchEnd = Position . createAt ( root , isForward ? 'end' : 0 ) ;
@@ -144,3 +195,24 @@ function getSearchRange( start, isForward ) {
144195 return new Range ( searchEnd , start ) ;
145196 }
146197}
198+
199+ // Checks if selection is on word boundary.
200+ //
201+ // @param {String } data The text node value to investigate.
202+ // @param {Number } offset Position offset.
203+ // @param {Boolean } isForward Is the direction in which the selection should be modified is forward.
204+ function isAtWordBoundary ( data , offset , isForward ) {
205+ // The offset to check depends on direction.
206+ const offsetToCheck = offset + ( isForward ? 0 : - 1 ) ;
207+
208+ return wordBoundaryCharacters . includes ( data . charAt ( offsetToCheck ) ) ;
209+ }
210+
211+ // Checks if selection is on node boundary.
212+ //
213+ // @param {module:engine/model/text~Text } textNode The text node to investigate.
214+ // @param {Number } offset Position offset.
215+ // @param {Boolean } isForward Is the direction in which the selection should be modified is forward.
216+ function isAtNodeBoundary ( textNode , offset , isForward ) {
217+ return offset === ( isForward ? textNode . endOffset : 0 ) ;
218+ }
0 commit comments