nmitchko's picture
Upload 77 files
1e4c25f
raw
history blame contribute delete
No virus
10.2 kB
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
'use strict';
( 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);