2 * tr = X.Doc.createRange('selection')
\r
3 * X.Doc.createRange({from : num, to : num})
\r
4 * tr = xnode.createRange( from, to ),
\r
5 * ( 'selection' ) docment の slection のうち xnode の配下のもの, textarea, input, iframe[desineMode] の場合、選択範囲またはカーソル位置
\r
6 * ( 'select', from[, to] ) 選択する
\r
7 * ( 'char', from[, to] )
\r
8 * ( 'lineAt', index ),
\r
9 * ( 'line', x, y ), | ( 'line', pointerEvent )
\r
10 * ( 'point', x, y ) | ( 'point', pointerEvent )
\r
11 * tr.move( from, to )
\r
12 * tr.select( true | false )
\r
13 * tr.getRect() { width, height, x, y } -> tr.mesure()
\r
14 * tr.getOffset() { from, to }
\r
17 * naming は mozilla に寄せる
\r
20 var X_TextRange_range,
\r
22 X_TextRange_isW3C = !document.selection || 10 <= X_UA[ 'IE' ];
\r
25 * ユーザーによって選択されたテキストへの参照や文字の座標の取得
\r
26 * @alias X.TextRange
\r
27 * @class TextRange テキストレンジ
\r
28 * @extends {__ClassBase__}
\r
30 var X_TextRange = X_Class_create(
\r
33 // TODO コールバックの最後に破棄されるクラス 1刻みの間存在するクラス. X.XML も
\r
35 /** @lends X.TextRange.prototype */
\r
42 'Constructor' : function( xnode, arg2, arg3, arg4 ){
\r
43 if( !X_TextRange_range ){
\r
44 X_TextRange_range = X_TextRange_isW3C ? document.createRange() : X_elmBody.createTextRange();
\r
45 if( !X_TextRange_isW3C ) X_TextRange_range2 = X_elmBody.createTextRange();
\r
55 this.createFrom = arg2;
\r
62 if( arg2 !== 'selection' ){
\r
63 this.v1 = arg3 || 0;
\r
64 this.v2 = arg4 || 0;
\r
66 this[ 'getOffset' ]();
\r
70 'move' : X_TextRange_move,
\r
72 'select' : X_TextRange_select,
\r
74 'getRect' : X_TextRange_getRect,
\r
76 'getOffset' : X_TextRange_getOffset,
\r
78 'text' : X_TextRange_text
\r
82 // TextNode を探して flat な配列に格納する
\r
83 function X_TextRange_collectTextNodes( elm, ary ){
\r
84 var kids = elm.childNodes,
\r
87 if( !kids || !kids.length ) return;
\r
89 for( i = 0; e = kids[ i ]; ++i ){
\r
90 switch( e.nodeType ){
\r
92 X_TextRange_collectTextNodes( e, ary );
\r
95 ary[ ary.length ] = e;
\r
101 function X_TextRange_getRawRange( tr, createFrom ){
\r
102 var xnode = tr.xnode,
\r
104 range = 10 <= X_UA[ 'IE' ] /* || X_UA[ 'iOS' ] */ ? document.createRange() :
\r
105 8 <= X_UA[ 'IE' ] ? X_elmBody.createTextRange() : X_TextRange_range,
\r
106 elm, selection, isPoint,
\r
107 texts, i, offset, j, l, x, y, rect;
\r
109 if( xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){
\r
111 X_Node_updateTimerID && X_Node_startUpdate();
\r
113 elm = xnode[ '_rawObject' ];
\r
115 switch( createFrom || tr.createFrom ){
\r
117 if( X_TextRange_isW3C ){
\r
118 selection = window.getSelection();
\r
120 if( selection.getRangeAt ){
\r
121 return selection.rangeCount && selection.getRangeAt( 0 );
\r
123 // http://d.hatena.ne.jp/dayflower/20080423/1208941641
\r
125 range = document.createRange();
\r
126 range.setStart( selection.anchorNode, selection.anchorOffset );
\r
127 range.setEnd( selection.focusNode, selection.focusOffset );
\r
130 switch( document.selection.type ){
\r
132 return document.selection.createRange();
\r
143 if( X_TextRange_isW3C ){
\r
147 // TextNode をフラットな配列に回収
\r
148 X_TextRange_collectTextNodes( elm, texts = [] );
\r
150 for( i = offset = 0; text = texts[ i ]; ++i ){
\r
151 range.selectNodeContents( text ); // selectNodeContents は TextNode のみ?? Firefox
\r
152 l = text.data.length;
\r
154 for( j = 0, x = tr.v1, y = tr.v2; j < l; ++j ){
\r
156 range.setStart( text, j );
\r
157 range.setEnd( text, j + 1 );
\r
158 rect = range.getBoundingClientRect();
\r
160 if( rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom ){
\r
162 'hitRange' : range,
\r
174 range.setEnd( elm, l < tr.v2 ? l : tr.v2 );
\r
175 range.setStart( elm, tr.v1 );
\r
176 return { 'hitRange' : range };
\r
179 // !save && ( text = text.split( '\r\n' ).join( '\n' ) ); textarea用
\r
181 // ie11 の ie10モード で moveToPoint がないといわれる. よって isW3C:false で動作するのは ie9 以下
\r
182 range.moveToPoint( tr.v1, tr.v2 );
\r
183 if( !range.duplicate().expand( 'character' ) ) range = null;
\r
185 range.moveToElementText( elm );
\r
186 //range.collapse( true );
\r
187 range.moveEnd( 'character', l < tr.v2 ? l : tr.v2 );
\r
188 range.moveStart( 'character', tr.v1 );
\r
196 function X_TextRange_getRect(){
\r
197 var result = X_TextRange_getRawRange( this ),
\r
201 if( X_TextRange_isW3C ){
\r
202 if( result.hitRange ){
\r
203 rect = result.hitRange.getBoundingClientRect();
\r
207 'width' : rect.width,
\r
208 'height' : rect.height
\r
210 //range.detach && range.detach();
\r
214 'x' : result.boundingLeft,
\r
215 'y' : result.boundingTop,
\r
216 'width' : result.boundingWidth,
\r
217 'height' : result.boundingHeight // ie は right, bottom を持たない...
\r
221 return ret || { 'x' : 0, 'y' : 0, 'width' : 0, 'height' : 0 };
\r
224 // X.Text を探して flat な配列に格納する
\r
225 function X_TextRange_collectXTexts( xnode, ary ){
\r
226 var kids = xnode[ '_xnodes' ],
\r
229 if( !kids || !kids.length ) return;
\r
231 for( i = -1; xnode = kids[ ++i ]; ){
\r
232 if( xnode[ '_tag' ] ){
\r
233 X_TextRange_collectXTexts( xnode, ary );
\r
235 ary[ ary.length ] = xnode;
\r
240 function X_TextRange_getOffset(){
\r
241 var xnode = this.xnode,
\r
242 elm, result, range,
\r
243 ret, from, xtexts, n, i, l, xtext;
\r
245 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
246 elm = xnode[ '_rawObject' ];
\r
248 if( elm && xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){
\r
249 if( X_UA[ 'IE' ] < 9 ){
\r
252 return cursorPosition.call( this, elm );
\r
254 } else if( elm.setSelectionRange ){
\r
255 if( X_UA[ 'IE' ] < 12 ){
\r
256 l = elm.value.length;
\r
258 'from' : this.v1 = elm.selectionStart < l ? elm.selectionStart : l,
\r
259 'to' : this.v2 = elm.selectionEnd < l ? elm.selectionEnd : l
\r
263 'from' : this.v1 = elm.selectionStart,
\r
264 'to' : this.v2 = elm.selectionEnd
\r
270 if( result = X_TextRange_getRawRange( this ) ){
\r
271 if( X_TextRange_isW3C ){
\r
272 range = result.hitRange;
\r
274 'offset' : result.offset,
\r
275 'from' : this.v1 = range.startOffset,
\r
276 'to' : this.v2 = range.endOffset,
\r
277 'text' : X_Node_getXNode( result.text )
\r
279 // range.detach && range.detach();
\r
281 // http://www.studio-freesky.net/programming/javascript/3/
\r
283 range = X_TextRange_range2;
\r
284 range.moveToElementText( xnode[ '_rawObject' ] );
\r
285 range.setEndPoint( 'EndToStart', result );
\r
286 from = range.text.length;
\r
288 X_TextRange_collectXTexts( xnode, xtexts = [] );
\r
290 if( xtexts.length ){
\r
291 for( n = 0, i = -1; xtext = xtexts[ ++i ]; ){
\r
292 l = xtext[ '_rawObject' ].data.length;
\r
293 if( from < n + l ){
\r
300 'offset' : n, // elm の何個目の node か?
\r
301 'from' : this.v1 = from - n,
\r
302 'to' : this.v2 = from - n + result.text.length,
\r
309 return ret || { 'from' : -1, 'to' : -1 };
\r
312 function X_TextRange_text( v ){
\r
313 var xnode = this.xnode, elm, val, offset, from, to;
\r
315 if( v === undefined ){
\r
318 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
319 elm = xnode[ '_rawObject' ];
\r
320 val = X_UA[ 'IE' ] < 9 ? X_Node_Attr_getValueForIE( elm ) : elm.value;
\r
322 if( this.createFrom === 'char' ){
\r
324 'value' : val.substr( 0, this.v1 ) + v + val.substr( this.v2 )
\r
327 offset = this[ 'getOffset' ]();
\r
329 from = offset[ 'from' ];
\r
330 to = offset[ 'to' ];
\r
332 if( X_UA[ 'IE' ] < 9 ){
\r
333 range = document.selection.createRange();
\r
334 // TODO check textarea
\r
336 // ここには range.text がいない https://msdn.microsoft.com/ja-jp/library/cc427934.aspx
\r
338 val = val.substr( 0, from ) + v + val.substr( to );
\r
343 // カーソル位置を挿入した文字列の最後へ
\r
344 to = from + v.length;
\r
346 // カーソル位置を挿入した文字列の後ろへ
\r
349 this.move( to, to );
\r
355 function X_TextRange_move( from, to ){
\r
356 var xnode = this.xnode,
\r
357 elm = xnode[ '_rawObject' ],
\r
363 this.v1 = this.v1 + from;
\r
364 this.v1 < 0 && ( this.v1 = 0 );
\r
367 if( X_Type_isNumber( to ) ){
\r
371 this.v2 = this.v2 + to;
\r
372 this.v2 < this.v1 && ( this.v2 = this.v1 );
\r
376 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
377 // http://blog.enjoyxstudy.com/entry/20060305/p1
\r
379 if( X_UA[ 'IE' ] < 9 || X_UA[ 'Opera' ] ){
\r
380 len = ( X_UA[ 'IE' ] < 9 ? X_Node_Attr_getValueForIE( elm ) : elm.value ).length;
\r
382 if( X_UA[ 'Opera' ] ){
\r
383 X_EventDispatcher_ignoreActualEvent = 'focus';
\r
384 elm.focus(); // Operaの為(IEでは無くても大丈夫)
\r
385 X_EventDispatcher_ignoreActualEvent = '';
\r
388 range = elm.createTextRange();
\r
390 if( this.v1 === this.v2 && this.v1 === 0 ){
\r
391 range.collapse( true ); // 先頭に移動
\r
393 if( this.v1 !== this.v2 || this.v1 < len ){
\r
394 range.collapse(); // おまじない?
\r
396 if( this.v1 === this.v2 ){
\r
397 range.move( 'character', this.v1 );
\r
399 range.moveEnd( 'character', this.v2 );
\r
400 range.moveStart( 'character', this.v1 );
\r
403 range.collapse( false ); // 末美に移動
\r
408 } else if( elm.setSelectionRange ){
\r
409 elm.setSelectionRange( this.v1, this.v2 );
\r
414 function X_TextRange_select( v ){
\r
418 // http://www.studio-freesky.net/programming/javascript/3/
\r
419 // それは、IEのTextRangeオブジェクトで取得した範囲にもしラストに改行コード¥r¥nがあった場合それが含まれないのです。(視覚的な選択範囲には含まれています)
\r
421 // https://web.archive.org/web/20090904134938/http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
\r
422 // https://web.archive.org/web/20090904183807/http://www.dedestruct.com/cursorPosition.html
\r
423 function cursorPosition( textarea ){
\r
425 var selection_range = document.selection.createRange().duplicate();
\r
428 if (selection_range.parentElement() !== textarea) {
\r
429 // TODO 正しくはカーソル位置・選択範囲の復帰
\r
431 X_EventDispatcher_ignoreActualEvent = 'focus';
\r
433 X_EventDispatcher_ignoreActualEvent = '';
\r
435 // BODY要素のテキスト範囲を作成する
\r
436 selection_range = X_elmBody.createTextRange();
\r
438 // BODY要素のテキスト範囲をeのテキスト範囲に移動する
\r
439 // これはe.createTextRange()とほぼ同等
\r
440 selection_range.moveToElementText( textarea );
\r
442 selection_range.collapse( true ); // 末美に移動
\r
443 selection_range.select();
\r
446 //if (selection_range.parentElement() == textarea) {// Check that the selection is actually in our textarea
\r
447 // Create three ranges, one containing all the text before the selection,
\r
448 // one containing all the text in the selection (this already exists), and one containing all
\r
449 // the text after the selection.
\r
450 var before_range = X_elmBody.createTextRange();
\r
451 before_range.moveToElementText(textarea);
\r
452 // Selects all the text
\r
453 before_range.setEndPoint('EndToStart', selection_range);
\r
454 // Moves the end where we need it
\r
456 var after_range = X_elmBody.createTextRange();
\r
457 after_range.moveToElementText(textarea);
\r
458 // Selects all the text
\r
459 after_range.setEndPoint('StartToEnd', selection_range);
\r
460 // Moves the start where we need it
\r
462 var before_finished = false, selection_finished = false, after_finished = false;
\r
463 var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;
\r
465 // Load the text values we need to compare
\r
466 before_text = untrimmed_before_text = before_range.text;
\r
467 selection_text = untrimmed_selection_text = selection_range.text;
\r
468 after_text = untrimmed_after_text = after_range.text;
\r
470 // Check each range for trimmed newlines by shrinking the range by 1 character and seeing
\r
471 // if the text property has changed. If it has not changed then we know that IE has trimmed
\r
472 // a \r\n from the end.
\r
474 if (!before_finished) {
\r
475 if (before_range.compareEndPoints('StartToEnd', before_range) == 0) {
\r
476 before_finished = true;
\r
478 before_range.moveEnd('character', -1);
\r
479 if (before_range.text == before_text) {
\r
480 untrimmed_before_text += '\r\n';
\r
482 before_finished = true;
\r
486 if (!selection_finished) {
\r
487 if (selection_range.compareEndPoints('StartToEnd', selection_range) == 0) {
\r
488 selection_finished = true;
\r
490 selection_range.moveEnd('character', -1);
\r
491 if (selection_range.text == selection_text) {
\r
492 untrimmed_selection_text += '\r\n';
\r
494 selection_finished = true;
\r
498 if (!after_finished) {
\r
499 if (after_range.compareEndPoints('StartToEnd', after_range) == 0) {
\r
500 after_finished = true;
\r
502 after_range.moveEnd('character', -1);
\r
503 if (after_range.text == after_text) {
\r
504 untrimmed_after_text += '\r\n';
\r
506 after_finished = true;
\r
511 } while ((!before_finished || !selection_finished || !after_finished));
\r
513 // Untrimmed success test to make sure our results match what is actually in the textarea
\r
514 // This can be removed once you're confident it's working correctly
\r
516 var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
\r
517 var untrimmed_successful = false;
\r
518 if (textarea.value == untrimmed_text) {
\r
519 untrimmed_successful = true;
\r
521 // ** END Untrimmed success test
\r
523 var startPoint = untrimmed_before_text.split( '\r' ).join( '' ).length;
\r
524 // alert(startPoint);
\r
526 'from' : this.v1 = startPoint,
\r
527 'to' : this.v2 = startPoint + untrimmed_selection_text.split( '\r' ).join( '' ).length
\r