Selection.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. /**
  2. * Selection.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /**
  11. * This class handles text and control selection it's an crossbrowser utility class.
  12. * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
  13. *
  14. * @class tinymce.dom.Selection
  15. * @example
  16. * // Getting the currently selected node for the active editor
  17. * alert(tinymce.activeEditor.selection.getNode().nodeName);
  18. */
  19. define("tinymce/dom/Selection", [
  20. "tinymce/dom/TreeWalker",
  21. "tinymce/dom/TridentSelection",
  22. "tinymce/dom/ControlSelection",
  23. "tinymce/dom/RangeUtils",
  24. "tinymce/Env",
  25. "tinymce/util/Tools"
  26. ], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, Env, Tools) {
  27. var each = Tools.each, grep = Tools.grep, trim = Tools.trim;
  28. var isIE = Env.ie, isOpera = Env.opera;
  29. /**
  30. * Constructs a new selection instance.
  31. *
  32. * @constructor
  33. * @method Selection
  34. * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
  35. * @param {Window} win Window to bind the selection object to.
  36. * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
  37. */
  38. function Selection(dom, win, serializer, editor) {
  39. var self = this;
  40. self.dom = dom;
  41. self.win = win;
  42. self.serializer = serializer;
  43. self.editor = editor;
  44. self.controlSelection = new ControlSelection(self, editor);
  45. // No W3C Range support
  46. if (!self.win.getSelection) {
  47. self.tridentSel = new TridentSelection(self);
  48. }
  49. }
  50. Selection.prototype = {
  51. /**
  52. * Move the selection cursor range to the specified node and offset.
  53. * If there is no node specified it will move it to the first suitable location within the body.
  54. *
  55. * @method setCursorLocation
  56. * @param {Node} node Optional node to put the cursor in.
  57. * @param {Number} offset Optional offset from the start of the node to put the cursor at.
  58. */
  59. setCursorLocation: function(node, offset) {
  60. var self = this, rng = self.dom.createRng();
  61. if (!node) {
  62. self._moveEndPoint(rng, self.editor.getBody(), true);
  63. self.setRng(rng);
  64. } else {
  65. rng.setStart(node, offset);
  66. rng.setEnd(node, offset);
  67. self.setRng(rng);
  68. self.collapse(false);
  69. }
  70. },
  71. /**
  72. * Returns the selected contents using the DOM serializer passed in to this class.
  73. *
  74. * @method getContent
  75. * @param {Object} s Optional settings class with for example output format text or html.
  76. * @return {String} Selected contents in for example HTML format.
  77. * @example
  78. * // Alerts the currently selected contents
  79. * alert(tinymce.activeEditor.selection.getContent());
  80. *
  81. * // Alerts the currently selected contents as plain text
  82. * alert(tinymce.activeEditor.selection.getContent({format: 'text'}));
  83. */
  84. getContent: function(args) {
  85. var self = this, rng = self.getRng(), tmpElm = self.dom.create("body");
  86. var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment;
  87. args = args || {};
  88. whiteSpaceBefore = whiteSpaceAfter = '';
  89. args.get = true;
  90. args.format = args.format || 'html';
  91. args.selection = true;
  92. self.editor.fire('BeforeGetContent', args);
  93. if (args.format == 'text') {
  94. return self.isCollapsed() ? '' : (rng.text || (se.toString ? se.toString() : ''));
  95. }
  96. if (rng.cloneContents) {
  97. fragment = rng.cloneContents();
  98. if (fragment) {
  99. tmpElm.appendChild(fragment);
  100. }
  101. } else if (rng.item !== undefined || rng.htmlText !== undefined) {
  102. // IE will produce invalid markup if elements are present that
  103. // it doesn't understand like custom elements or HTML5 elements.
  104. // Adding a BR in front of the contents and then remoiving it seems to fix it though.
  105. tmpElm.innerHTML = '<br>' + (rng.item ? rng.item(0).outerHTML : rng.htmlText);
  106. tmpElm.removeChild(tmpElm.firstChild);
  107. } else {
  108. tmpElm.innerHTML = rng.toString();
  109. }
  110. // Keep whitespace before and after
  111. if (/^\s/.test(tmpElm.innerHTML)) {
  112. whiteSpaceBefore = ' ';
  113. }
  114. if (/\s+$/.test(tmpElm.innerHTML)) {
  115. whiteSpaceAfter = ' ';
  116. }
  117. args.getInner = true;
  118. args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter;
  119. self.editor.fire('GetContent', args);
  120. return args.content;
  121. },
  122. /**
  123. * Sets the current selection to the specified content. If any contents is selected it will be replaced
  124. * with the contents passed in to this function. If there is no selection the contents will be inserted
  125. * where the caret is placed in the editor/page.
  126. *
  127. * @method setContent
  128. * @param {String} content HTML contents to set could also be other formats depending on settings.
  129. * @param {Object} args Optional settings object with for example data format.
  130. * @example
  131. * // Inserts some HTML contents at the current selection
  132. * tinymce.activeEditor.selection.setContent('<strong>Some contents</strong>');
  133. */
  134. setContent: function(content, args) {
  135. var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp;
  136. args = args || {format: 'html'};
  137. args.set = true;
  138. args.selection = true;
  139. content = args.content = content;
  140. // Dispatch before set content event
  141. if (!args.no_events) {
  142. self.editor.fire('BeforeSetContent', args);
  143. }
  144. content = args.content;
  145. if (rng.insertNode) {
  146. // Make caret marker since insertNode places the caret in the beginning of text after insert
  147. content += '<span id="__caret">_</span>';
  148. // Delete and insert new node
  149. if (rng.startContainer == doc && rng.endContainer == doc) {
  150. // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
  151. doc.body.innerHTML = content;
  152. } else {
  153. rng.deleteContents();
  154. if (doc.body.childNodes.length === 0) {
  155. doc.body.innerHTML = content;
  156. } else {
  157. // createContextualFragment doesn't exists in IE 9 DOMRanges
  158. if (rng.createContextualFragment) {
  159. rng.insertNode(rng.createContextualFragment(content));
  160. } else {
  161. // Fake createContextualFragment call in IE 9
  162. frag = doc.createDocumentFragment();
  163. temp = doc.createElement('div');
  164. frag.appendChild(temp);
  165. temp.outerHTML = content;
  166. rng.insertNode(frag);
  167. }
  168. }
  169. }
  170. // Move to caret marker
  171. caretNode = self.dom.get('__caret');
  172. // Make sure we wrap it compleatly, Opera fails with a simple select call
  173. rng = doc.createRange();
  174. rng.setStartBefore(caretNode);
  175. rng.setEndBefore(caretNode);
  176. self.setRng(rng);
  177. // Remove the caret position
  178. self.dom.remove('__caret');
  179. try {
  180. self.setRng(rng);
  181. } catch (ex) {
  182. // Might fail on Opera for some odd reason
  183. }
  184. } else {
  185. if (rng.item) {
  186. // Delete content and get caret text selection
  187. doc.execCommand('Delete', false, null);
  188. rng = self.getRng();
  189. }
  190. // Explorer removes spaces from the beginning of pasted contents
  191. if (/^\s+/.test(content)) {
  192. rng.pasteHTML('<span id="__mce_tmp">_</span>' + content);
  193. self.dom.remove('__mce_tmp');
  194. } else {
  195. rng.pasteHTML(content);
  196. }
  197. }
  198. // Dispatch set content event
  199. if (!args.no_events) {
  200. self.editor.fire('SetContent', args);
  201. }
  202. },
  203. /**
  204. * Returns the start element of a selection range. If the start is in a text
  205. * node the parent element will be returned.
  206. *
  207. * @method getStart
  208. * @return {Element} Start element of selection range.
  209. */
  210. getStart: function() {
  211. var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node;
  212. if (rng.duplicate || rng.item) {
  213. // Control selection, return first item
  214. if (rng.item) {
  215. return rng.item(0);
  216. }
  217. // Get start element
  218. checkRng = rng.duplicate();
  219. checkRng.collapse(1);
  220. startElement = checkRng.parentElement();
  221. if (startElement.ownerDocument !== self.dom.doc) {
  222. startElement = self.dom.getRoot();
  223. }
  224. // Check if range parent is inside the start element, then return the inner parent element
  225. // This will fix issues when a single element is selected, IE would otherwise return the wrong start element
  226. parentElement = node = rng.parentElement();
  227. while ((node = node.parentNode)) {
  228. if (node == startElement) {
  229. startElement = parentElement;
  230. break;
  231. }
  232. }
  233. return startElement;
  234. } else {
  235. startElement = rng.startContainer;
  236. if (startElement.nodeType == 1 && startElement.hasChildNodes()) {
  237. startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];
  238. }
  239. if (startElement && startElement.nodeType == 3) {
  240. return startElement.parentNode;
  241. }
  242. return startElement;
  243. }
  244. },
  245. /**
  246. * Returns the end element of a selection range. If the end is in a text
  247. * node the parent element will be returned.
  248. *
  249. * @method getEnd
  250. * @return {Element} End element of selection range.
  251. */
  252. getEnd: function() {
  253. var self = this, rng = self.getRng(), endElement, endOffset;
  254. if (rng.duplicate || rng.item) {
  255. if (rng.item) {
  256. return rng.item(0);
  257. }
  258. rng = rng.duplicate();
  259. rng.collapse(0);
  260. endElement = rng.parentElement();
  261. if (endElement.ownerDocument !== self.dom.doc) {
  262. endElement = self.dom.getRoot();
  263. }
  264. if (endElement && endElement.nodeName == 'BODY') {
  265. return endElement.lastChild || endElement;
  266. }
  267. return endElement;
  268. } else {
  269. endElement = rng.endContainer;
  270. endOffset = rng.endOffset;
  271. if (endElement.nodeType == 1 && endElement.hasChildNodes()) {
  272. endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset];
  273. }
  274. if (endElement && endElement.nodeType == 3) {
  275. return endElement.parentNode;
  276. }
  277. return endElement;
  278. }
  279. },
  280. /**
  281. * Returns a bookmark location for the current selection. This bookmark object
  282. * can then be used to restore the selection after some content modification to the document.
  283. *
  284. * @method getBookmark
  285. * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
  286. * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
  287. * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
  288. * @example
  289. * // Stores a bookmark of the current selection
  290. * var bm = tinymce.activeEditor.selection.getBookmark();
  291. *
  292. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  293. *
  294. * // Restore the selection bookmark
  295. * tinymce.activeEditor.selection.moveToBookmark(bm);
  296. */
  297. getBookmark: function(type, normalized) {
  298. var self = this, dom = self.dom, rng, rng2, id, collapsed, name, element, chr = '&#xFEFF;', styles;
  299. function findIndex(name, element) {
  300. var index = 0;
  301. each(dom.select(name), function(node, i) {
  302. if (node == element) {
  303. index = i;
  304. }
  305. });
  306. return index;
  307. }
  308. function normalizeTableCellSelection(rng) {
  309. function moveEndPoint(start) {
  310. var container, offset, childNodes, prefix = start ? 'start' : 'end';
  311. container = rng[prefix + 'Container'];
  312. offset = rng[prefix + 'Offset'];
  313. if (container.nodeType == 1 && container.nodeName == "TR") {
  314. childNodes = container.childNodes;
  315. container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
  316. if (container) {
  317. offset = start ? 0 : container.childNodes.length;
  318. rng['set' + (start ? 'Start' : 'End')](container, offset);
  319. }
  320. }
  321. }
  322. moveEndPoint(true);
  323. moveEndPoint();
  324. return rng;
  325. }
  326. function getLocation() {
  327. var rng = self.getRng(true), root = dom.getRoot(), bookmark = {};
  328. function getPoint(rng, start) {
  329. var container = rng[start ? 'startContainer' : 'endContainer'],
  330. offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
  331. if (container.nodeType == 3) {
  332. if (normalized) {
  333. for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) {
  334. offset += node.nodeValue.length;
  335. }
  336. }
  337. point.push(offset);
  338. } else {
  339. childNodes = container.childNodes;
  340. if (offset >= childNodes.length && childNodes.length) {
  341. after = 1;
  342. offset = Math.max(0, childNodes.length - 1);
  343. }
  344. point.push(self.dom.nodeIndex(childNodes[offset], normalized) + after);
  345. }
  346. for (; container && container != root; container = container.parentNode) {
  347. point.push(self.dom.nodeIndex(container, normalized));
  348. }
  349. return point;
  350. }
  351. bookmark.start = getPoint(rng, true);
  352. if (!self.isCollapsed()) {
  353. bookmark.end = getPoint(rng);
  354. }
  355. return bookmark;
  356. }
  357. if (type == 2) {
  358. element = self.getNode();
  359. name = element ? element.nodeName : null;
  360. if (name == 'IMG') {
  361. return {name: name, index: findIndex(name, element)};
  362. }
  363. if (self.tridentSel) {
  364. return self.tridentSel.getBookmark(type);
  365. }
  366. return getLocation();
  367. }
  368. // Handle simple range
  369. if (type) {
  370. return {rng: self.getRng()};
  371. }
  372. rng = self.getRng();
  373. id = dom.uniqueId();
  374. collapsed = self.isCollapsed();
  375. styles = 'overflow:hidden;line-height:0px';
  376. // Explorer method
  377. if (rng.duplicate || rng.item) {
  378. // Text selection
  379. if (!rng.item) {
  380. rng2 = rng.duplicate();
  381. try {
  382. // Insert start marker
  383. rng.collapse();
  384. rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
  385. // Insert end marker
  386. if (!collapsed) {
  387. rng2.collapse(false);
  388. // Detect the empty space after block elements in IE and move the
  389. // end back one character <p></p>] becomes <p>]</p>
  390. rng.moveToElementText(rng2.parentElement());
  391. if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
  392. rng2.move('character', -1);
  393. }
  394. rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
  395. }
  396. } catch (ex) {
  397. // IE might throw unspecified error so lets ignore it
  398. return null;
  399. }
  400. } else {
  401. // Control selection
  402. element = rng.item(0);
  403. name = element.nodeName;
  404. return {name: name, index: findIndex(name, element)};
  405. }
  406. } else {
  407. element = self.getNode();
  408. name = element.nodeName;
  409. if (name == 'IMG') {
  410. return {name: name, index: findIndex(name, element)};
  411. }
  412. // W3C method
  413. rng2 = normalizeTableCellSelection(rng.cloneRange());
  414. // Insert end marker
  415. if (!collapsed) {
  416. rng2.collapse(false);
  417. rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr));
  418. }
  419. rng = normalizeTableCellSelection(rng);
  420. rng.collapse(true);
  421. rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr));
  422. }
  423. self.moveToBookmark({id: id, keep: 1});
  424. return {id: id};
  425. },
  426. /**
  427. * Restores the selection to the specified bookmark.
  428. *
  429. * @method moveToBookmark
  430. * @param {Object} bookmark Bookmark to restore selection from.
  431. * @return {Boolean} true/false if it was successful or not.
  432. * @example
  433. * // Stores a bookmark of the current selection
  434. * var bm = tinymce.activeEditor.selection.getBookmark();
  435. *
  436. * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
  437. *
  438. * // Restore the selection bookmark
  439. * tinymce.activeEditor.selection.moveToBookmark(bm);
  440. */
  441. moveToBookmark: function(bookmark) {
  442. var self = this, dom = self.dom, rng, root, startContainer, endContainer, startOffset, endOffset;
  443. function setEndPoint(start) {
  444. var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
  445. if (point) {
  446. offset = point[0];
  447. // Find container node
  448. for (node = root, i = point.length - 1; i >= 1; i--) {
  449. children = node.childNodes;
  450. if (point[i] > children.length - 1) {
  451. return;
  452. }
  453. node = children[point[i]];
  454. }
  455. // Move text offset to best suitable location
  456. if (node.nodeType === 3) {
  457. offset = Math.min(point[0], node.nodeValue.length);
  458. }
  459. // Move element offset to best suitable location
  460. if (node.nodeType === 1) {
  461. offset = Math.min(point[0], node.childNodes.length);
  462. }
  463. // Set offset within container node
  464. if (start) {
  465. rng.setStart(node, offset);
  466. } else {
  467. rng.setEnd(node, offset);
  468. }
  469. }
  470. return true;
  471. }
  472. function restoreEndPoint(suffix) {
  473. var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
  474. if (marker) {
  475. node = marker.parentNode;
  476. if (suffix == 'start') {
  477. if (!keep) {
  478. idx = dom.nodeIndex(marker);
  479. } else {
  480. node = marker.firstChild;
  481. idx = 1;
  482. }
  483. startContainer = endContainer = node;
  484. startOffset = endOffset = idx;
  485. } else {
  486. if (!keep) {
  487. idx = dom.nodeIndex(marker);
  488. } else {
  489. node = marker.firstChild;
  490. idx = 1;
  491. }
  492. endContainer = node;
  493. endOffset = idx;
  494. }
  495. if (!keep) {
  496. prev = marker.previousSibling;
  497. next = marker.nextSibling;
  498. // Remove all marker text nodes
  499. each(grep(marker.childNodes), function(node) {
  500. if (node.nodeType == 3) {
  501. node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
  502. }
  503. });
  504. // Remove marker but keep children if for example contents where inserted into the marker
  505. // Also remove duplicated instances of the marker for example by a
  506. // split operation or by WebKit auto split on paste feature
  507. while ((marker = dom.get(bookmark.id + '_' + suffix))) {
  508. dom.remove(marker, 1);
  509. }
  510. // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
  511. // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
  512. // isn't worth the effort. Sorry, Opera but it's just a fact
  513. if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !isOpera) {
  514. idx = prev.nodeValue.length;
  515. prev.appendData(next.nodeValue);
  516. dom.remove(next);
  517. if (suffix == 'start') {
  518. startContainer = endContainer = prev;
  519. startOffset = endOffset = idx;
  520. } else {
  521. endContainer = prev;
  522. endOffset = idx;
  523. }
  524. }
  525. }
  526. }
  527. }
  528. function addBogus(node) {
  529. // Adds a bogus BR element for empty block elements
  530. if (dom.isBlock(node) && !node.innerHTML && !isIE) {
  531. node.innerHTML = '<br data-mce-bogus="1" />';
  532. }
  533. return node;
  534. }
  535. if (bookmark) {
  536. if (bookmark.start) {
  537. rng = dom.createRng();
  538. root = dom.getRoot();
  539. if (self.tridentSel) {
  540. return self.tridentSel.moveToBookmark(bookmark);
  541. }
  542. if (setEndPoint(true) && setEndPoint()) {
  543. self.setRng(rng);
  544. }
  545. } else if (bookmark.id) {
  546. // Restore start/end points
  547. restoreEndPoint('start');
  548. restoreEndPoint('end');
  549. if (startContainer) {
  550. rng = dom.createRng();
  551. rng.setStart(addBogus(startContainer), startOffset);
  552. rng.setEnd(addBogus(endContainer), endOffset);
  553. self.setRng(rng);
  554. }
  555. } else if (bookmark.name) {
  556. self.select(dom.select(bookmark.name)[bookmark.index]);
  557. } else if (bookmark.rng) {
  558. self.setRng(bookmark.rng);
  559. }
  560. }
  561. },
  562. /**
  563. * Selects the specified element. This will place the start and end of the selection range around the element.
  564. *
  565. * @method select
  566. * @param {Element} node HMTL DOM element to select.
  567. * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
  568. * @return {Element} Selected element the same element as the one that got passed in.
  569. * @example
  570. * // Select the first paragraph in the active editor
  571. * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]);
  572. */
  573. select: function(node, content) {
  574. var self = this, dom = self.dom, rng = dom.createRng(), idx;
  575. // Clear stored range set by FocusManager
  576. self.lastFocusBookmark = null;
  577. if (node) {
  578. if (!content && self.controlSelection.controlSelect(node)) {
  579. return;
  580. }
  581. idx = dom.nodeIndex(node);
  582. rng.setStart(node.parentNode, idx);
  583. rng.setEnd(node.parentNode, idx + 1);
  584. // Find first/last text node or BR element
  585. if (content) {
  586. self._moveEndPoint(rng, node, true);
  587. self._moveEndPoint(rng, node);
  588. }
  589. self.setRng(rng);
  590. }
  591. return node;
  592. },
  593. /**
  594. * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
  595. *
  596. * @method isCollapsed
  597. * @return {Boolean} true/false state if the selection range is collapsed or not.
  598. * Collapsed means if it's a caret or a larger selection.
  599. */
  600. isCollapsed: function() {
  601. var self = this, rng = self.getRng(), sel = self.getSel();
  602. if (!rng || rng.item) {
  603. return false;
  604. }
  605. if (rng.compareEndPoints) {
  606. return rng.compareEndPoints('StartToEnd', rng) === 0;
  607. }
  608. return !sel || rng.collapsed;
  609. },
  610. /**
  611. * Collapse the selection to start or end of range.
  612. *
  613. * @method collapse
  614. * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start.
  615. */
  616. collapse: function(to_start) {
  617. var self = this, rng = self.getRng(), node;
  618. // Control range on IE
  619. if (rng.item) {
  620. node = rng.item(0);
  621. rng = self.win.document.body.createTextRange();
  622. rng.moveToElementText(node);
  623. }
  624. rng.collapse(!!to_start);
  625. self.setRng(rng);
  626. },
  627. /**
  628. * Returns the browsers internal selection object.
  629. *
  630. * @method getSel
  631. * @return {Selection} Internal browser selection object.
  632. */
  633. getSel: function() {
  634. var win = this.win;
  635. return win.getSelection ? win.getSelection() : win.document.selection;
  636. },
  637. /**
  638. * Returns the browsers internal range object.
  639. *
  640. * @method getRng
  641. * @param {Boolean} w3c Forces a compatible W3C range on IE.
  642. * @return {Range} Internal browser range object.
  643. * @see http://www.quirksmode.org/dom/range_intro.html
  644. * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/
  645. */
  646. getRng: function(w3c) {
  647. var self = this, selection, rng, elm, doc = self.win.document, ieRng;
  648. function tryCompareBounderyPoints(how, sourceRange, destinationRange) {
  649. try {
  650. return sourceRange.compareBoundaryPoints(how, destinationRange);
  651. } catch (ex) {
  652. // Gecko throws wrong document exception if the range points
  653. // to nodes that where removed from the dom #6690
  654. // Browsers should mutate existing DOMRange instances so that they always point
  655. // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink
  656. // For performance reasons just return -1
  657. return -1;
  658. }
  659. }
  660. // Use last rng passed from FocusManager if it's available this enables
  661. // calls to editor.selection.getStart() to work when caret focus is lost on IE
  662. if (!w3c && self.lastFocusBookmark) {
  663. var bookmark = self.lastFocusBookmark;
  664. // Convert bookmark to range IE 11 fix
  665. if (bookmark.startContainer) {
  666. rng = doc.createRange();
  667. rng.setStart(bookmark.startContainer, bookmark.startOffset);
  668. rng.setEnd(bookmark.endContainer, bookmark.endOffset);
  669. } else {
  670. rng = bookmark;
  671. }
  672. return rng;
  673. }
  674. // Found tridentSel object then we need to use that one
  675. if (w3c && self.tridentSel) {
  676. return self.tridentSel.getRangeAt(0);
  677. }
  678. try {
  679. if ((selection = self.getSel())) {
  680. if (selection.rangeCount > 0) {
  681. rng = selection.getRangeAt(0);
  682. } else {
  683. rng = selection.createRange ? selection.createRange() : doc.createRange();
  684. }
  685. }
  686. } catch (ex) {
  687. // IE throws unspecified error here if TinyMCE is placed in a frame/iframe
  688. }
  689. // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet
  690. // IE 11 doesn't support the selection object so we check for that as well
  691. if (isIE && rng && rng.setStart && doc.selection) {
  692. try {
  693. // IE will sometimes throw an exception here
  694. ieRng = doc.selection.createRange();
  695. } catch (ex) {
  696. }
  697. if (ieRng && ieRng.item) {
  698. elm = ieRng.item(0);
  699. rng = doc.createRange();
  700. rng.setStartBefore(elm);
  701. rng.setEndAfter(elm);
  702. }
  703. }
  704. // No range found then create an empty one
  705. // This can occur when the editor is placed in a hidden container element on Gecko
  706. // Or on IE when there was an exception
  707. if (!rng) {
  708. rng = doc.createRange ? doc.createRange() : doc.body.createTextRange();
  709. }
  710. // If range is at start of document then move it to start of body
  711. if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) {
  712. elm = self.dom.getRoot();
  713. rng.setStart(elm, 0);
  714. rng.setEnd(elm, 0);
  715. }
  716. if (self.selectedRange && self.explicitRange) {
  717. if (tryCompareBounderyPoints(rng.START_TO_START, rng, self.selectedRange) === 0 &&
  718. tryCompareBounderyPoints(rng.END_TO_END, rng, self.selectedRange) === 0) {
  719. // Safari, Opera and Chrome only ever select text which causes the range to change.
  720. // This lets us use the originally set range if the selection hasn't been changed by the user.
  721. rng = self.explicitRange;
  722. } else {
  723. self.selectedRange = null;
  724. self.explicitRange = null;
  725. }
  726. }
  727. return rng;
  728. },
  729. /**
  730. * Changes the selection to the specified DOM range.
  731. *
  732. * @method setRng
  733. * @param {Range} rng Range to select.
  734. */
  735. setRng: function(rng, forward) {
  736. var self = this, sel;
  737. // Is IE specific range
  738. if (rng.select) {
  739. try {
  740. rng.select();
  741. } catch (ex) {
  742. // Needed for some odd IE bug #1843306
  743. }
  744. return;
  745. }
  746. if (!self.tridentSel) {
  747. sel = self.getSel();
  748. if (sel) {
  749. self.explicitRange = rng;
  750. try {
  751. sel.removeAllRanges();
  752. sel.addRange(rng);
  753. } catch (ex) {
  754. // IE might throw errors here if the editor is within a hidden container and selection is changed
  755. }
  756. // Forward is set to false and we have an extend function
  757. if (forward === false && sel.extend) {
  758. sel.collapse(rng.endContainer, rng.endOffset);
  759. sel.extend(rng.startContainer, rng.startOffset);
  760. }
  761. // adding range isn't always successful so we need to check range count otherwise an exception can occur
  762. self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
  763. }
  764. } else {
  765. // Is W3C Range fake range on IE
  766. if (rng.cloneRange) {
  767. try {
  768. self.tridentSel.addRange(rng);
  769. return;
  770. } catch (ex) {
  771. //IE9 throws an error here if called before selection is placed in the editor
  772. }
  773. }
  774. }
  775. },
  776. /**
  777. * Sets the current selection to the specified DOM element.
  778. *
  779. * @method setNode
  780. * @param {Element} elm Element to set as the contents of the selection.
  781. * @return {Element} Returns the element that got passed in.
  782. * @example
  783. * // Inserts a DOM node at current selection/caret location
  784. * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'}));
  785. */
  786. setNode: function(elm) {
  787. var self = this;
  788. self.setContent(self.dom.getOuterHTML(elm));
  789. return elm;
  790. },
  791. /**
  792. * Returns the currently selected element or the common ancestor element for both start and end of the selection.
  793. *
  794. * @method getNode
  795. * @return {Element} Currently selected element or common ancestor element.
  796. * @example
  797. * // Alerts the currently selected elements node name
  798. * alert(tinymce.activeEditor.selection.getNode().nodeName);
  799. */
  800. getNode: function() {
  801. var self = this, rng = self.getRng(), elm;
  802. var startContainer = rng.startContainer, endContainer = rng.endContainer;
  803. var startOffset = rng.startOffset, endOffset = rng.endOffset, root = self.dom.getRoot();
  804. function skipEmptyTextNodes(node, forwards) {
  805. var orig = node;
  806. while (node && node.nodeType === 3 && node.length === 0) {
  807. node = forwards ? node.nextSibling : node.previousSibling;
  808. }
  809. return node || orig;
  810. }
  811. // Range maybe lost after the editor is made visible again
  812. if (!rng) {
  813. return root;
  814. }
  815. if (rng.setStart) {
  816. elm = rng.commonAncestorContainer;
  817. // Handle selection a image or other control like element such as anchors
  818. if (!rng.collapsed) {
  819. if (startContainer == endContainer) {
  820. if (endOffset - startOffset < 2) {
  821. if (startContainer.hasChildNodes()) {
  822. elm = startContainer.childNodes[startOffset];
  823. }
  824. }
  825. }
  826. // If the anchor node is a element instead of a text node then return this element
  827. //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1)
  828. // return sel.anchorNode.childNodes[sel.anchorOffset];
  829. // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent.
  830. // This happens when you double click an underlined word in FireFox.
  831. if (startContainer.nodeType === 3 && endContainer.nodeType === 3) {
  832. if (startContainer.length === startOffset) {
  833. startContainer = skipEmptyTextNodes(startContainer.nextSibling, true);
  834. } else {
  835. startContainer = startContainer.parentNode;
  836. }
  837. if (endOffset === 0) {
  838. endContainer = skipEmptyTextNodes(endContainer.previousSibling, false);
  839. } else {
  840. endContainer = endContainer.parentNode;
  841. }
  842. if (startContainer && startContainer === endContainer) {
  843. return startContainer;
  844. }
  845. }
  846. }
  847. if (elm && elm.nodeType == 3) {
  848. return elm.parentNode;
  849. }
  850. return elm;
  851. }
  852. elm = rng.item ? rng.item(0) : rng.parentElement();
  853. // IE 7 might return elements outside the iframe
  854. if (elm.ownerDocument !== self.win.document) {
  855. elm = root;
  856. }
  857. return elm;
  858. },
  859. getSelectedBlocks: function(startElm, endElm) {
  860. var self = this, dom = self.dom, node, root, selectedBlocks = [];
  861. root = dom.getRoot();
  862. startElm = dom.getParent(startElm || self.getStart(), dom.isBlock);
  863. endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock);
  864. if (startElm && startElm != root) {
  865. selectedBlocks.push(startElm);
  866. }
  867. if (startElm && endElm && startElm != endElm) {
  868. node = startElm;
  869. var walker = new TreeWalker(startElm, root);
  870. while ((node = walker.next()) && node != endElm) {
  871. if (dom.isBlock(node)) {
  872. selectedBlocks.push(node);
  873. }
  874. }
  875. }
  876. if (endElm && startElm != endElm && endElm != root) {
  877. selectedBlocks.push(endElm);
  878. }
  879. return selectedBlocks;
  880. },
  881. isForward: function() {
  882. var dom = this.dom, sel = this.getSel(), anchorRange, focusRange;
  883. // No support for selection direction then always return true
  884. if (!sel || !sel.anchorNode || !sel.focusNode) {
  885. return true;
  886. }
  887. anchorRange = dom.createRng();
  888. anchorRange.setStart(sel.anchorNode, sel.anchorOffset);
  889. anchorRange.collapse(true);
  890. focusRange = dom.createRng();
  891. focusRange.setStart(sel.focusNode, sel.focusOffset);
  892. focusRange.collapse(true);
  893. return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0;
  894. },
  895. normalize: function() {
  896. var self = this, rng = self.getRng();
  897. if (!isIE && new RangeUtils(self.dom).normalize(rng)) {
  898. self.setRng(rng, self.isForward());
  899. }
  900. return rng;
  901. },
  902. /**
  903. * Executes callback of the current selection matches the specified selector or not and passes the state and args to the callback.
  904. *
  905. * @method selectorChanged
  906. * @param {String} selector CSS selector to check for.
  907. * @param {function} callback Callback with state and args when the selector is matches or not.
  908. */
  909. selectorChanged: function(selector, callback) {
  910. var self = this, currentSelectors;
  911. if (!self.selectorChangedData) {
  912. self.selectorChangedData = {};
  913. currentSelectors = {};
  914. self.editor.on('NodeChange', function(e) {
  915. var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {};
  916. // Check for new matching selectors
  917. each(self.selectorChangedData, function(callbacks, selector) {
  918. each(parents, function(node) {
  919. if (dom.is(node, selector)) {
  920. if (!currentSelectors[selector]) {
  921. // Execute callbacks
  922. each(callbacks, function(callback) {
  923. callback(true, {node: node, selector: selector, parents: parents});
  924. });
  925. currentSelectors[selector] = callbacks;
  926. }
  927. matchedSelectors[selector] = callbacks;
  928. return false;
  929. }
  930. });
  931. });
  932. // Check if current selectors still match
  933. each(currentSelectors, function(callbacks, selector) {
  934. if (!matchedSelectors[selector]) {
  935. delete currentSelectors[selector];
  936. each(callbacks, function(callback) {
  937. callback(false, {node: node, selector: selector, parents: parents});
  938. });
  939. }
  940. });
  941. });
  942. }
  943. // Add selector listeners
  944. if (!self.selectorChangedData[selector]) {
  945. self.selectorChangedData[selector] = [];
  946. }
  947. self.selectorChangedData[selector].push(callback);
  948. return self;
  949. },
  950. getScrollContainer: function() {
  951. var scrollContainer, node = this.dom.getRoot();
  952. while (node && node.nodeName != 'BODY') {
  953. if (node.scrollHeight > node.clientHeight) {
  954. scrollContainer = node;
  955. break;
  956. }
  957. node = node.parentNode;
  958. }
  959. return scrollContainer;
  960. },
  961. scrollIntoView: function(elm) {
  962. var y, viewPort, self = this, dom = self.dom, root = dom.getRoot(), viewPortY, viewPortH;
  963. function getPos(elm) {
  964. var x = 0, y = 0;
  965. var offsetParent = elm;
  966. while (offsetParent && offsetParent.nodeType) {
  967. x += offsetParent.offsetLeft || 0;
  968. y += offsetParent.offsetTop || 0;
  969. offsetParent = offsetParent.offsetParent;
  970. }
  971. return {x: x, y: y};
  972. }
  973. if (root.nodeName != 'BODY') {
  974. var scrollContainer = self.getScrollContainer();
  975. if (scrollContainer) {
  976. y = getPos(elm).y - getPos(scrollContainer).y;
  977. viewPortH = scrollContainer.clientHeight;
  978. viewPortY = scrollContainer.scrollTop;
  979. if (y < viewPortY || y + 25 > viewPortY + viewPortH) {
  980. scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25;
  981. }
  982. return;
  983. }
  984. }
  985. viewPort = dom.getViewPort(self.editor.getWin());
  986. y = dom.getPos(elm).y;
  987. viewPortY = viewPort.y;
  988. viewPortH = viewPort.h;
  989. if (y < viewPort.y || y + 25 > viewPortY + viewPortH) {
  990. self.editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25);
  991. }
  992. },
  993. _moveEndPoint: function(rng, node, start) {
  994. var root = node, walker = new TreeWalker(node, root);
  995. var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements();
  996. do {
  997. // Text node
  998. if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) {
  999. if (start) {
  1000. rng.setStart(node, 0);
  1001. } else {
  1002. rng.setEnd(node, node.nodeValue.length);
  1003. }
  1004. return;
  1005. }
  1006. // BR/IMG/INPUT elements
  1007. if (nonEmptyElementsMap[node.nodeName]) {
  1008. if (start) {
  1009. rng.setStartBefore(node);
  1010. } else {
  1011. if (node.nodeName == 'BR') {
  1012. rng.setEndBefore(node);
  1013. } else {
  1014. rng.setEndAfter(node);
  1015. }
  1016. }
  1017. return;
  1018. }
  1019. // Found empty text block old IE can place the selection inside those
  1020. if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) {
  1021. if (start) {
  1022. rng.setStart(node, 0);
  1023. } else {
  1024. rng.setEnd(node, 0);
  1025. }
  1026. return;
  1027. }
  1028. } while ((node = (start ? walker.next() : walker.prev())));
  1029. // Failed to find any text node or other suitable location then move to the root of body
  1030. if (root.nodeName == 'BODY') {
  1031. if (start) {
  1032. rng.setStart(root, 0);
  1033. } else {
  1034. rng.setEnd(root, root.childNodes.length);
  1035. }
  1036. }
  1037. },
  1038. destroy: function() {
  1039. this.win = null;
  1040. this.controlSelection.destroy();
  1041. }
  1042. };
  1043. return Selection;
  1044. });