[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 /* 2 YUI 3.17.2 (build 9c3c78e) 3 Copyright 2014 Yahoo! Inc. All rights reserved. 4 Licensed under the BSD License. 5 http://yuilibrary.com/license/ 6 */ 7 8 YUI.add('editor-selection', function (Y, NAME) { 9 10 /** 11 * Wraps some common Selection/Range functionality into a simple object 12 * @class EditorSelection 13 * @constructor 14 * @module editor 15 * @submodule selection 16 */ 17 18 //TODO This shouldn't be there, Y.Node doesn't normalize getting textnode content. 19 var textContent = 'textContent', 20 INNER_HTML = 'innerHTML', 21 FONT_FAMILY = 'fontFamily'; 22 23 if (Y.UA.ie && Y.UA.ie < 11) { 24 textContent = 'nodeValue'; 25 } 26 27 Y.EditorSelection = function(domEvent) { 28 var sel, par, ieNode, nodes, rng, i, 29 comp, moved = 0, n, id, root = Y.EditorSelection.ROOT; 30 31 32 if (Y.config.win.getSelection && (!Y.UA.ie || Y.UA.ie < 9 || Y.UA.ie > 10)) { 33 sel = Y.config.win.getSelection(); 34 } else if (Y.config.doc.selection) { 35 sel = Y.config.doc.selection.createRange(); 36 } 37 this._selection = sel; 38 39 if (!sel) { 40 return false; 41 } 42 43 if (sel.pasteHTML) { 44 this.isCollapsed = (sel.compareEndPoints('StartToEnd', sel)) ? false : true; 45 if (this.isCollapsed) { 46 this.anchorNode = this.focusNode = Y.one(sel.parentElement()); 47 48 if (domEvent) { 49 ieNode = Y.config.doc.elementFromPoint(domEvent.clientX, domEvent.clientY); 50 } 51 rng = sel.duplicate(); 52 if (!ieNode) { 53 par = sel.parentElement(); 54 nodes = par.childNodes; 55 56 for (i = 0; i < nodes.length; i++) { 57 //This causes IE to not allow a selection on a doubleclick 58 //rng.select(nodes[i]); 59 if (rng.inRange(sel)) { 60 if (!ieNode) { 61 ieNode = nodes[i]; 62 } 63 } 64 } 65 } 66 67 this.ieNode = ieNode; 68 69 if (ieNode) { 70 if (ieNode.nodeType !== 3) { 71 if (ieNode.firstChild) { 72 ieNode = ieNode.firstChild; 73 } 74 if (root.compareTo(ieNode)) { 75 if (ieNode.firstChild) { 76 ieNode = ieNode.firstChild; 77 } 78 } 79 } 80 this.anchorNode = this.focusNode = Y.EditorSelection.resolve(ieNode); 81 82 rng.moveToElementText(sel.parentElement()); 83 comp = sel.compareEndPoints('StartToStart', rng); 84 if (comp) { 85 //We are not at the beginning of the selection. 86 //Setting the move to something large, may need to increase it later 87 moved = this.getEditorOffset(root); 88 sel.move('character', -(moved)); 89 } 90 91 this.anchorOffset = this.focusOffset = moved; 92 93 this.anchorTextNode = this.focusTextNode = Y.one(ieNode); 94 } 95 96 97 } else { 98 //This helps IE deal with a selection and nodeChange events 99 if (sel.htmlText && sel.htmlText !== '') { 100 n = Y.Node.create(sel.htmlText); 101 if (n && n.get('id')) { 102 id = n.get('id'); 103 this.anchorNode = this.focusNode = Y.one('#' + id); 104 } else if (n) { 105 n = n.get('childNodes'); 106 this.anchorNode = this.focusNode = n.item(0); 107 } 108 } 109 } 110 111 //var self = this; 112 //debugger; 113 } else { 114 this.isCollapsed = sel.isCollapsed; 115 this.anchorNode = Y.EditorSelection.resolve(sel.anchorNode); 116 this.focusNode = Y.EditorSelection.resolve(sel.focusNode); 117 this.anchorOffset = sel.anchorOffset; 118 this.focusOffset = sel.focusOffset; 119 120 this.anchorTextNode = Y.one(sel.anchorNode || this.anchorNode); 121 this.focusTextNode = Y.one(sel.focusNode || this.focusNode); 122 } 123 if (Y.Lang.isString(sel.text)) { 124 this.text = sel.text; 125 } else { 126 if (sel.toString) { 127 this.text = sel.toString(); 128 } else { 129 this.text = ''; 130 } 131 } 132 }; 133 134 /** 135 * Utility method to remove dead font-family styles from an element. 136 * @static 137 * @method removeFontFamily 138 */ 139 Y.EditorSelection.removeFontFamily = function(n) { 140 n.removeAttribute('face'); 141 var s = n.getAttribute('style').toLowerCase(); 142 if (s === '' || (s === 'font-family: ')) { 143 n.removeAttribute('style'); 144 } 145 if (s.match(Y.EditorSelection.REG_FONTFAMILY)) { 146 s = s.replace(Y.EditorSelection.REG_FONTFAMILY, ''); 147 n.setAttribute('style', s); 148 } 149 }; 150 151 /** 152 * Performs a prefilter on all nodes in the editor. Looks for nodes with a style: fontFamily or font face 153 * It then creates a dynamic class assigns it and removed the property. This is so that we don't lose 154 * the fontFamily when selecting nodes. 155 * @static 156 * @method filter 157 */ 158 Y.EditorSelection.filter = function(blocks) { 159 Y.log('Filtering nodes', 'info', 'editor-selection'); 160 161 var startTime = (new Date()).getTime(), 162 editorSelection = Y.EditorSelection, 163 root = editorSelection.ROOT, 164 endTime, 165 nodes = root.all(editorSelection.ALL), 166 baseNodes = root.all('strong,em'), 167 doc = Y.config.doc, hrs, 168 classNames = {}, cssString = '', 169 ls, startTime1 = (new Date()).getTime(), 170 endTime1; 171 172 nodes.each(function(n) { 173 var raw = Y.Node.getDOMNode(n); 174 if (raw.style[FONT_FAMILY]) { 175 classNames['.' + n._yuid] = raw.style[FONT_FAMILY]; 176 n.addClass(n._yuid); 177 178 editorSelection.removeFontFamily(raw); 179 } 180 }); 181 endTime1 = (new Date()).getTime(); 182 Y.log('Node Filter Timer: ' + (endTime1 - startTime1) + 'ms', 'info', 'editor-selection'); 183 184 root.all('.hr').addClass('yui-skip').addClass('yui-non'); 185 186 if (Y.UA.ie) { 187 hrs = Y.Node.getDOMNode(root).getElementsByTagName('hr'); 188 Y.each(hrs, function(hr) { 189 var el = doc.createElement('div'), 190 s = el.style; 191 192 el.className = 'hr yui-non yui-skip'; 193 194 el.setAttribute('readonly', true); 195 el.setAttribute('contenteditable', false); //Keep it from being Edited 196 if (hr.parentNode) { 197 hr.parentNode.replaceChild(el, hr); 198 } 199 //Had to move to inline style. writes for ie's < 8. They don't render el.setAttribute('style'); 200 s.border = '1px solid #ccc'; 201 s.lineHeight = '0'; 202 s.height = '0'; 203 s.fontSize = '0'; 204 s.marginTop = '5px'; 205 s.marginBottom = '5px'; 206 s.marginLeft = '0px'; 207 s.marginRight = '0px'; 208 s.padding = '0'; 209 }); 210 } 211 212 213 Y.each(classNames, function(v, k) { 214 cssString += k + ' { font-family: ' + v.replace(/"/gi, '') + '; }'; 215 }); 216 Y.StyleSheet(cssString, 'editor'); 217 218 219 //Not sure about this one? 220 baseNodes.each(function(n, k) { 221 var t = n.get('tagName').toLowerCase(), 222 newTag = 'i'; 223 if (t === 'strong') { 224 newTag = 'b'; 225 } 226 editorSelection.prototype._swap(baseNodes.item(k), newTag); 227 }); 228 229 //Filter out all the empty UL/OL's 230 ls = root.all('ol,ul'); 231 ls.each(function(v) { 232 var lis = v.all('li'); 233 if (!lis.size()) { 234 v.remove(); 235 } 236 }); 237 238 if (blocks) { 239 editorSelection.filterBlocks(); 240 } 241 endTime = (new Date()).getTime(); 242 Y.log('Filter Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection'); 243 }; 244 245 /** 246 * Method attempts to replace all "orphined" text nodes in the main body by wrapping them with a <p>. Called from filter. 247 * @static 248 * @method filterBlocks 249 */ 250 Y.EditorSelection.filterBlocks = function() { 251 Y.log('RAW filter blocks', 'info', 'editor-selection'); 252 var startTime = (new Date()).getTime(), endTime, 253 childs = Y.Node.getDOMNode(Y.EditorSelection.ROOT).childNodes, i, node, wrapped = false, doit = true, 254 sel, single, br, c, s, html; 255 256 if (childs) { 257 for (i = 0; i < childs.length; i++) { 258 node = Y.one(childs[i]); 259 if (!node.test(Y.EditorSelection.BLOCKS)) { 260 doit = true; 261 if (childs[i].nodeType === 3) { 262 c = childs[i][textContent].match(Y.EditorSelection.REG_CHAR); 263 s = childs[i][textContent].match(Y.EditorSelection.REG_NON); 264 if (c === null && s) { 265 doit = false; 266 267 } 268 } 269 if (doit) { 270 if (!wrapped) { 271 wrapped = []; 272 } 273 wrapped.push(childs[i]); 274 } 275 } else { 276 wrapped = Y.EditorSelection._wrapBlock(wrapped); 277 } 278 } 279 wrapped = Y.EditorSelection._wrapBlock(wrapped); 280 } 281 282 single = Y.all(Y.EditorSelection.DEFAULT_BLOCK_TAG); 283 if (single.size() === 1) { 284 Y.log('Only One default block tag (' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '), focus it..', 'info', 'editor-selection'); 285 br = single.item(0).all('br'); 286 if (br.size() === 1) { 287 if (!br.item(0).test('.yui-cursor')) { 288 br.item(0).remove(); 289 } 290 html = single.item(0).get('innerHTML'); 291 if (html === '' || html === ' ') { 292 Y.log('Paragraph empty, focusing cursor', 'info', 'editor-selection'); 293 single.set('innerHTML', Y.EditorSelection.CURSOR); 294 sel = new Y.EditorSelection(); 295 sel.focusCursor(true, true); 296 } 297 if (br.item(0).test('.yui-cursor') && Y.UA.ie) { 298 br.item(0).remove(); 299 } 300 } 301 } else { 302 single.each(function(p) { 303 var html = p.get('innerHTML'); 304 if (html === '') { 305 Y.log('Empty Paragraph Tag Found, Removing It', 'info', 'editor-selection'); 306 p.remove(); 307 } 308 }); 309 } 310 311 endTime = (new Date()).getTime(); 312 Y.log('FilterBlocks Timer: ' + (endTime - startTime) + 'ms', 'info', 'editor-selection'); 313 }; 314 315 /** 316 * Regular Expression used to find dead font-family styles 317 * @static 318 * @property REG_FONTFAMILY 319 */ 320 Y.EditorSelection.REG_FONTFAMILY = /font-family:\s*;/; 321 322 /** 323 * Regular Expression to determine if a string has a character in it 324 * @static 325 * @property REG_CHAR 326 */ 327 Y.EditorSelection.REG_CHAR = /[a-zA-Z-0-9_!@#\$%\^&*\(\)-=_+\[\]\\{}|;':",.\/<>\?]/gi; 328 329 /** 330 * Regular Expression to determine if a string has a non-character in it 331 * @static 332 * @property REG_NON 333 */ 334 Y.EditorSelection.REG_NON = /[\s|\n|\t]/gi; 335 336 /** 337 * Regular Expression to remove all HTML from a string 338 * @static 339 * @property REG_NOHTML 340 */ 341 Y.EditorSelection.REG_NOHTML = /<\S[^><]*>/g; 342 343 344 /** 345 * Wraps an array of elements in a Block level tag 346 * @static 347 * @private 348 * @method _wrapBlock 349 */ 350 Y.EditorSelection._wrapBlock = function(wrapped) { 351 if (wrapped) { 352 var newChild = Y.Node.create('<' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '></' + Y.EditorSelection.DEFAULT_BLOCK_TAG + '>'), 353 firstChild = Y.one(wrapped[0]), i; 354 355 for (i = 1; i < wrapped.length; i++) { 356 newChild.append(wrapped[i]); 357 } 358 firstChild.replace(newChild); 359 newChild.prepend(firstChild); 360 } 361 return false; 362 }; 363 364 /** 365 * Undoes what filter does enough to return the HTML from the Editor, then re-applies the filter. 366 * @static 367 * @method unfilter 368 * @return {String} The filtered HTML 369 */ 370 Y.EditorSelection.unfilter = function() { 371 var root = Y.EditorSelection.ROOT, 372 nodes = root.all('[class]'), 373 html = '', nons, ids, 374 body = root; 375 376 Y.log('UnFiltering nodes', 'info', 'editor-selection'); 377 378 nodes.each(function(n) { 379 if (n.hasClass(n._yuid)) { 380 //One of ours 381 n.setStyle(FONT_FAMILY, n.getStyle(FONT_FAMILY)); 382 n.removeClass(n._yuid); 383 if (n.getAttribute('class') === '') { 384 n.removeAttribute('class'); 385 } 386 } 387 }); 388 389 nons = root.all('.yui-non'); 390 nons.each(function(n) { 391 if (!n.hasClass('yui-skip') && n.get('innerHTML') === '') { 392 n.remove(); 393 } else { 394 n.removeClass('yui-non').removeClass('yui-skip'); 395 } 396 }); 397 398 ids = root.all('[id]'); 399 ids.each(function(n) { 400 if (n.get('id').indexOf('yui_3_') === 0) { 401 n.removeAttribute('id'); 402 n.removeAttribute('_yuid'); 403 } 404 }); 405 406 if (body) { 407 html = body.get('innerHTML'); 408 } 409 410 root.all('.hr').addClass('yui-skip').addClass('yui-non'); 411 412 /* 413 nodes.each(function(n) { 414 n.addClass(n._yuid); 415 n.setStyle(FONT_FAMILY, ''); 416 if (n.getAttribute('style') === '') { 417 n.removeAttribute('style'); 418 } 419 }); 420 */ 421 422 return html; 423 }; 424 /** 425 * Resolve a node from the selection object and return a Node instance 426 * @static 427 * @method resolve 428 * @param {HTMLElement} n The HTMLElement to resolve. Might be a TextNode, gives parentNode. 429 * @return {Node} The Resolved node 430 */ 431 Y.EditorSelection.resolve = function(n) { 432 if (!n) { 433 return Y.EditorSelection.ROOT; 434 } 435 436 if (n && n.nodeType === 3) { 437 //Adding a try/catch here because in rare occasions IE will 438 //Throw a error accessing the parentNode of a stranded text node. 439 //In the case of Ctrl+Z (Undo) 440 try { 441 n = n.parentNode; 442 } catch (re) { 443 n = Y.EditorSelection.ROOT; 444 } 445 } 446 return Y.one(n); 447 }; 448 449 /** 450 * Returns the innerHTML of a node with all HTML tags removed. 451 * @static 452 * @method getText 453 * @param {Node} node The Node instance to remove the HTML from 454 * @return {String} The string of text 455 */ 456 Y.EditorSelection.getText = function(node) { 457 var txt = node.get('innerHTML').replace(Y.EditorSelection.REG_NOHTML, ''); 458 //Clean out the cursor subs to see if the Node is empty 459 txt = txt.replace('<span><br></span>', '').replace('<br>', ''); 460 return txt; 461 }; 462 463 //Y.EditorSelection.DEFAULT_BLOCK_TAG = 'div'; 464 Y.EditorSelection.DEFAULT_BLOCK_TAG = 'p'; 465 466 /** 467 * The selector to use when looking for Nodes to cache the value of: [style],font[face] 468 * @static 469 * @property ALL 470 */ 471 Y.EditorSelection.ALL = '[style],font[face]'; 472 473 /** 474 * The selector to use when looking for block level items. 475 * @static 476 * @property BLOCKS 477 */ 478 Y.EditorSelection.BLOCKS = 'p,div,ul,ol,table,style'; 479 /** 480 * The temporary fontname applied to a selection to retrieve their values: yui-tmp 481 * @static 482 * @property TMP 483 */ 484 Y.EditorSelection.TMP = 'yui-tmp'; 485 /** 486 * The default tag to use when creating elements: span 487 * @static 488 * @property DEFAULT_TAG 489 */ 490 Y.EditorSelection.DEFAULT_TAG = 'span'; 491 492 /** 493 * The id of the outer cursor wrapper 494 * @static 495 * @property CURID 496 */ 497 Y.EditorSelection.CURID = 'yui-cursor'; 498 499 /** 500 * The id used to wrap the inner space of the cursor position 501 * @static 502 * @property CUR_WRAPID 503 */ 504 Y.EditorSelection.CUR_WRAPID = 'yui-cursor-wrapper'; 505 506 /** 507 * The default HTML used to focus the cursor.. 508 * @static 509 * @property CURSOR 510 */ 511 Y.EditorSelection.CURSOR = '<span><br class="yui-cursor"></span>'; 512 513 /** 514 * The default HTML element from which data will be retrieved. Default: body 515 * @static 516 * @property ROOT 517 */ 518 Y.EditorSelection.ROOT = Y.one('body'); 519 520 Y.EditorSelection.hasCursor = function() { 521 var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID); 522 Y.log('Has Cursor: ' + cur.size(), 'info', 'editor-selection'); 523 return cur.size(); 524 }; 525 526 /** 527 * Called from Editor keydown to remove the "extra" space before the cursor. 528 * @static 529 * @method cleanCursor 530 */ 531 Y.EditorSelection.cleanCursor = function() { 532 //Y.log('Cleaning Cursor', 'info', 'Selection'); 533 var cur, sel = 'br.yui-cursor'; 534 cur = Y.all(sel); 535 if (cur.size()) { 536 cur.each(function(b) { 537 var c = b.get('parentNode.parentNode.childNodes'), html; 538 if (c.size()) { 539 b.remove(); 540 } else { 541 html = Y.EditorSelection.getText(c.item(0)); 542 if (html !== '') { 543 b.remove(); 544 } 545 } 546 }); 547 } 548 /* 549 var cur = Y.all('#' + Y.EditorSelection.CUR_WRAPID); 550 if (cur.size()) { 551 cur.each(function(c) { 552 var html = c.get('innerHTML'); 553 if (html == ' ' || html == '<br>') { 554 if (c.previous() || c.next()) { 555 c.remove(); 556 } 557 } 558 }); 559 } 560 */ 561 }; 562 563 Y.EditorSelection.prototype = { 564 /** 565 * Range text value 566 * @property text 567 * @type String 568 */ 569 text: null, 570 /** 571 * Flag to show if the range is collapsed or not 572 * @property isCollapsed 573 * @type Boolean 574 */ 575 isCollapsed: null, 576 /** 577 * A Node instance of the parentNode of the anchorNode of the range 578 * @property anchorNode 579 * @type Node 580 */ 581 anchorNode: null, 582 /** 583 * The offset from the range object 584 * @property anchorOffset 585 * @type Number 586 */ 587 anchorOffset: null, 588 /** 589 * A Node instance of the actual textNode of the range. 590 * @property anchorTextNode 591 * @type Node 592 */ 593 anchorTextNode: null, 594 /** 595 * A Node instance of the parentNode of the focusNode of the range 596 * @property focusNode 597 * @type Node 598 */ 599 focusNode: null, 600 /** 601 * The offset from the range object 602 * @property focusOffset 603 * @type Number 604 */ 605 focusOffset: null, 606 /** 607 * A Node instance of the actual textNode of the range. 608 * @property focusTextNode 609 * @type Node 610 */ 611 focusTextNode: null, 612 /** 613 * The actual Selection/Range object 614 * @property _selection 615 * @private 616 */ 617 _selection: null, 618 /** 619 * Wrap an element, with another element 620 * @private 621 * @method _wrap 622 * @param {HTMLElement} n The node to wrap 623 * @param {String} tag The tag to use when creating the new element. 624 * @return {HTMLElement} The wrapped node 625 */ 626 _wrap: function(n, tag) { 627 var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); 628 tmp.set(INNER_HTML, n.get(INNER_HTML)); 629 n.set(INNER_HTML, ''); 630 n.append(tmp); 631 return Y.Node.getDOMNode(tmp); 632 }, 633 /** 634 * Swap an element, with another element 635 * @private 636 * @method _swap 637 * @param {HTMLElement} n The node to swap 638 * @param {String} tag The tag to use when creating the new element. 639 * @return {HTMLElement} The new node 640 */ 641 _swap: function(n, tag) { 642 var tmp = Y.Node.create('<' + tag + '></' + tag + '>'); 643 tmp.set(INNER_HTML, n.get(INNER_HTML)); 644 n.replace(tmp, n); 645 return Y.Node.getDOMNode(tmp); 646 }, 647 /** 648 * Get all the nodes in the current selection. This method will actually perform a filter first. 649 * Then it calls doc.execCommand('fontname', null, 'yui-tmp') to touch all nodes in the selection. 650 * The it compiles a list of all nodes affected by the execCommand and builds a NodeList to return. 651 * @method getSelected 652 * @return {NodeList} A NodeList of all items in the selection. 653 */ 654 getSelected: function() { 655 var editorSelection = Y.EditorSelection, 656 root = editorSelection.ROOT, 657 nodes, 658 items = []; 659 660 editorSelection.filter(); 661 Y.config.doc.execCommand('fontname', null, editorSelection.TMP); 662 nodes = root.all(editorSelection.ALL); 663 664 nodes.each(function(n, k) { 665 if (n.getStyle(FONT_FAMILY) === editorSelection.TMP) { 666 n.setStyle(FONT_FAMILY, ''); 667 editorSelection.removeFontFamily(n); 668 if (!n.compareTo(root)) { 669 items.push(Y.Node.getDOMNode(nodes.item(k))); 670 } 671 } 672 }); 673 return Y.all(items); 674 }, 675 /** 676 * Insert HTML at the current cursor position and return a Node instance of the newly inserted element. 677 * @method insertContent 678 * @param {String} html The HTML to insert. 679 * @return {Node} The inserted Node. 680 */ 681 insertContent: function(html) { 682 return this.insertAtCursor(html, this.anchorTextNode, this.anchorOffset, true); 683 }, 684 /** 685 * Insert HTML at the current cursor position, this method gives you control over the text node to insert into and the offset where to put it. 686 * @method insertAtCursor 687 * @param {String} html The HTML to insert. 688 * @param {Node} node The text node to break when inserting. 689 * @param {Number} offset The left offset of the text node to break and insert the new content. 690 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false 691 * @return {Node} The inserted Node. 692 */ 693 insertAtCursor: function(html, node, offset, collapse) { 694 var cur = Y.Node.create('<' + Y.EditorSelection.DEFAULT_TAG + ' class="yui-non"></' + Y.EditorSelection.DEFAULT_TAG + '>'), 695 inHTML, txt, txt2, newNode, range = this.createRange(), b, root = Y.EditorSelection.ROOT; 696 697 if (root.compareTo(node)) { 698 b = Y.Node.create('<span></span>'); 699 node.append(b); 700 node = b; 701 } 702 703 704 if (range.pasteHTML) { 705 if (offset === 0 && node && !node.previous() && node.get('nodeType') === 3) { 706 /* 707 * For some strange reason, range.pasteHTML fails if the node is a textNode and 708 * the offset is 0. (The cursor is at the beginning of the line) 709 * It will always insert the new content at position 1 instead of 710 * position 0. Here we test for that case and do it the hard way. 711 */ 712 node.insert(html, 'before'); 713 if (range.moveToElementText) { 714 range.moveToElementText(Y.Node.getDOMNode(node.previous())); 715 } 716 //Move the cursor after the new node 717 range.collapse(false); 718 range.select(); 719 return node.previous(); 720 } else { 721 newNode = Y.Node.create(html); 722 try { 723 range.pasteHTML('<span id="rte-insert"></span>'); 724 } catch (e) {} 725 inHTML = root.one('#rte-insert'); 726 if (inHTML) { 727 inHTML.set('id', ''); 728 inHTML.replace(newNode); 729 if (range.moveToElementText) { 730 range.moveToElementText(Y.Node.getDOMNode(newNode)); 731 } 732 range.collapse(false); 733 range.select(); 734 return newNode; 735 } else { 736 Y.on('available', function() { 737 inHTML.set('id', ''); 738 inHTML.replace(newNode); 739 if (range.moveToElementText) { 740 range.moveToElementText(Y.Node.getDOMNode(newNode)); 741 } 742 range.collapse(false); 743 range.select(); 744 }, '#rte-insert'); 745 } 746 } 747 } else { 748 //TODO using Y.Node.create here throws warnings & strips first white space character 749 //txt = Y.one(Y.Node.create(inHTML.substr(0, offset))); 750 //txt2 = Y.one(Y.Node.create(inHTML.substr(offset))); 751 if (offset > 0) { 752 inHTML = node.get(textContent); 753 754 txt = Y.one(Y.config.doc.createTextNode(inHTML.substr(0, offset))); 755 txt2 = Y.one(Y.config.doc.createTextNode(inHTML.substr(offset))); 756 757 node.replace(txt, node); 758 newNode = Y.Node.create(html); 759 if (newNode.get('nodeType') === 11) { 760 b = Y.Node.create('<span></span>'); 761 b.append(newNode); 762 newNode = b; 763 } 764 txt.insert(newNode, 'after'); 765 //if (txt2 && txt2.get('length')) { 766 if (txt2) { 767 newNode.insert(cur, 'after'); 768 cur.insert(txt2, 'after'); 769 this.selectNode(cur, collapse); 770 } 771 } else { 772 if (node.get('nodeType') === 3) { 773 node = node.get('parentNode') || root; 774 } 775 newNode = Y.Node.create(html); 776 html = node.get('innerHTML').replace(/\n/gi, ''); 777 if (html === '' || html === '<br>') { 778 node.append(newNode); 779 } else { 780 if (newNode.get('parentNode')) { 781 node.insert(newNode, 'before'); 782 } else { 783 root.prepend(newNode); 784 } 785 } 786 if (node.get('firstChild').test('br')) { 787 node.get('firstChild').remove(); 788 } 789 } 790 } 791 return newNode; 792 }, 793 /** 794 * Get all elements inside a selection and wrap them with a new element and return a NodeList of all elements touched. 795 * @method wrapContent 796 * @param {String} tag The tag to wrap all selected items with. 797 * @return {NodeList} A NodeList of all items in the selection. 798 */ 799 wrapContent: function(tag) { 800 tag = (tag) ? tag : Y.EditorSelection.DEFAULT_TAG; 801 802 if (!this.isCollapsed) { 803 Y.log('Wrapping selection with: ' + tag, 'info', 'editor-selection'); 804 var items = this.getSelected(), 805 changed = [], range, last, first, range2; 806 807 items.each(function(n, k) { 808 var t = n.get('tagName').toLowerCase(); 809 if (t === 'font') { 810 changed.push(this._swap(items.item(k), tag)); 811 } else { 812 changed.push(this._wrap(items.item(k), tag)); 813 } 814 }, this); 815 816 range = this.createRange(); 817 first = changed[0]; 818 last = changed[changed.length - 1]; 819 if (this._selection.removeAllRanges) { 820 range.setStart(changed[0], 0); 821 range.setEnd(last, last.childNodes.length); 822 this._selection.removeAllRanges(); 823 this._selection.addRange(range); 824 } else { 825 if (range.moveToElementText) { 826 range.moveToElementText(Y.Node.getDOMNode(first)); 827 range2 = this.createRange(); 828 range2.moveToElementText(Y.Node.getDOMNode(last)); 829 range.setEndPoint('EndToEnd', range2); 830 } 831 range.select(); 832 } 833 834 changed = Y.all(changed); 835 Y.log('Returning NodeList with (' + changed.size() + ') item(s)' , 'info', 'editor-selection'); 836 return changed; 837 838 839 } else { 840 Y.log('Can not wrap a collapsed selection, use insertContent', 'error', 'editor-selection'); 841 return Y.all([]); 842 } 843 }, 844 /** 845 * Find and replace a string inside a text node and replace it with HTML focusing the node after 846 * to allow you to continue to type. 847 * @method replace 848 * @param {String} se The string to search for. 849 * @param {String} re The string of HTML to replace it with. 850 * @return {Node} The node inserted. 851 */ 852 replace: function(se,re) { 853 Y.log('replacing (' + se + ') with (' + re + ')'); 854 var range = this.createRange(), node, txt, index, newNode; 855 856 if (range.getBookmark) { 857 index = range.getBookmark(); 858 txt = this.anchorNode.get('innerHTML').replace(se, re); 859 this.anchorNode.set('innerHTML', txt); 860 range.moveToBookmark(index); 861 newNode = Y.one(range.parentElement()); 862 } else { 863 node = this.anchorTextNode; 864 txt = node.get(textContent); 865 index = txt.indexOf(se); 866 867 txt = txt.replace(se, ''); 868 node.set(textContent, txt); 869 newNode = this.insertAtCursor(re, node, index, true); 870 } 871 return newNode; 872 }, 873 /** 874 * Destroy the range. 875 * @method remove 876 * @chainable 877 * @return {EditorSelection} 878 */ 879 remove: function() { 880 if (this._selection && this._selection.removeAllRanges) { 881 this._selection.removeAllRanges(); 882 } 883 return this; 884 }, 885 /** 886 * Wrapper for the different range creation methods. 887 * @method createRange 888 * @return {Range} 889 */ 890 createRange: function() { 891 if (Y.config.doc.selection) { 892 return Y.config.doc.selection.createRange(); 893 } else { 894 return Y.config.doc.createRange(); 895 } 896 }, 897 /** 898 * Select a Node (hilighting it). 899 * @method selectNode 900 * @param {Node} node The node to select 901 * @param {Boolean} collapse Should the range be collapsed after insertion. default: false 902 * @chainable 903 * @return {EditorSelection} 904 */ 905 selectNode: function(node, collapse, end) { 906 if (!node) { 907 Y.log('Node passed to selectNode is null', 'error', 'editor-selection'); 908 return; 909 } 910 end = end || 0; 911 node = Y.Node.getDOMNode(node); 912 var range = this.createRange(); 913 if (range.selectNode) { 914 try { 915 range.selectNode(node); 916 } catch (err) { 917 // Ignore selection errors like INVALID_NODE_TYPE_ERR 918 } 919 this._selection.removeAllRanges(); 920 this._selection.addRange(range); 921 if (collapse) { 922 try { 923 this._selection.collapse(node, end); 924 } catch (err) { 925 this._selection.collapse(node, 0); 926 } 927 } 928 } else { 929 if (node.nodeType === 3) { 930 node = node.parentNode; 931 } 932 try { 933 range.moveToElementText(node); 934 } catch(e) {} 935 if (collapse) { 936 range.collapse(((end) ? false : true)); 937 } 938 range.select(); 939 } 940 return this; 941 }, 942 /** 943 * Put a placeholder in the DOM at the current cursor position. 944 * @method setCursor 945 * @return {Node} 946 */ 947 setCursor: function() { 948 this.removeCursor(false); 949 return this.insertContent(Y.EditorSelection.CURSOR); 950 }, 951 /** 952 * Get the placeholder in the DOM at the current cursor position. 953 * @method getCursor 954 * @return {Node} 955 */ 956 getCursor: function() { 957 return Y.EditorSelection.ROOT.all('.' + Y.EditorSelection.CURID); 958 }, 959 /** 960 * Remove the cursor placeholder from the DOM. 961 * @method removeCursor 962 * @param {Boolean} keep Setting this to true will keep the node, but remove the unique parts that make it the cursor. 963 * @return {Node} 964 */ 965 removeCursor: function(keep) { 966 var cur = this.getCursor(); 967 if (cur) { 968 if (keep) { 969 cur.removeAttribute('id'); 970 cur.set('innerHTML', '<br class="yui-cursor">'); 971 } else { 972 cur.remove(); 973 } 974 } 975 return cur; 976 }, 977 /** 978 * Gets a stored cursor and focuses it for editing, must be called sometime after setCursor 979 * @method focusCursor 980 * @return {Node} 981 */ 982 focusCursor: function(collapse, end) { 983 if (collapse !== false) { 984 collapse = true; 985 } 986 if (end !== false) { 987 end = true; 988 } 989 var cur = this.removeCursor(true); 990 if (cur) { 991 cur.each(function(c) { 992 this.selectNode(c, collapse, end); 993 }, this); 994 } 995 }, 996 /** 997 * Generic toString for logging. 998 * @method toString 999 * @return {String} 1000 */ 1001 toString: function() { 1002 return 'EditorSelection Object'; 1003 }, 1004 1005 /** 1006 Gets the offset of the selection for the selection within the current 1007 editor 1008 @public 1009 @method getEditorOffset 1010 @param {Y.Node} [node] Element used to measure the offset to 1011 @return Number Number of characters the selection is from the beginning 1012 @since 3.13.0 1013 */ 1014 getEditorOffset: function(node) { 1015 var container = (node || Y.EditorSelection.ROOT).getDOMNode(), 1016 caretOffset = 0, 1017 doc = Y.config.doc, 1018 win = Y.config.win, 1019 sel, 1020 range, 1021 preCaretRange; 1022 1023 if (typeof win.getSelection !== "undefined") { 1024 range = win.getSelection().getRangeAt(0); 1025 preCaretRange = range.cloneRange(); 1026 preCaretRange.selectNodeContents(container); 1027 preCaretRange.setEnd(range.endContainer, range.endOffset); 1028 caretOffset = preCaretRange.toString().length; 1029 } else { 1030 sel = doc.selection; 1031 1032 if ( sel && sel.type !== "Control") { 1033 range = sel.createRange(); 1034 preCaretRange = doc.body.createTextRange(); 1035 preCaretRange.moveToElementText(container); 1036 preCaretRange.setEndPoint("EndToEnd", range); 1037 caretOffset = preCaretRange.text.length; 1038 } 1039 } 1040 1041 return caretOffset; 1042 } 1043 }; 1044 1045 //TODO Remove this alias in 3.6.0 1046 Y.Selection = Y.EditorSelection; 1047 1048 1049 1050 }, '3.17.2', {"requires": ["node"]});
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |