Spaces:
Running
Running
/** | |
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. | |
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license | |
*/ | |
; | |
( function() { | |
CKEDITOR.plugins.add( 'textmatch', {} ); | |
/** | |
* A global namespace for methods exposed by the [Text Match](https://ckeditor.com/cke4/addon/textmatch) plugin. | |
* | |
* The most important function is {@link #match} which performs a text | |
* search in the DOM. | |
* | |
* @singleton | |
* @class | |
* @since 4.10.0 | |
*/ | |
CKEDITOR.plugins.textMatch = {}; | |
/** | |
* Allows to search in the DOM for matching text using a callback which operates on strings instead of text nodes. | |
* Returns {@link CKEDITOR.dom.range} and the matching text. | |
* | |
* ```javascript | |
* var range = editor.getSelection().getRanges()[ 0 ]; | |
* | |
* CKEDITOR.plugins.textMatch.match( range, function( text, offset ) { | |
* // Let's assume that text is 'Special thanks to #jo.' and offset is 21. | |
* // The offset "21" means that the caret is between '#jo' and '.'. | |
* | |
* // Get the text before the caret. | |
* var left = text.slice( 0, offset ), | |
* // Will look for a literal '#' character and at least two word characters. | |
* match = left.match( /#\w{2,}$/ ); | |
* | |
* if ( !match ) { | |
* return null; | |
* } | |
* | |
* // The matching fragment is the '#jo', which can | |
* // be identified by the following offsets: { start: 18, end: 21 }. | |
* return { start: match.index, end: offset }; | |
* } ); | |
* ``` | |
* | |
* @member CKEDITOR.plugins.textMatch | |
* @param {CKEDITOR.dom.range} range A collapsed range — the position from which the scanning starts. | |
* Usually the caret position. | |
* @param {Function} testCallback A callback executed to check if the text matches. | |
* @param {String} testCallback.text The full text to check. | |
* @param {Number} testCallback.rangeOffset An offset of the `range` in the `text` to be checked. | |
* @param {Object} [testCallback.return] The position of the matching fragment (`null` if nothing matches). | |
* @param {Number} testCallback.return.start The offset of the start of the matching fragment. | |
* @param {Number} testCallback.return.end The offset of the end of the matching fragment. | |
* | |
* @returns {Object/null} An object with information about the matching text or `null`. | |
* @returns {String} return.text The matching text. | |
* The text does not reflect the range offsets. The range could contain additional, | |
* browser-related characters like {@link CKEDITOR.dom.selection#FILLING_CHAR_SEQUENCE}. | |
* @returns {CKEDITOR.dom.range} return.range A range in the DOM for the text that matches. | |
*/ | |
CKEDITOR.plugins.textMatch.match = function( range, callback ) { | |
var textAndOffset = CKEDITOR.plugins.textMatch.getTextAndOffset( range ), | |
fillingCharSequence = CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE, | |
fillingSequenceOffset = 0; | |
if ( !textAndOffset ) { | |
return; | |
} | |
// Remove filling char sequence for clean query (#2038). | |
if ( textAndOffset.text.indexOf( fillingCharSequence ) == 0 ) { | |
fillingSequenceOffset = fillingCharSequence.length; | |
textAndOffset.text = textAndOffset.text.replace( fillingCharSequence, '' ); | |
textAndOffset.offset -= fillingSequenceOffset; | |
} | |
var result = callback( textAndOffset.text, textAndOffset.offset ); | |
if ( !result ) { | |
return null; | |
} | |
return { | |
range: CKEDITOR.plugins.textMatch.getRangeInText( range, result.start, result.end + fillingSequenceOffset ), | |
text: textAndOffset.text.slice( result.start, result.end ) | |
}; | |
}; | |
/** | |
* Returns a text (as a string) in which the DOM range is located (the function scans for adjacent text nodes) | |
* and the offset of the caret in that text. | |
* | |
* ## Examples | |
* | |
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position). | |
* * `[]` is the range position in the element (it means that the text node is split at that position). | |
* * `.` is a separator for text nodes (it means that the text node is split at that position). | |
* | |
* Examples: | |
* | |
* ``` | |
* Input: <p>he[]llo</p> | |
* Result: { text: 'hello', offset: 2 } | |
* | |
* Input: <p>he.llo{}</p> | |
* Result: { text: 'hello', offset: 5 } | |
* | |
* Input: <p>{}he.ll<i>o</i></p> | |
* Result: { text: 'hell', offset: 0 } | |
* | |
* Input: <p>he{}<i>ll</i>o</p> | |
* Result: { text: 'he', offset: 2 } | |
* | |
* Input: <p>he<i>ll</i>o.m{}y.friend</p> | |
* Result: { text: 'omyfriend', offset: 2 } | |
* ``` | |
* | |
* @member CKEDITOR.plugins.textMatch | |
* @param {CKEDITOR.dom.range} range | |
* @returns {Object/null} | |
* @returns {String} return.text The text in which the DOM range is located. | |
* @returns {Number} return.offset An offset of the caret. | |
*/ | |
CKEDITOR.plugins.textMatch.getTextAndOffset = function( range ) { | |
if ( !range.collapsed ) { | |
return null; | |
} | |
var text = '', offset = 0, | |
textNodes = CKEDITOR.plugins.textMatch.getAdjacentTextNodes( range ), | |
nodeReached = false, | |
elementIndex, | |
startContainerIsText = ( range.startContainer.type != CKEDITOR.NODE_ELEMENT ); | |
if ( startContainerIsText ) { | |
// Determining element index in textNodes array. | |
elementIndex = indexOf( textNodes, function( current ) { | |
return range.startContainer.equals( current ); | |
} ); | |
} else { | |
// Based on range startOffset decreased by first text node index. | |
elementIndex = range.startOffset - ( textNodes[ 0 ] ? textNodes[ 0 ].getIndex() : 0 ); | |
} | |
var max = textNodes.length; | |
for ( var i = 0; i < max; i += 1 ) { | |
var currentNode = textNodes[ i ]; | |
text += currentNode.getText(); | |
// We want to increase text offset only when startContainer is not reached. | |
if ( !nodeReached ) { | |
if ( startContainerIsText ) { | |
if ( i == elementIndex ) { | |
nodeReached = true; | |
offset += range.startOffset; | |
} else { | |
offset += currentNode.getText().length; | |
} | |
} else { | |
if ( i == elementIndex ) { | |
nodeReached = true; | |
} | |
// In below example there are three text nodes in p element and four possible offsets ( 0, 1, 2, 3 ) | |
// We are going to increase offset while iteration: | |
// index 0 ==> 0 | |
// index 1 ==> 3 | |
// index 2 ==> 3 + 3 | |
// index 3 ==> 3 + 3 + 2 | |
// <p> foo bar ba </p> | |
// 0^^^1^^^2^^3 | |
if ( i > 0 ) { | |
offset += textNodes[ i - 1 ].getText().length; | |
} | |
// If element index at last element we also want to increase offset. | |
if ( max == elementIndex && i + 1 == max ) { | |
offset += currentNode.getText().length; | |
} | |
} | |
} | |
} | |
return { | |
text: text, | |
offset: offset | |
}; | |
}; | |
/** | |
* Transforms the `start` and `end` offsets in the text generated by the {@link #getTextAndOffset} | |
* method into a DOM range. | |
* | |
* ## Examples | |
* | |
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position). | |
* * `.` is a separator for text nodes (it means that the text node is split at that position). | |
* | |
* Examples: | |
* | |
* ``` | |
* Input: <p>f{}oo.bar</p>, 0, 3 | |
* Result: <p>{foo}.bar</p> | |
* | |
* Input: <p>f{}oo.bar</p>, 1, 5 | |
* Result: <p>f{oo.ba}r</p> | |
* ``` | |
* | |
* @member CKEDITOR.plugins.textMatch | |
* @param {CKEDITOR.dom.range} range | |
* @param {Number} start A start offset. | |
* @param {Number} end An end offset. | |
* @returns {CKEDITOR.dom.range} Transformed range. | |
*/ | |
CKEDITOR.plugins.textMatch.getRangeInText = function( range, start, end ) { | |
var resultRange = new CKEDITOR.dom.range( range.root ), | |
elements = CKEDITOR.plugins.textMatch.getAdjacentTextNodes( range ), | |
startData = findElementAtOffset( elements, start ), | |
endData = findElementAtOffset( elements, end ); | |
resultRange.setStart( startData.element, startData.offset ); | |
resultRange.setEnd( endData.element, endData.offset ); | |
return resultRange; | |
}; | |
/** | |
* Creates a collection of adjacent text nodes which are between DOM elements, starting from the given range. | |
* This function works only for collapsed ranges. | |
* | |
* ## Examples | |
* | |
* * `{}` is the range position in the text node (it means that the text node is **not** split at that position). | |
* * `.` is a separator for text nodes (it means that the text node is split at that position). | |
* | |
* Examples: | |
* | |
* ``` | |
* Input: <p>he.llo{}</p> | |
* Result: [ 'he', 'llo' ] | |
* | |
* Input: <p>{}he.ll<i>o</i></p> | |
* Result: [ 'he', 'll' ] | |
* | |
* Input: <p>he{}<i>ll</i>o.</p> | |
* Result: [ 'he' ] | |
* | |
* Input: <p>he<i>ll</i>{}o.my.friend</p> | |
* Result: [ 'o', 'my', 'friend' ] | |
* ``` | |
* | |
* @member CKEDITOR.plugins.textMatch | |
* @param {CKEDITOR.dom.range} range | |
* @return {CKEDITOR.dom.text[]} An array of text nodes. | |
*/ | |
CKEDITOR.plugins.textMatch.getAdjacentTextNodes = function( range ) { | |
if ( !range.collapsed ) { | |
throw new Error( 'Range must be collapsed.' ); // %REMOVE_LINE% | |
// Reachable in prod mode. | |
return []; // jshint ignore:line | |
} | |
var collection = [], | |
siblings, | |
elementIndex, | |
node, i; | |
if ( range.startContainer.type != CKEDITOR.NODE_ELEMENT ) { | |
siblings = range.startContainer.getParent().getChildren(); | |
elementIndex = range.startContainer.getIndex(); | |
} else { | |
siblings = range.startContainer.getChildren(); | |
elementIndex = range.startOffset; | |
} | |
i = elementIndex; | |
while ( node = siblings.getItem( --i ) ) { | |
if ( node.type == CKEDITOR.NODE_TEXT ) { | |
collection.unshift( node ); | |
} else { | |
break; | |
} | |
} | |
i = elementIndex; | |
while ( node = siblings.getItem( i++ ) ) { | |
if ( node.type == CKEDITOR.NODE_TEXT ) { | |
collection.push( node ); | |
} else { | |
break; | |
} | |
} | |
return collection; | |
}; | |
function findElementAtOffset( elements, offset ) { | |
var max = elements.length, | |
currentOffset = 0; | |
for ( var i = 0; i < max; i += 1 ) { | |
var current = elements[ i ]; | |
if ( offset >= currentOffset && currentOffset + current.getText().length >= offset ) { | |
return { | |
element: current, | |
offset: offset - currentOffset | |
}; | |
} | |
currentOffset += current.getText().length; | |
} | |
return null; | |
} | |
function indexOf( arr, checker ) { | |
for ( var i = 0; i < arr.length; i++ ) { | |
if ( checker( arr[ i ] ) ) { | |
return i; | |
} | |
} | |
return -1; | |
} | |
} )(jQuery); | |