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
21 X_TextRange_selection,
\r
22 X_TextRange_isW3C = !document.selection || 9 <= X_UA[ 'IE' ] || X_UA[ 'Edge' ];
\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
51 if( !X_TextRange_selection ){
\r
52 X_TextRange_selection = X_TextRange_isW3C ? window.getSelection() : document.selection.createRange;
\r
56 this[ 'by' ] = arg2;
\r
63 if( arg2 !== 'selection' ){
\r
64 this[ 'v1' ] = arg3 || 0;
\r
65 this[ 'v2' ] = arg4 || 0;
\r
67 this[ 'getOffset' ]();
\r
71 'move' : X_TextRange_move,
\r
73 'select' : X_TextRange_select,
\r
75 'getRect' : X_TextRange_getRect,
\r
77 'getOffset' : X_TextRange_getOffset,
\r
79 'text' : X_TextRange_text
\r
83 // TextNode を探して flat な配列に格納する
\r
84 function X_TextRange_collectTextNodes( elm, ary ){
\r
85 var kids = elm.childNodes,
\r
88 if( !kids || !kids.length ) return;
\r
90 for( i = 0; e = kids[ i ]; ++i ){
\r
91 switch( e.nodeType ){
\r
93 X_TextRange_collectTextNodes( e, ary );
\r
96 ary[ ary.length ] = e;
\r
102 function X_TextRange_getRawRange( tr ){
\r
103 var xnode = tr.xnode,
\r
105 range = //10 <= X_UA[ 'IE' ] /* || X_UA[ 'iOS' ] */ ? document.createRange() :
\r
106 //8 <= X_UA[ 'IE' ] ? X_elmBody.createTextRange() :
\r
108 selection = X_TextRange_selection,
\r
110 texts, i, offset, j, l, x, y, rect, top, btm, left;
\r
112 if( xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){
\r
114 X_Node_updateTimerID && X_Node_startUpdate();
\r
116 elm = xnode[ '_rawObject' ];
\r
118 switch( tr[ 'by' ] ){
\r
120 if( X_TextRange_isW3C ){
\r
121 //selection = window.getSelection();
\r
123 if( selection.getRangeAt ){
\r
124 return selection.rangeCount && selection.getRangeAt( 0 );
\r
126 // http://d.hatena.ne.jp/dayflower/20080423/1208941641
\r
128 //range = document.createRange();
\r
129 range.setStart( selection.anchorNode, selection.anchorOffset );
\r
130 range.setEnd( selection.focusNode, selection.focusOffset );
\r
133 switch( document.selection.type ){
\r
135 return selection();
\r
144 if( X_TextRange_isW3C ){
\r
146 // TextNode をフラットな配列に回収
\r
147 X_TextRange_collectTextNodes( elm, texts = [] );
\r
152 for( i = offset = 0; text = texts[ i ]; ++i ){
\r
153 range.selectNodeContents( text ); // selectNodeContents は TextNode のみ?? Firefox
\r
154 l = text.data.length;
\r
156 for( j = 0; j < l; ++j ){
\r
157 if( X_UA[ 'IE' ] || X_UA[ 'Edge' ] ){
\r
158 // 改行の直前の文字を選択すると rect が巨大になってしまう
\r
159 range.setEnd( text, j );
\r
160 range.setStart( text, j );
\r
161 rect = range.getBoundingClientRect();
\r
165 range.setEnd( text, j + 1 );
\r
166 rect = range.getBoundingClientRect();
\r
168 if( rect.left < left ){
\r
169 //console.log( '= ', text.data.charAt( j ), ' x:', x, ' y:', y, ' top:', top | 0, ' left:', left | 0, ' bottom:', btm | 0, ' right:', rect.right | 0 );
\r
170 if( left <= x && x <= rect.right && top <= y && y <= btm ){
\r
172 'hitRange' : range, // TODO startContainer, endContainer
\r
175 'text' : text // TODO xtext じゃないの?
\r
181 range.setEnd( text, j + 1 );
\r
182 range.setStart( text, j );
\r
183 rect = range.getBoundingClientRect();
\r
186 //console.log( text.data.charAt( j ), ' x:', x, ' y:', y, ' top:', rect.top | 0, ' left:', rect.left | 0, ' bottom:', rect.bottom | 0, ' right:', rect.right | 0 );
\r
187 if( rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom ){
\r
189 'hitRange' : range, // TODO startContainer, endContainer
\r
192 'text' : text // TODO xtext じゃないの?
\r
200 // ie11 の ie10モード で moveToPoint がないといわれる. よって isW3C:false で動作するのは ie9 以下
\r
201 // 行の最後の文字の端をクリックすると次の行の文字が選択されてしまう ie8, ie7
\r
202 // 選択を移動して補正する https://msdn.microsoft.com/ja-jp/library/ms535872(v=vs.85).aspx
\r
203 range.moveToPoint( x = tr[ 'v1' ], y = tr[ 'v2' ] );
\r
205 // if( range.parentElement() !== elm || elm.contains( range.parentElement() ) ){
\r
207 if( range.expand( 'character' ) ){
\r
208 left = range.boundingLeft;
\r
209 top = range.boundingTop;
\r
210 if( x < left || left + range.boundingWidth < x || y < top || top + range.boundingHeight < y ){
\r
211 range.moveStart( 'character', -1 );
\r
212 range.moveEnd( 'character', -1 );
\r
213 left = range.boundingLeft;
\r
214 top = range.boundingTop;
\r
215 if( x < left || left + range.boundingWidth < x || y < top || top + range.boundingHeight < y ){
\r
226 if( X_TextRange_isW3C ){
\r
228 range.setEnd( elm, l < tr[ 'v2' ] ? l : tr[ 'v2' ] );
\r
229 range.setStart( elm, tr[ 'v1' ] );
\r
230 return { 'hitRange' : range };
\r
232 range.moveToElementText( elm );
\r
233 //range.collapse( true );
\r
234 range.moveEnd( 'character', l < tr[ 'v2' ] ? l : tr[ 'v2' ] );
\r
235 range.moveStart( 'character', tr[ 'v1' ] );
\r
242 function X_TextRange_getRect(){
\r
243 var result = X_TextRange_getRawRange( this ),
\r
247 if( X_TextRange_isW3C ){
\r
248 if( result.hitRange ){
\r
249 rect = result.hitRange.getBoundingClientRect();
\r
253 'width' : rect.width,
\r
254 'height' : rect.height
\r
256 //range.detach && range.detach();
\r
260 'x' : result.boundingLeft,
\r
261 'y' : result.boundingTop,
\r
262 'width' : result.boundingWidth,
\r
263 'height' : result.boundingHeight // ie は right, bottom を持たない...
\r
267 return ret || { 'x' : 0, 'y' : 0, 'width' : 0, 'height' : 0 };
\r
270 // X.Text を探して flat な配列に格納する
\r
271 function X_TextRange_collectXTexts( xnode, ary ){
\r
272 var kids = xnode[ '_xnodes' ],
\r
275 if( !kids || !kids.length ) return;
\r
277 for( i = -1; xnode = kids[ ++i ]; ){
\r
278 if( xnode[ '_tag' ] ){
\r
279 X_TextRange_collectXTexts( xnode, ary );
\r
281 ary[ ary.length ] = xnode;
\r
286 function X_TextRange_getOffset(){
\r
287 var xnode = this.xnode,
\r
288 elm, result, range,
\r
289 ret, from, xtexts, n, i, l, xtext;
\r
291 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
292 elm = xnode[ '_rawObject' ];
\r
294 if( elm && xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){
\r
295 if( X_UA[ 'IE' ] < 9 ){
\r
298 return cursorPosition.call( this, elm );
\r
300 } else if( elm.setSelectionRange ){
\r
301 if( X_UA[ 'IE' ] < 12 ){
\r
302 l = elm.value.length;
\r
304 'from' : this[ 'v1' ] = elm.selectionStart < l ? elm.selectionStart : l,
\r
305 'to' : this[ 'v2' ] = elm.selectionEnd < l ? elm.selectionEnd : l
\r
309 'from' : this[ 'v1' ] = elm.selectionStart,
\r
310 'to' : this[ 'v2' ] = elm.selectionEnd
\r
316 if( result = X_TextRange_getRawRange( this ) ){
\r
317 if( X_TextRange_isW3C ){
\r
318 range = result.hitRange;
\r
320 'offset' : result.offset,
\r
321 'from' : this[ 'v1' ] = range.startOffset,
\r
322 'to' : this[ 'v2' ] = range.endOffset,
\r
323 'text' : X_Node_getXNode( result.text )
\r
325 // range.detach && range.detach();
\r
327 // http://www.studio-freesky.net/programming/javascript/3/
\r
329 range = X_TextRange_range.duplicate();
\r
330 range.moveToElementText( xnode[ '_rawObject' ] );
\r
331 range.setEndPoint( 'EndToStart', result );
\r
332 //range.text && range.moveEnd( 'character', -1 );
\r
333 from = range.text.length;
\r
335 X_TextRange_collectXTexts( xnode, xtexts = [] );
\r
337 if( xtexts.length ){
\r
338 for( n = 0, i = -1; xtext = xtexts[ ++i ]; ){
\r
339 l = xtext[ '_rawObject' ].data.length;
\r
340 if( from < n + l ){
\r
347 'offset' : n, // elm の何個目の node か?
\r
348 'from' : this[ 'v1' ] = from - n,
\r
349 'to' : this[ 'v2' ] = from - n + result.text.length,
\r
356 return ret || { 'from' : -1, 'to' : -1 };
\r
359 function X_TextRange_text( v ){
\r
360 var xnode = this.xnode, elm, val, offset, from, to;
\r
362 if( v === undefined ){
\r
365 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
366 elm = xnode[ '_rawObject' ];
\r
367 val = X_UA[ 'IE' ] < 9 ? X_Node_Attr_getValueForIE( elm ) : elm.value;
\r
369 if( this[ 'by' ] === 'char' ){
\r
371 'value' : val.substr( 0, this[ 'v1' ] ) + v + val.substr( this[ 'v2' ] )
\r
374 offset = this[ 'getOffset' ]();
\r
376 from = offset[ 'from' ];
\r
377 to = offset[ 'to' ];
\r
379 if( X_UA[ 'IE' ] < 9 ){
\r
380 range = X_TextRange_selection();
\r
381 // TODO check textarea
\r
383 // ここには range.text がいない https://msdn.microsoft.com/ja-jp/library/cc427934.aspx
\r
385 val = val.substr( 0, from ) + v + val.substr( to );
\r
390 // カーソル位置を挿入した文字列の最後へ
\r
391 to = from + v.length;
\r
393 // カーソル位置を挿入した文字列の後ろへ
\r
396 this.move( to, to );
\r
402 function X_TextRange_move( from, to ){
\r
403 var xnode = this.xnode,
\r
404 elm = xnode[ '_rawObject' ],
\r
408 this[ 'v1' ] = from;
\r
410 this[ 'v1' ] = this[ 'v1' ] + from;
\r
411 this[ 'v1' ] < 0 && ( this[ 'v1' ] = 0 );
\r
414 if( X_Type_isNumber( to ) ){
\r
418 this[ 'v2' ] = this[ 'v2' ] + to;
\r
419 this[ 'v2' ] < this[ 'v1' ] && ( this[ 'v2' ] = this[ 'v1' ] );
\r
423 if( xnode[ '_tag' ] === 'TEXTAREA' ){
\r
424 // http://blog.enjoyxstudy.com/entry/20060305/p1
\r
426 if( X_UA[ 'IE' ] < 9 || X_UA[ 'Opera' ] ){
\r
427 len = ( X_UA[ 'IE' ] < 9 ? X_Node_Attr_getValueForIE( elm ) : elm.value ).length;
\r
429 if( X_UA[ 'Opera' ] ){
\r
430 FocusUtility_setTemporarilyFocus( elm );
\r
433 range = elm.createTextRange();
\r
435 if( this[ 'v1' ] === this[ 'v2' ] && this[ 'v1' ] === 0 ){
\r
436 range.collapse( true ); // 先頭に移動
\r
438 if( this[ 'v1' ] !== this[ 'v2' ] || this[ 'v1' ] < len ){
\r
439 range.collapse(); // おまじない?
\r
441 if( this[ 'v1' ] === this[ 'v2' ] ){
\r
442 range.move( 'character', this[ 'v1' ] );
\r
444 range.moveEnd( 'character', this[ 'v2' ] );
\r
445 range.moveStart( 'character', this[ 'v1' ] );
\r
448 range.collapse( false ); // 末美に移動
\r
453 } else if( elm.setSelectionRange ){
\r
454 elm.setSelectionRange( this[ 'v1' ], this[ 'v2' ] );
\r
459 function X_TextRange_select( v ){
\r
463 // http://www.studio-freesky.net/programming/javascript/3/
\r
464 // それは、IEのTextRangeオブジェクトで取得した範囲にもしラストに改行コード¥r¥nがあった場合それが含まれないのです。(視覚的な選択範囲には含まれています)
\r
466 // https://web.archive.org/web/20090904134938/http://www.dedestruct.com/2008/03/22/howto-cross-browser-cursor-position-in-textareas/
\r
467 // https://web.archive.org/web/20090904183807/http://www.dedestruct.com/cursorPosition.html
\r
468 function cursorPosition( textarea ){
\r
470 var selection_range = X_TextRange_selection().duplicate();
\r
473 if (selection_range.parentElement() !== textarea) {
\r
474 // TODO 正しくはカーソル位置・選択範囲の復帰
\r
476 FocusUtility_setTemporarilyFocus( textarea );
\r
478 // BODY要素のテキスト範囲を作成する
\r
479 selection_range = X_elmBody.createTextRange();
\r
481 // BODY要素のテキスト範囲をeのテキスト範囲に移動する
\r
482 // これはe.createTextRange()とほぼ同等
\r
483 selection_range.moveToElementText( textarea );
\r
485 selection_range.collapse( true ); // 末美に移動
\r
486 selection_range.select();
\r
489 //if (selection_range.parentElement() == textarea) {// Check that the selection is actually in our textarea
\r
490 // Create three ranges, one containing all the text before the selection,
\r
491 // one containing all the text in the selection (this already exists), and one containing all
\r
492 // the text after the selection.
\r
493 var before_range = X_elmBody.createTextRange();
\r
494 before_range.moveToElementText(textarea);
\r
495 // Selects all the text
\r
496 before_range.setEndPoint('EndToStart', selection_range);
\r
497 // Moves the end where we need it
\r
499 var after_range = X_elmBody.createTextRange();
\r
500 after_range.moveToElementText(textarea);
\r
501 // Selects all the text
\r
502 after_range.setEndPoint('StartToEnd', selection_range);
\r
503 // Moves the start where we need it
\r
505 var before_finished = false, selection_finished = false, after_finished = false;
\r
506 var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;
\r
508 // Load the text values we need to compare
\r
509 before_text = untrimmed_before_text = before_range.text;
\r
510 selection_text = untrimmed_selection_text = selection_range.text;
\r
511 after_text = untrimmed_after_text = after_range.text;
\r
513 // Check each range for trimmed newlines by shrinking the range by 1 character and seeing
\r
514 // if the text property has changed. If it has not changed then we know that IE has trimmed
\r
515 // a \r\n from the end.
\r
517 if (!before_finished) {
\r
518 if (before_range.compareEndPoints('StartToEnd', before_range) == 0) {
\r
519 before_finished = true;
\r
521 before_range.moveEnd('character', -1);
\r
522 if (before_range.text == before_text) {
\r
523 untrimmed_before_text += '\r\n';
\r
525 before_finished = true;
\r
529 if (!selection_finished) {
\r
530 if (selection_range.compareEndPoints('StartToEnd', selection_range) == 0) {
\r
531 selection_finished = true;
\r
533 selection_range.moveEnd('character', -1);
\r
534 if (selection_range.text == selection_text) {
\r
535 untrimmed_selection_text += '\r\n';
\r
537 selection_finished = true;
\r
541 if (!after_finished) {
\r
542 if (after_range.compareEndPoints('StartToEnd', after_range) == 0) {
\r
543 after_finished = true;
\r
545 after_range.moveEnd('character', -1);
\r
546 if (after_range.text == after_text) {
\r
547 untrimmed_after_text += '\r\n';
\r
549 after_finished = true;
\r
554 } while ((!before_finished || !selection_finished || !after_finished));
\r
556 // Untrimmed success test to make sure our results match what is actually in the textarea
\r
557 // This can be removed once you're confident it's working correctly
\r
559 var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
\r
560 var untrimmed_successful = false;
\r
561 if (textarea.value == untrimmed_text) {
\r
562 untrimmed_successful = true;
\r
564 // ** END Untrimmed success test
\r
566 var startPoint = untrimmed_before_text.split( '\r' ).join( '' ).length;
\r
567 // alert(startPoint);
\r
569 'from' : this[ 'v1' ] = startPoint,
\r
570 'to' : this[ 'v2' ] = startPoint + untrimmed_selection_text.split( '\r' ).join( '' ).length
\r