annotate js/tinymce/atomic/editor_plugin_src.js @ 29:37ca57016cbe

Added TinyMCE atomic selection plugin.
author David Eads <eads@chicagotech.org>
date Tue, 17 Mar 2009 10:59:50 -0500
parents
children
rev   line source
eads@29 1 /**
eads@29 2 *
eads@29 3 * @author Sander Kruger
eads@29 4 * @copyright Copyright � 2009, Sander Kruger, All rights reserved.
eads@29 5 */
eads@29 6
eads@29 7 (function() {
eads@29 8 var Event = tinymce.dom.Event;
eads@29 9 var direction = 0; // Track cursor direction for skipping
eads@29 10 var hasClicked = false; // Track mouseclicks for selecting
eads@29 11 var atStart = null; // Cache if the cursor is at the start or at the end of
eads@29 12 var atEnd = null; // an atom.
eads@29 13
eads@29 14 tinymce.create('tinymce.plugins.AtomicPlugin', {
eads@29 15 init : function(ed, url) {
eads@29 16 var t = this, atomicClass;
eads@29 17
eads@29 18 t.editor = ed;
eads@29 19 atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic");
eads@29 20
eads@29 21 ed.onNodeChange.addToTop(function(ed, cm, n) {
eads@29 22 var sc, ec, wasEnd=false;
eads@29 23
eads@29 24 // Check if the start or end of the selection is in an atom
eads@29 25 sc = ed.dom.getParent(ed.selection.getStart(), function(n) {
eads@29 26 return ed.dom.hasClass(n, atomicClass);
eads@29 27 });
eads@29 28
eads@29 29 ec = ed.dom.getParent(ed.selection.getEnd(), function(n) {
eads@29 30 return ed.dom.hasClass(n, atomicClass);
eads@29 31 });
eads@29 32 if (atEnd)
eads@29 33 {
eads@29 34 wasEnd = true;
eads@29 35 }
eads@29 36 atStart = null;
eads@29 37 atEnd = null;
eads@29 38 // Check situation to move the cursor or selection.
eads@29 39 if (sc || ec)
eads@29 40 {
eads@29 41 var s = ed.selection.getSel();
eads@29 42 var r = ed.selection.getRng();
eads@29 43 var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
eads@29 44 var move = false;
eads@29 45 if (!select)
eads@29 46 {
eads@29 47 // The user has not selected anything but is moving the
eads@29 48 // cursor or clicked on an atom.
eads@29 49 if (direction == -1)
eads@29 50 {
eads@29 51 if (r.setStart)
eads@29 52 {
eads@29 53 var lc = t._leftNeighbour( ed, sc );
eads@29 54 if (lc)
eads@29 55 {
eads@29 56 // Make sure that when skipping to the left,
eads@29 57 // the next input doesn't go into the atom.
eads@29 58 var offset = lc.nodeType==3?lc.length:1;
eads@29 59 r.setStart( lc, offset );
eads@29 60 r.setEnd( lc, offset );
eads@29 61 }
eads@29 62 else
eads@29 63 {
eads@29 64 r.setStart( sc, 0 );
eads@29 65 r.setEnd( sc, 0 );
eads@29 66 }
eads@29 67 atStart = sc;
eads@29 68 move = true;
eads@29 69 }
eads@29 70 else
eads@29 71 {
eads@29 72 // ie (check if the caret is not just after the
eads@29 73 // atom)
eads@29 74 var r2 = ed.getBody().createTextRange();
eads@29 75 r2.moveToElementText( ec );
eads@29 76 r2.collapse(false);
eads@29 77 if (!r2.isEqual( r ))
eads@29 78 {
eads@29 79 r.moveToElementText( sc );
eads@29 80 r.collapse();
eads@29 81 atStart = sc;
eads@29 82 move = true;
eads@29 83 }
eads@29 84 else
eads@29 85 {
eads@29 86 var tempEl = ed.dom.create("span", null, "&shy;");
eads@29 87 ec.insertAdjacentElement("afterEnd", tempEl);
eads@29 88
eads@29 89 r.moveToElementText(tempEl);
eads@29 90 r.collapse(false);
eads@29 91 ed.selection.setRng(r);
eads@29 92
eads@29 93 ed.dom.remove(tempEl);
eads@29 94 atEnd = ec;
eads@29 95 }
eads@29 96 }
eads@29 97 }
eads@29 98 else if (direction == 1)
eads@29 99 {
eads@29 100 if (r.setStartAfter)
eads@29 101 {
eads@29 102 r.setStartAfter( ec );
eads@29 103 r.setEndAfter( ec );
eads@29 104 move = true;
eads@29 105 }
eads@29 106 else
eads@29 107 {
eads@29 108 if (wasEnd)
eads@29 109 {
eads@29 110 r.move( 'character' );
eads@29 111 ed.selection.setRng(r);
eads@29 112 ec = null;
eads@29 113 }
eads@29 114 else
eads@29 115 {
eads@29 116 var tempEl = ed.dom.create("span", null, "&shy;");
eads@29 117 ec.insertAdjacentElement("afterEnd", tempEl);
eads@29 118
eads@29 119 r.moveToElementText(tempEl);
eads@29 120 r.collapse(false);
eads@29 121 ed.selection.setRng(r);
eads@29 122
eads@29 123 ed.dom.remove(tempEl);
eads@29 124 }
eads@29 125 }
eads@29 126 atEnd = ec;
eads@29 127 }
eads@29 128 else if (hasClicked)
eads@29 129 {
eads@29 130 if (r.setStart)
eads@29 131 {
eads@29 132 r.setStart( sc, 0 );
eads@29 133 r.setEnd( ec, ec.childNodes.length );
eads@29 134 }
eads@29 135 else
eads@29 136 {
eads@29 137 // ie
eads@29 138 r.moveToElementText( sc );
eads@29 139 }
eads@29 140 move = true;
eads@29 141 }
eads@29 142 else if (wasEnd && !r.setStart)
eads@29 143 {
eads@29 144 // ie
eads@29 145 var tempEl = ed.dom.create("span", null, "&shy;");
eads@29 146 ec.insertAdjacentElement("afterEnd", tempEl);
eads@29 147
eads@29 148 r.moveToElementText(tempEl);
eads@29 149 r.collapse(false);
eads@29 150 ed.selection.setRng(r);
eads@29 151
eads@29 152 ed.dom.remove(tempEl);
eads@29 153 atEnd = ec;
eads@29 154 }
eads@29 155 }
eads@29 156 else
eads@29 157 {
eads@29 158 // Reverse selections have the anchor at the end and the
eads@29 159 // focus at the start. This happens when selecting
eads@29 160 // backwards. Make sure the extended selection keeps the
eads@29 161 // same direction.
eads@29 162 // *** Issue: ie doesn't support backward selections
eads@29 163 // programmatically (or at least I have not found how to
eads@29 164 // do this).
eads@29 165 var reverse = false;
eads@29 166 if (s.anchorNode && s.anchorNode == r.endContainer && s.anchorOffset == r.endOffset)
eads@29 167 {
eads@29 168 reverse = true;
eads@29 169 }
eads@29 170 var moveStart = false;
eads@29 171 var moveEnd = false;
eads@29 172 var rsc, rec;
eads@29 173 if (sc)
eads@29 174 {
eads@29 175 if (s.anchorNode)
eads@29 176 {
eads@29 177 if (sc != r.startContainer || (r.startOffset != 0 && r.startOffset != sc.childNodes.length))
eads@29 178 {
eads@29 179 // The start pos is not where it should be.
eads@29 180 moveStart = true;
eads@29 181 }
eads@29 182 }
eads@29 183 else
eads@29 184 {
eads@29 185 // ie
eads@29 186 rsc = ed.getBody().createTextRange();
eads@29 187 rsc.moveToElementText( sc );
eads@29 188 moveStart = (r.compareEndPoints( "StartToStart", rsc ) != 0);
eads@29 189 }
eads@29 190 }
eads@29 191 if (ec)
eads@29 192 {
eads@29 193 if (s.anchorNode)
eads@29 194 {
eads@29 195 if (ec != r.endContainer || (r.endOffset != 0 && r.endOffset != ec.childNodes.length))
eads@29 196 {
eads@29 197 // The end pos is not where it should be
eads@29 198 moveEnd = true;
eads@29 199 }
eads@29 200 }
eads@29 201 else
eads@29 202 {
eads@29 203 // ie
eads@29 204 rec = ed.getBody().createTextRange();
eads@29 205 rec.moveToElementText( ec );
eads@29 206 moveEnd = (r.compareEndPoints( "EndToEnd", rec ) != 0);
eads@29 207 }
eads@29 208 }
eads@29 209 if (reverse)
eads@29 210 {
eads@29 211 // Start with the end. Since 'selection direction detection'
eads@29 212 // doesn't work on IE, don't bother with the IE code here.
eads@29 213 if (moveEnd)
eads@29 214 {
eads@29 215 // If both start and end move and they move leftwards, set start first and then extend instead of starting with setend
eads@29 216 if (direction == -1)
eads@29 217 {
eads@29 218 r.setEnd( ec, 0 );
eads@29 219 }
eads@29 220 else
eads@29 221 {
eads@29 222 r.setEnd( ec, ec.childNodes.length );
eads@29 223 }
eads@29 224 move = true;
eads@29 225 }
eads@29 226 if (moveStart)
eads@29 227 {
eads@29 228 // Use the extend method to advance the focus
eads@29 229 // position of the selection.
eads@29 230 if (direction == 1)
eads@29 231 {
eads@29 232 s.extend( sc, sc.childNodes.length );
eads@29 233 move = false;
eads@29 234 }
eads@29 235 else
eads@29 236 {
eads@29 237 s.extend( sc, 0 );
eads@29 238 move = false;
eads@29 239 }
eads@29 240 }
eads@29 241 }
eads@29 242 else
eads@29 243 {
eads@29 244 if (moveStart)
eads@29 245 {
eads@29 246 if (direction == 1)
eads@29 247 {
eads@29 248 if (r.setStart)
eads@29 249 {
eads@29 250 r.setStart( sc, sc.childNodes.length );
eads@29 251 }
eads@29 252 else
eads@29 253 {
eads@29 254 // ie
eads@29 255 r.setEndPoint( "StartToEnd", rsc );
eads@29 256 }
eads@29 257 }
eads@29 258 else
eads@29 259 {
eads@29 260 if (r.setStart)
eads@29 261 {
eads@29 262 r.setStart( sc, 0 );
eads@29 263 }
eads@29 264 else
eads@29 265 {
eads@29 266 // ie
eads@29 267 r.setEndPoint( "StartToStart", rsc );
eads@29 268 }
eads@29 269 }
eads@29 270 move = true;
eads@29 271 }
eads@29 272 if (moveEnd)
eads@29 273 {
eads@29 274 if (direction == -1)
eads@29 275 {
eads@29 276 if (r.setEnd)
eads@29 277 {
eads@29 278 s.extend( ec, 0 );
eads@29 279 }
eads@29 280 else
eads@29 281 {
eads@29 282 // ie
eads@29 283 r.setEndPoint( "EndToStart", rec );
eads@29 284 move = true;
eads@29 285 }
eads@29 286 }
eads@29 287 else
eads@29 288 {
eads@29 289 if (r.setEnd)
eads@29 290 {
eads@29 291 s.extend( ec, ec.childNodes.length );
eads@29 292 }
eads@29 293 else
eads@29 294 {
eads@29 295 // ie
eads@29 296 r.setEndPoint( "EndToEnd", rec );
eads@29 297 move = true;
eads@29 298 }
eads@29 299 }
eads@29 300 // move = false;
eads@29 301 }
eads@29 302 }
eads@29 303 }
eads@29 304 if (move)
eads@29 305 {
eads@29 306 // Make sure other modules can react to this event.
eads@29 307 ed.selection.setRng( r );
eads@29 308 }
eads@29 309 }
eads@29 310 });
eads@29 311
eads@29 312 ed.onKeyDown.addToTop( function(ed, e) {
eads@29 313 var k = e.keyCode, atomicClass;
eads@29 314 atomicClass = ed.getParam("atomic_atomic_class", "mceAtomic");
eads@29 315 hasClicked = false;
eads@29 316 direction = 0;
eads@29 317 if (k == 37 || k == 38)
eads@29 318 {
eads@29 319 direction = -1;
eads@29 320 }
eads@29 321 else if (k == 39 || k == 40)
eads@29 322 {
eads@29 323 direction = 1;
eads@29 324 }
eads@29 325 else if (k == 8)
eads@29 326 {
eads@29 327 // Check if an atom is being 'backspace'd
eads@29 328 if (!atEnd)
eads@29 329 {
eads@29 330 var s = ed.selection.getSel();
eads@29 331 var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
eads@29 332 if (!select)
eads@29 333 {
eads@29 334 var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != 0)?null:s.focusNode):ed.selection.getStart();
eads@29 335 if (ed.dom.hasClass(ep, atomicClass))
eads@29 336 {
eads@29 337 atEnd = ep;
eads@29 338 }
eads@29 339 else if (ep)
eads@29 340 {
eads@29 341 var rc;
eads@29 342 if (!s.focusNode)
eads@29 343 {
eads@29 344 // ie
eads@29 345 // focusOffset is not given. So, we need to find
eads@29 346 // it by walking through the children and comparing
eads@29 347 // end points.
eads@29 348 var r = ed.selection.getRng();
eads@29 349 var r2 = ed.getBody().createTextRange();
eads@29 350 r2.moveToElementText( ep );
eads@29 351 if (r2.compareEndPoints( "StartToStart", r ) == 0)
eads@29 352 {
eads@29 353 // The caret is at the start of this element.
eads@29 354 // Find the left neighbouring element
eads@29 355 rc = t._leftNeighbour( ed, ep );
eads@29 356 }
eads@29 357 else
eads@29 358 {
eads@29 359 // The focus is not at the start of the parent.
eads@29 360 // Find the child left of the caret.
eads@29 361 rc = ep.firstChild;
eads@29 362 while (rc)
eads@29 363 {
eads@29 364 // We are not interested in text nodes
eads@29 365 // because they cannot be atoms (all by
eads@29 366 // themselves)
eads@29 367 if (rc.nodeType == 1)
eads@29 368 {
eads@29 369 r2.moveToElementText( rc );
eads@29 370 if (r.compareEndPoints( "EndToEnd", r2 ) == 0)
eads@29 371 {
eads@29 372 break;
eads@29 373 }
eads@29 374 }
eads@29 375 rc = rc.nextSibling;
eads@29 376 }
eads@29 377 }
eads@29 378 }
eads@29 379 else
eads@29 380 {
eads@29 381 if (ep.nodeType == 1 && s.focusOffset != 0)
eads@29 382 {
eads@29 383 // ep encloses the current caret position.
eads@29 384 // select the child to the left of the caret.
eads@29 385 rc = ep.childNodes[s.focusOffset-1];
eads@29 386 }
eads@29 387 else
eads@29 388 {
eads@29 389 // The caret is on the left-most end of ep.
eads@29 390 // Find it's left neighbour.
eads@29 391 rc = t._leftNeighbour( ed, ep );
eads@29 392 }
eads@29 393 }
eads@29 394 if (rc)
eads@29 395 {
eads@29 396 // Now see if this element has a right-most
eads@29 397 // descendent that is an atom
eads@29 398 while (rc && !ed.dom.hasClass(rc, atomicClass))
eads@29 399 {
eads@29 400 rc = rc.lastChild;
eads@29 401 }
eads@29 402 if (rc)
eads@29 403 {
eads@29 404 atEnd = rc;
eads@29 405 }
eads@29 406 }
eads@29 407 }
eads@29 408 }
eads@29 409 }
eads@29 410 if (atEnd)
eads@29 411 {
eads@29 412 ed.dom.remove( atEnd );
eads@29 413 atEnd = null;
eads@29 414 return Event.cancel(e);
eads@29 415 }
eads@29 416 }
eads@29 417 else if (k == 46)
eads@29 418 {
eads@29 419 // Check if an atom is being 'delete'd
eads@29 420 if (!atStart)
eads@29 421 {
eads@29 422 var s = ed.selection.getSel();
eads@29 423 var select = s.type?(s.type!="None"):(s.anchorNode != s.focusNode || s.anchorOffset != s.focusOffset);
eads@29 424 if (!select)
eads@29 425 {
eads@29 426 var ep = s.focusNode?((s.focusNode.nodeType == 3 && s.focusOffset != s.focusNode.length)?null:s.focusNode):ed.selection.getEnd();
eads@29 427 if (ed.dom.hasClass(ep, atomicClass))
eads@29 428 {
eads@29 429 atStart = ep;
eads@29 430 }
eads@29 431 else if (ep)
eads@29 432 {
eads@29 433 var lc;
eads@29 434 if (!s.focusNode)
eads@29 435 {
eads@29 436 // ie
eads@29 437 // focusOffset is not given. So, we need to find
eads@29 438 // it by walking through the children and comparing
eads@29 439 // end points.
eads@29 440 var r = ed.selection.getRng();
eads@29 441 var r2 = ed.getBody().createTextRange();
eads@29 442 r2.moveToElementText( ep );
eads@29 443 if (r2.compareEndPoints( "EndToEnd", r ) == 0)
eads@29 444 {
eads@29 445 // The caret is at the start of this element.
eads@29 446 // Find the right neighbouring element
eads@29 447 lc = t._rightNeighbour( ed, ep );
eads@29 448 }
eads@29 449 else
eads@29 450 {
eads@29 451 // The focus is not at the end of the parent.
eads@29 452 // Find the child right of the caret.
eads@29 453 lc = ep.firstChild;
eads@29 454 while (lc)
eads@29 455 {
eads@29 456 // We are not interested in text nodes
eads@29 457 // because they cannot be atoms (all by
eads@29 458 // themselves)
eads@29 459 if (lc.nodeType == 1)
eads@29 460 {
eads@29 461 r2.moveToElementText( lc );
eads@29 462 if (r.compareEndPoints( "StartToStart", r2 ) == 0)
eads@29 463 {
eads@29 464 break;
eads@29 465 }
eads@29 466 }
eads@29 467 lc = lc.nextSibling;
eads@29 468 }
eads@29 469 }
eads@29 470 }
eads@29 471 else
eads@29 472 {
eads@29 473 if (ep.nodeType == 1 && s.focusOffset != ep.childNodes.length)
eads@29 474 {
eads@29 475 // ep encloses the current caret position.
eads@29 476 // select the child to the right of the caret.
eads@29 477 lc = ep.childNodes[s.focusOffset];
eads@29 478 }
eads@29 479 else
eads@29 480 {
eads@29 481 // The caret is on the right-most end of ep.
eads@29 482 // Find it's right neighbour.
eads@29 483 lc = t._rightNeighbour( ed, ep );
eads@29 484 }
eads@29 485 }
eads@29 486 if (lc)
eads@29 487 {
eads@29 488 // Now see if this element has a left-most
eads@29 489 // descendent that is an atom
eads@29 490 while (lc && !ed.dom.hasClass(lc, atomicClass))
eads@29 491 {
eads@29 492 lc = lc.firstChild;
eads@29 493 }
eads@29 494 if (lc)
eads@29 495 {
eads@29 496 atStart = lc;
eads@29 497 }
eads@29 498 }
eads@29 499 }
eads@29 500 }
eads@29 501 }
eads@29 502 if (atStart)
eads@29 503 {
eads@29 504 ed.dom.remove( atStart );
eads@29 505 atStart = null;
eads@29 506 return Event.cancel(e);
eads@29 507 }
eads@29 508 }
eads@29 509 });
eads@29 510
eads@29 511 ed.onMouseDown.addToTop(t._onClick);
eads@29 512 ed.onMouseUp.addToTop(t._onClick);
eads@29 513 },
eads@29 514
eads@29 515 getInfo : function() {
eads@29 516 return {
eads@29 517 longname : 'Atomic elements',
eads@29 518 author : 'Sander Kruger',
eads@29 519 authorurl : 'http://www.3gsp.eu',
eads@29 520 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/atomic',
eads@29 521 version : tinymce.majorVersion + "." + tinymce.minorVersion
eads@29 522 };
eads@29 523 },
eads@29 524
eads@29 525 _onClick : function(ed, e) {
eads@29 526 direction = 0;
eads@29 527 hasClicked = true;
eads@29 528 atStart = null;
eads@29 529 atEnd = null;
eads@29 530 },
eads@29 531
eads@29 532 // Helper methods
eads@29 533 _leftNeighbour : function( ed, e ) {
eads@29 534 var l = ed.dom.getParent(e, function(n) {
eads@29 535 return e.previousSibling != null;
eads@29 536 });
eads@29 537 if (l)
eads@29 538 {
eads@29 539 return l.previousSibling;
eads@29 540 }
eads@29 541 return null;
eads@29 542 },
eads@29 543
eads@29 544 _rightNeighbour : function( ed, e ) {
eads@29 545 var r = ed.dom.getParent(e, function(n) {
eads@29 546 return e.nextSibling != null;
eads@29 547 });
eads@29 548 if (r)
eads@29 549 {
eads@29 550 return r.nextSibling;
eads@29 551 }
eads@29 552 return null;
eads@29 553 }
eads@29 554 });
eads@29 555
eads@29 556 // Register plugin
eads@29 557 tinymce.PluginManager.add('atomic', tinymce.plugins.AtomicPlugin);
eads@29 558 })();