OSDN Git Service

a2fc5520798be1256b7b8937897eadae1193465d
[pettanr/clientJs.git] / 0.6.x / js / 02_dom / 30_XTextRange.js
1 /*\r
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
15  * tr.text()\r
16  * \r
17  * naming は mozilla に寄せる\r
18  */\r
19 \r
20 var X_TextRange_range,\r
21         X_TextRange_range2,\r
22         X_TextRange_isW3C = !document.selection || 10 <= X_UA[ 'IE' ];\r
23 \r
24 /**\r
25  * ユーザーによって選択されたテキストへの参照や文字の座標の取得\r
26  * @alias X.TextRange\r
27  * @class TextRange テキストレンジ\r
28  * @extends {__ClassBase__}\r
29  */\r
30 var X_TextRange = X_Class_create(\r
31         'X.TextRange',\r
32         \r
33         // TODO コールバックの最後に破棄されるクラス 1刻みの間存在するクラス. X.XML も\r
34         \r
35         /** @lends X.TextRange.prototype */\r
36         {\r
37                 xnode      : null,\r
38                 createFrom : '',\r
39                 v1         : 0,\r
40                 v2         : 0,\r
41                 \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
46                         };\r
47                         \r
48                         this.xnode = xnode;\r
49                         \r
50                         switch( arg2 ){\r
51                                 case 'selection' :\r
52                                         //break;\r
53                                 case 'point' :\r
54                                 case 'char' :\r
55                                         this.createFrom = arg2;\r
56                                         break;\r
57                                 default :\r
58                                         arg4 = arg3;\r
59                                         arg3 = arg2;\r
60                         };\r
61                         \r
62                         if( arg2 !== 'selection' ){\r
63                                 this.v1 = arg3 || 0;\r
64                                 this.v2 = arg4 || 0;\r
65                         } else {\r
66                                 this[ 'getOffset' ]();\r
67                         };\r
68                 },\r
69                 \r
70                 'move'      : X_TextRange_move,\r
71                 \r
72                 'select'    : X_TextRange_select,\r
73                 \r
74                 'getRect'   : X_TextRange_getRect,\r
75                 \r
76                 'getOffset' : X_TextRange_getOffset,\r
77                 \r
78                 'text'      : X_TextRange_text\r
79         }\r
80 );\r
81 \r
82 // TextNode を探して flat な配列に格納する\r
83 function X_TextRange_collectTextNodes( elm, ary ){\r
84         var kids = elm.childNodes,\r
85                 i, e;\r
86         \r
87         if( !kids || !kids.length ) return;\r
88         \r
89         for( i = 0; e = kids[ i ]; ++i ){\r
90                 switch( e.nodeType ){\r
91                         case 1 :\r
92                                 X_TextRange_collectTextNodes( e, ary );\r
93                                 break;\r
94                         case 3 :\r
95                                 ary[ ary.length ] = e;\r
96                                 break;\r
97                 };\r
98         };\r
99 };\r
100 \r
101 function X_TextRange_getRawRange( tr, createFrom ){\r
102         var xnode = tr.xnode,\r
103                                 //\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
108         \r
109         if( xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){\r
110                 \r
111                 X_Node_updateTimerID && X_Node_startUpdate();\r
112                 \r
113                 elm = xnode[ '_rawObject' ];\r
114                 \r
115                 switch( createFrom || tr.createFrom ){\r
116                         case 'selection' :\r
117                                 if( X_TextRange_isW3C ){\r
118                                         selection = window.getSelection();\r
119                                         \r
120                             if( selection.getRangeAt ){\r
121                                 return selection.rangeCount && selection.getRangeAt( 0 );\r
122                             };\r
123                             // http://d.hatena.ne.jp/dayflower/20080423/1208941641\r
124                             // for Safari 1.3\r
125                             range = document.createRange();\r
126                             range.setStart( selection.anchorNode, selection.anchorOffset );\r
127                             range.setEnd( selection.focusNode, selection.focusOffset );\r
128                                         return range;\r
129                                 } else {\r
130                                         switch( document.selection.type ){\r
131                                                 case 'text' :\r
132                                                         return document.selection.createRange();\r
133                                                 case 'Control' :\r
134                                                         // TODO\r
135                                                 case 'none' :\r
136                                         };\r
137                                 };\r
138                                 break;\r
139 \r
140                         case 'point' :\r
141                                 isPoint = true;\r
142                         case 'char' :\r
143                                 if( X_TextRange_isW3C ){\r
144                                         // textarea で異なる\r
145 \r
146                                         if( isPoint ){\r
147                                                 // TextNode をフラットな配列に回収\r
148                                                 X_TextRange_collectTextNodes( elm, texts = [] );                                                \r
149                                                 \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
153 \r
154                                                 for( j = 0, x = tr.v1, y = tr.v2; j < l; ++j ){\r
155                                                         if( range ){\r
156                                                             range.setStart( text, j );\r
157                                                             range.setEnd( text, j + 1 );\r
158                                                             rect = range.getBoundingClientRect();\r
159                                                         };\r
160                                                     if( rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom ){\r
161                                                         return {\r
162                                                                 'hitRange' : range,\r
163                                                                 'rect'     : rect,\r
164                                                                 'offset'   : offset,\r
165                                                                 'text'     : text\r
166                                                         };\r
167                                                     };\r
168                                                 };\r
169                                                 offset += l;\r
170                                                 };\r
171                                                 range = null;\r
172                                         } else {\r
173                                                 // 未チェック!\r
174                                                 range.setEnd( elm, l < tr.v2 ? l : tr.v2 );\r
175                                     range.setStart( elm, tr.v1 );\r
176                                     return { 'hitRange' : range };\r
177                                         };\r
178                                 } else {\r
179                                         // !save && ( text = text.split( '\r\n' ).join( '\n' ) ); textarea用\r
180                                         if( isPoint ){\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
184                                         } else {\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
189                                         };\r
190                                 };\r
191                                 return range;\r
192                 };\r
193         };\r
194 };\r
195 \r
196 function X_TextRange_getRect(){\r
197         var result = X_TextRange_getRawRange( this ),\r
198                 rect, ret;\r
199         \r
200         if( result ){\r
201                 if( X_TextRange_isW3C ){\r
202                         if( result.hitRange ){\r
203                                 rect = result.hitRange.getBoundingClientRect();\r
204                                 ret = {\r
205                                         'x'      : rect.left,\r
206                                         'y'      : rect.top,\r
207                                         'width'  : rect.width,\r
208                                         'height' : rect.height\r
209                                 };\r
210                                 //range.detach && range.detach();\r
211                         };\r
212                 } else {\r
213                         ret = {\r
214                                 'x'      : result.boundingLeft,\r
215                                 'y'      : result.boundingTop,\r
216                                 'width'  : result.boundingWidth,\r
217                                 'height' : result.boundingHeight // ie は right, bottom を持たない...\r
218                         };\r
219                 };\r
220         };\r
221         return ret || { 'x' : 0, 'y' : 0, 'width' : 0, 'height' : 0 };\r
222 };\r
223 \r
224 // X.Text を探して flat な配列に格納する\r
225 function X_TextRange_collectXTexts( xnode, ary ){\r
226         var kids = xnode[ '_xnodes' ],\r
227                 i;\r
228         \r
229         if( !kids || !kids.length ) return;\r
230         \r
231         for( i = -1; xnode = kids[ ++i ]; ){\r
232                 if( xnode[ '_tag' ] ){\r
233                         X_TextRange_collectXTexts( xnode, ary );\r
234                 } else {\r
235                         ary[ ary.length ] = xnode;\r
236                 };\r
237         };\r
238 };\r
239 \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
244         \r
245         if( xnode[ '_tag' ] === 'TEXTAREA' ){\r
246                 elm = xnode[ '_rawObject' ];\r
247                 \r
248                 if( elm && xnode[ '_flags' ] & X_NodeFlags_IN_TREE ){\r
249                         if( X_UA[ 'IE' ] < 9 ){\r
250                                 \r
251 \r
252                                 return cursorPosition.call( this, elm );\r
253 \r
254                         } else if( elm.setSelectionRange ){\r
255                                 if( X_UA[ 'IE' ] < 12 ){\r
256                                         l = elm.value.length;\r
257                                         ret = {\r
258                                                 'from' : this.v1 = elm.selectionStart < l ? elm.selectionStart : l,\r
259                                                 'to'   : this.v2 = elm.selectionEnd   < l ? elm.selectionEnd   : l\r
260                                         };\r
261                                 } else {\r
262                                         ret = {\r
263                                                 'from' : this.v1 = elm.selectionStart,\r
264                                                 'to'   : this.v2 = elm.selectionEnd\r
265                                         };      \r
266                                 };\r
267                         };\r
268                 };\r
269         } else\r
270         if( result = X_TextRange_getRawRange( this ) ){\r
271                 if( X_TextRange_isW3C ){\r
272                         range = result.hitRange;\r
273                         ret = {\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
278                         };\r
279                         // range.detach && range.detach();              \r
280                 } else {\r
281                         // http://www.studio-freesky.net/programming/javascript/3/\r
282                         \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
287                         \r
288                         X_TextRange_collectXTexts( xnode, xtexts = [] );\r
289                         \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
294                                                 break;\r
295                                         };\r
296                                         n += l;\r
297                                 };\r
298 \r
299                                 ret = {\r
300                                         'offset' : n, // elm の何個目の node か?\r
301                                         'from'   : this.v1 = from - n,\r
302                                         'to'     : this.v2 = from - n + result.text.length,\r
303                                         'text'   : xtext\r
304                                 };                              \r
305                         };\r
306                 };\r
307         };\r
308         \r
309         return ret || { 'from' : -1, 'to' : -1 };\r
310 };\r
311 \r
312 function X_TextRange_text( v ){\r
313         var xnode = this.xnode, elm, val, offset, from, to;\r
314         \r
315         if( v === undefined ){\r
316                 \r
317         } else {\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
321                         \r
322                         if( this.createFrom === 'char' ){\r
323                                 xnode.attr( {\r
324                                         'value' : val.substr( 0, this.v1 ) + v + val.substr( this.v2 )\r
325                                 } );\r
326                         } else {\r
327                                 offset = this[ 'getOffset' ]();\r
328                                 \r
329                                 from   = offset[ 'from' ];\r
330                                 to     = offset[ 'to' ];\r
331 \r
332                                 if( X_UA[ 'IE' ] < 9 ){\r
333                                         range = document.selection.createRange();\r
334                                         // TODO check textarea\r
335                                         range.text = v;\r
336                                         // ここには range.text がいない https://msdn.microsoft.com/ja-jp/library/cc427934.aspx\r
337                                 } else {\r
338                                         val = val.substr( 0, from ) + v + val.substr( to );\r
339                                         elm.value = val;\r
340                                 };\r
341                                         \r
342                                 if( to !== from ){\r
343                                         // カーソル位置を挿入した文字列の最後へ\r
344                                         to = from + v.length;\r
345                                 } else {\r
346                                         // カーソル位置を挿入した文字列の後ろへ\r
347                                         to += v.length;\r
348                                 };\r
349                                 this.move( to, to );\r
350                         };\r
351                 };\r
352         };\r
353 };\r
354 \r
355 function X_TextRange_move( from, to ){\r
356         var xnode  = this.xnode,\r
357                 elm    = xnode[ '_rawObject' ],\r
358                 len, range;\r
359 \r
360         if( 0 <= from ){\r
361                 this.v1 = from;\r
362         } else {\r
363                 this.v1 = this.v1 + from;\r
364                 this.v1 < 0 && ( this.v1 = 0 );\r
365         };\r
366 \r
367         if( X_Type_isNumber( to ) ){\r
368                 if( 0 <= to ){\r
369                         this.v2 = to;\r
370                 } else {\r
371                         this.v2 = this.v2 + to;\r
372                         this.v2 < this.v1 && ( this.v2 = this.v1 );\r
373                 };\r
374         };\r
375         \r
376         if( xnode[ '_tag' ] === 'TEXTAREA' ){\r
377                 // http://blog.enjoyxstudy.com/entry/20060305/p1\r
378                 \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
381                         \r
382                         if( X_UA[ 'Opera' ] ){\r
383                                 X_EventDispatcher_ignoreActualEvent = 'focus';\r
384                                 elm.focus(); // Operaの為(IEでは無くても大丈夫)\r
385                                 X_EventDispatcher_ignoreActualEvent = '';\r
386                         };\r
387 \r
388                         range = elm.createTextRange();\r
389 \r
390                         if( this.v1 === this.v2 && this.v1 === 0 ){\r
391                                 range.collapse( true ); // 先頭に移動\r
392                         } else {\r
393                                 if( this.v1 !== this.v2 || this.v1 < len ){\r
394                                         range.collapse(); // おまじない?\r
395 \r
396                                         if( this.v1 === this.v2 ){\r
397                                                 range.move( 'character', this.v1 );\r
398                                         } else {\r
399                                                 range.moveEnd( 'character', this.v2 );\r
400                                                 range.moveStart( 'character', this.v1 );\r
401                                         };\r
402                                 } else {\r
403                                         range.collapse( false ); // 末美に移動\r
404                                 };\r
405                         };\r
406                         range.select();\r
407 \r
408                 } else if( elm.setSelectionRange ){\r
409                         elm.setSelectionRange( this.v1, this.v2 );\r
410                 };\r
411         };\r
412 };\r
413 \r
414 function X_TextRange_select( v ){\r
415         \r
416 };\r
417 \r
418 // http://www.studio-freesky.net/programming/javascript/3/\r
419 // それは、IEのTextRangeオブジェクトで取得した範囲にもしラストに改行コード¥r¥nがあった場合それが含まれないのです。(視覚的な選択範囲には含まれています)\r
420 \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
424 \r
425                                                 var selection_range = document.selection.createRange().duplicate();\r
426                                                 \r
427 \r
428                                                 if (selection_range.parentElement() !== textarea) {\r
429                                                         // TODO 正しくはカーソル位置・選択範囲の復帰\r
430                                                         \r
431                                                         X_EventDispatcher_ignoreActualEvent = 'focus';\r
432                                                         textarea.focus();\r
433                                                         X_EventDispatcher_ignoreActualEvent = '';\r
434                                                                 \r
435                                                         // BODY要素のテキスト範囲を作成する\r
436                                                         selection_range = X_elmBody.createTextRange();\r
437                                                 \r
438                                                         // BODY要素のテキスト範囲をeのテキスト範囲に移動する\r
439                                                         // これはe.createTextRange()とほぼ同等\r
440                                                         selection_range.moveToElementText( textarea );\r
441                                                         \r
442                                                         selection_range.collapse( true ); // 末美に移動\r
443                                                         selection_range.select();\r
444                                                 };\r
445 \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
455                                                 \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
461                                                 \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
464                                                 \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
469                                                 \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
473                                                         do {\r
474                                                                 if (!before_finished) {\r
475                                                                         if (before_range.compareEndPoints('StartToEnd', before_range) == 0) {\r
476                                                                                 before_finished = true;\r
477                                                                         } else {\r
478                                                                                 before_range.moveEnd('character', -1);\r
479                                                                                 if (before_range.text == before_text) {\r
480                                                                                         untrimmed_before_text += '\r\n';\r
481                                                                                 } else {\r
482                                                                                         before_finished = true;\r
483                                                                                 }\r
484                                                                         }\r
485                                                                 }\r
486                                                                 if (!selection_finished) {\r
487                                                                         if (selection_range.compareEndPoints('StartToEnd', selection_range) == 0) {\r
488                                                                                 selection_finished = true;\r
489                                                                         } else {\r
490                                                                                 selection_range.moveEnd('character', -1);\r
491                                                                                 if (selection_range.text == selection_text) {\r
492                                                                                         untrimmed_selection_text += '\r\n';\r
493                                                                                 } else {\r
494                                                                                         selection_finished = true;\r
495                                                                                 }\r
496                                                                         }\r
497                                                                 }\r
498                                                                 if (!after_finished) {\r
499                                                                         if (after_range.compareEndPoints('StartToEnd', after_range) == 0) {\r
500                                                                                 after_finished = true;\r
501                                                                         } else {\r
502                                                                                 after_range.moveEnd('character', -1);\r
503                                                                                 if (after_range.text == after_text) {\r
504                                                                                         untrimmed_after_text += '\r\n';\r
505                                                                                 } else {\r
506                                                                                         after_finished = true;\r
507                                                                                 }\r
508                                                                         }\r
509                                                                 }\r
510                                                 \r
511                                                         } while ((!before_finished || !selection_finished || !after_finished));\r
512                                                 \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
515                                                         /*\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
520                                                         } */\r
521                                                         // ** END Untrimmed success test\r
522                                                 \r
523                                                         var startPoint = untrimmed_before_text.split( '\r' ).join( '' ).length;\r
524                                                         // alert(startPoint);\r
525                                                         return {\r
526                                                                 'from'   : this.v1 = startPoint,\r
527                                                                 'to'     : this.v2 = startPoint + untrimmed_selection_text.split( '\r' ).join( '' ).length\r
528                                                         };\r
529                                                 //}\r
530                                         }\r
531                                         \r