plugin.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. /*
  2. * @license Copyright (c) CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.html or http://ckeditor.com/license
  4. */
  5. /**
  6. * Represent plain text selection range.
  7. */
  8. CKEDITOR.plugins.add('textselection',
  9. {
  10. version: "1.08.0",
  11. init: function (editor) {
  12. if (editor.config.fullPage) {
  13. return;
  14. }
  15. // Corresponding text range of WYSIWYG bookmark.
  16. var wysiwygBookmark;
  17. // Auto sync text selection with 'wysiwyg' mode selection range.
  18. if (editor.config.syncSelection
  19. && CKEDITOR.plugins.sourcearea) {
  20. editor.on('beforeModeUnload', function (evt) {
  21. if (editor.mode === 'source') {
  22. if (editor.mode === 'source' && !editor.plugins.codemirror) {
  23. var range = editor.getTextSelection();
  24. // Fly the range when create bookmark.
  25. delete range.element;
  26. range.createBookmark(editor);
  27. sourceBookmark = true;
  28. evt.data = range.content;
  29. }
  30. }
  31. });
  32. editor.on('mode', function () {
  33. if (editor.mode === 'wysiwyg' && sourceBookmark) {
  34. editor.focus();
  35. var doc = editor.document,
  36. range = new CKEDITOR.dom.range(editor.document),
  37. startNode,
  38. endNode,
  39. isTextNode = false;
  40. range.setStartAt(doc.getBody(), CKEDITOR.POSITION_AFTER_START);
  41. range.setEndAt(doc.getBody(), CKEDITOR.POSITION_BEFORE_END);
  42. var walker = new CKEDITOR.dom.walker(range);
  43. // walker.type = CKEDITOR.NODE_COMMENT;
  44. walker.evaluator = function (node) {
  45. //
  46. var match = /cke_bookmark_\d+(\w)/.exec(node.$.nodeValue);
  47. if (match) {
  48. if (unescape(node.$.nodeValue)
  49. .match(/<!--cke_bookmark_[0-9]+S-->.*<!--cke_bookmark_[0-9]+E-->/)){
  50. isTextNode = true;
  51. startNode = endNode = node;
  52. return false;
  53. } else {
  54. if (match[1] === 'S') {
  55. startNode = node;
  56. } else if (match[1] === 'E') {
  57. endNode = node;
  58. return false;
  59. }
  60. }
  61. }
  62. return false;
  63. };
  64. walker.lastForward();
  65. try {
  66. range.setStartAfter(startNode);
  67. range.setEndBefore(endNode);
  68. range.select();
  69. // Scroll into view for non-IE.
  70. if (!CKEDITOR.env.ie || (CKEDITOR.env.ie && CKEDITOR.env.version === 9)) {
  71. editor.getSelection().getStartElement().scrollIntoView(true);
  72. } // Remove the comments node which are out of range.
  73. if (isTextNode) {
  74. //remove all of our bookmarks from the text node
  75. //then remove all of the cke_protected bits that added because we had a comment
  76. //whatever code is supposed to clean these cke_protected up doesn't work
  77. //when there's two comments in a row like: <!--{cke_protected}{C}--><!--{cke_protected}{C}-->
  78. startNode.$.nodeValue = unescape(startNode.$.nodeValue).
  79. replace(/<!--cke_bookmark_[0-9]+[SE]-->/g, '').
  80. replace(/<!--[\s]*\{cke_protected}[\s]*\{C}[\s]*-->/g, '');
  81. } else {
  82. //just remove the comment nodes
  83. startNode.remove();
  84. endNode.remove();
  85. }
  86. } catch (excec) {
  87. }
  88. }
  89. }, null, null, 10);
  90. editor.on('beforeGetModeData', function () {
  91. if (editor.mode === 'wysiwyg' && editor.getData()) {
  92. if (CKEDITOR.env.gecko && !editor.focusManager.hasFocus) {
  93. return;
  94. }
  95. var sel = editor.getSelection(), range;
  96. if (sel && (range = sel.getRanges()[0])) {
  97. wysiwygBookmark = range.createBookmark(editor);
  98. }
  99. }
  100. });
  101. // Build text range right after WYSIWYG has unloaded.
  102. editor.on('afterModeUnload', function (evt) {
  103. if (editor.mode === 'wysiwyg' && wysiwygBookmark) {
  104. textRange = new CKEDITOR.dom.textRange(evt.data);
  105. textRange.moveToBookmark(wysiwygBookmark, editor);
  106. evt.data = textRange.content;
  107. }
  108. });
  109. editor.on('mode', function () {
  110. if (editor.mode === 'source' && textRange && !editor.plugins.codemirror) {
  111. textRange.element = new CKEDITOR.dom.element(editor._.editable.$);
  112. textRange.select();
  113. }
  114. });
  115. editor.on('destroy', function () {
  116. textRange = null;
  117. sourceBookmark = null;
  118. });
  119. }
  120. }
  121. });
  122. /**
  123. * Gets the current text selection from the editing area when in Source mode.
  124. * @returns {CKEDITOR.dom.textRange} Text range represent the caret positions.
  125. * @example
  126. * var textSelection = CKEDITOR.instances.editor1.<b>getTextSelection()</b>;
  127. * alert( textSelection.startOffset );
  128. * alert( textSelection.endOffset );
  129. */
  130. CKEDITOR.editor.prototype.getTextSelection = function () {
  131. return this._.editable && getTextSelection(this._.editable.$) || null;
  132. };
  133. /**
  134. * Returns the caret position of the specified textfield/textarea.
  135. * @param {HTMLTextArea|HTMLTextInput} element
  136. */
  137. function getTextSelection(element) {
  138. var startOffset, endOffset;
  139. if (!CKEDITOR.env.ie) {
  140. startOffset = element.selectionStart;
  141. endOffset = element.selectionEnd;
  142. } else {
  143. element.focus();
  144. // The current selection
  145. if (window.getSelection) {
  146. startOffset = element.selectionStart;
  147. endOffset = element.selectionEnd;
  148. } else {
  149. var range = document.selection.createRange(),
  150. textLength = range.text.length;
  151. // Create a 'measuring' range to help calculate the start offset by
  152. // stretching it from start to current position.
  153. var measureRange = range.duplicate();
  154. measureRange.moveToElementText(element);
  155. measureRange.setEndPoint('EndToEnd', range);
  156. endOffset = measureRange.text.length;
  157. startOffset = endOffset - textLength;
  158. }
  159. }
  160. return new CKEDITOR.dom.textRange(
  161. new CKEDITOR.dom.element(element), startOffset, endOffset);
  162. }
  163. /**
  164. * Represent the selection range within a HTML textfield/textarea element,
  165. * or even a flyweight string content represent the text content.
  166. * @constructor
  167. * @param {CKEDITOR.dom.element|String} element
  168. * @param {Number} start
  169. * @param {Number} end
  170. */
  171. CKEDITOR.dom.textRange = function (element, start, end) {
  172. if (element instanceof CKEDITOR.dom.element
  173. && (element.is('textarea')
  174. || element.is('input') && element.getAttribute('type') == 'text')) {
  175. this.element = element;
  176. this.content = element.$.value;
  177. } else if (typeof element == 'string')
  178. this.content = element;
  179. else
  180. throw 'Unknown "element" type.';
  181. this.startOffset = start || 0;
  182. this.endOffset = end || 0;
  183. };
  184. /**
  185. * Changes the editing mode of this editor instance. (Override of the original function)
  186. *
  187. * **Note:** The mode switch could be asynchronous depending on the mode provider.
  188. * Use the `callback` to hook subsequent code.
  189. *
  190. * // Switch to "source" view.
  191. * CKEDITOR.instances.editor1.setMode( 'source' );
  192. * // Switch to "WYSIWYG" view and be notified on completion.
  193. * CKEDITOR.instances.editor1.setMode( 'wysiwyg', function() { alert( 'WYSIWYG mode loaded!' ); } );
  194. *
  195. * @param {String} [newMode] If not specified, the {@link CKEDITOR.config#startupMode} will be used.
  196. * @param {Function} [callback] Optional callback function which is invoked once the mode switch has succeeded.
  197. */
  198. CKEDITOR.editor.prototype.setMode = function (newMode, callback) {
  199. var editor = this;
  200. var modes = this._.modes;
  201. // Mode loading quickly fails.
  202. if (newMode == editor.mode || !modes || !modes[newMode])
  203. return;
  204. editor.fire('beforeSetMode', newMode);
  205. if (editor.mode) {
  206. var isDirty = editor.checkDirty();
  207. editor._.previousMode = editor.mode;
  208. // Get cached data, which was set while detaching editable.
  209. editor._.previousModeData = editor.getData();
  210. editor.fire('beforeModeUnload');
  211. editor.fire('beforeGetModeData');
  212. var data = editor.getData();
  213. data = editor.fire('beforeModeUnload', data);
  214. data = editor.fire('afterModeUnload', data);
  215. // Detach the current editable.
  216. editor.editable(0);
  217. editor._.data = data;
  218. // Clear up the mode space.
  219. editor.ui.space('contents').setHtml('');
  220. editor.mode = '';
  221. }
  222. // Fire the mode handler.
  223. this._.modes[newMode](function () {
  224. // Set the current mode.
  225. editor.mode = newMode;
  226. if (isDirty !== undefined) {
  227. !isDirty && editor.resetDirty();
  228. }
  229. // Delay to avoid race conditions (setMode inside setMode).
  230. setTimeout(function () {
  231. editor.fire('mode');
  232. callback && callback.call(editor);
  233. }, 0);
  234. });
  235. };
  236. CKEDITOR.dom.textRange.prototype =
  237. {
  238. /**
  239. * Sets the text selection of the specified textfield/textarea.
  240. * @param {HTMLTextArea|HTMLTextInput} element
  241. * @param {CKEDITOR.dom.textRange} range
  242. */
  243. select: function() {
  244. var startOffset = this.startOffset,
  245. endOffset = this.endOffset,
  246. element = this.element.$;
  247. if (endOffset == undefined) {
  248. endOffset = startOffset;
  249. }
  250. if (CKEDITOR.env.ie && CKEDITOR.env.version == 9) {
  251. element.focus();
  252. element.selectionStart = startOffset;
  253. element.selectionEnd = startOffset;
  254. setTimeout(function() {
  255. element.selectionStart = startOffset;
  256. element.selectionEnd = endOffset;
  257. }, 20);
  258. } else {
  259. if (element.setSelectionRange) {
  260. if (CKEDITOR.env.ie) {
  261. element.focus();
  262. }
  263. element.setSelectionRange(startOffset, endOffset);
  264. if (!CKEDITOR.env.ie) {
  265. element.focus();
  266. }
  267. } else if (element.createTextRange) {
  268. element.focus();
  269. var range = element.createTextRange();
  270. range.collapse(true);
  271. range.moveStart('character', startOffset);
  272. range.moveEnd('character', endOffset - startOffset);
  273. range.select();
  274. }
  275. }
  276. },
  277. /**
  278. * Select the range included within the bookmark text with the bookmark
  279. * text removed.
  280. * @param {Object} bookmark Exactly the one created by CKEDITOR.dom.range.createBookmark( true ).
  281. */
  282. moveToBookmark: function(bookmark, editor) {
  283. var content = this.content;
  284. function removeBookmarkText(bookmarkId) {
  285. var bookmarkRegex = new RegExp('<span[^<]*?' + bookmarkId + '.*?/span>'),
  286. offset;
  287. content = content.replace(bookmarkRegex, function(str, index) {
  288. offset = index;
  289. return '';
  290. });
  291. return offset;
  292. }
  293. this.startOffset = removeBookmarkText(bookmark.startNode);
  294. this.endOffset = removeBookmarkText(bookmark.endNode);
  295. var savedContent = editor._.previousModeData;
  296. if (savedContent.length > 0) {
  297. var diff = content.length - savedContent.length;
  298. this.startOffset = this.startOffset - diff;
  299. if (diff > 0) {
  300. //this.endOffset = this.endOffset - diff;
  301. }
  302. content = editor._.previousModeData;
  303. this.content = content;
  304. this.updateElement();
  305. if (editor.undoManager) {
  306. editor.undoManager.unlock();
  307. }
  308. }
  309. },
  310. /**
  311. * If startOffset/endOffset anchor inside element tag, start the range before/after the element
  312. */
  313. enlarge: function() {
  314. var htmlOpenTagRegexp = /<[a-zA-Z]+(>|.*?[^?]>)/g;
  315. var htmlCloseTagRegexp = /<\/[^>]+>/g;
  316. var content = this.content,
  317. start = this.startOffset,
  318. end = this.endOffset,
  319. match,
  320. tagStartIndex,
  321. tagEndIndex;
  322. while (match = htmlCloseTagRegexp.exec(content)) {
  323. tagStartIndex = match.index;
  324. tagEndIndex = tagStartIndex + match[0].length;
  325. if (tagEndIndex < start) {
  326. continue;
  327. }
  328. if (tagStartIndex <= start) {
  329. start = tagStartIndex;
  330. end = tagStartIndex;
  331. break;
  332. }
  333. }
  334. while (match = htmlOpenTagRegexp.exec(content)) {
  335. tagStartIndex = match.index;
  336. tagEndIndex = tagStartIndex + match[0].length;
  337. if (tagEndIndex < start) {
  338. continue;
  339. }
  340. if (tagStartIndex <= start) {
  341. start = tagEndIndex;
  342. end = tagEndIndex;
  343. break;
  344. }
  345. }
  346. this.startOffset = start;
  347. this.endOffset = end;
  348. },
  349. createBookmark: function (editor) {
  350. // Enlarge the range to avoid tag partial selection.
  351. this.enlarge();
  352. var content = this.content,
  353. start = this.startOffset,
  354. end = this.endOffset,
  355. id = CKEDITOR.tools.getNextNumber(),
  356. bookmarkTemplate = '<!--cke_bookmark_%1-->';
  357. content = content.substring(0, start) + bookmarkTemplate.replace('%1', id + 'S')
  358. + content.substring(start, end) + bookmarkTemplate.replace('%1', id + 'E')
  359. + content.substring(end);
  360. if (editor.undoManager) {
  361. editor.undoManager.lock();
  362. }
  363. this.content = content;
  364. this.updateElement();
  365. },
  366. updateElement: function() {
  367. if (this.element)
  368. this.element.$.value = this.content;
  369. }
  370. };
  371. var Browser = {
  372. Version: function() {
  373. var version = 999;
  374. if (navigator.appVersion.indexOf("MSIE") != -1)
  375. version = parseFloat(navigator.appVersion.split("MSIE")[1]);
  376. return version;
  377. }
  378. };
  379. // Seamless selection range across different modes.
  380. CKEDITOR.config.syncSelection = true;
  381. var textRange,sourceBookmark;