Quirks.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  1. /**
  2. * Quirks.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. * @ignore-file
  11. */
  12. /**
  13. * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes.
  14. *
  15. * @class tinymce.util.Quirks
  16. */
  17. define("tinymce/util/Quirks", [
  18. "tinymce/util/VK",
  19. "tinymce/dom/RangeUtils",
  20. "tinymce/html/Node",
  21. "tinymce/html/Entities",
  22. "tinymce/Env",
  23. "tinymce/util/Tools"
  24. ], function(VK, RangeUtils, Node, Entities, Env, Tools) {
  25. return function(editor) {
  26. var each = Tools.each;
  27. var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection,
  28. settings = editor.settings, parser = editor.parser, serializer = editor.serializer;
  29. var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit;
  30. /**
  31. * Executes a command with a specific state this can be to enable/disable browser editing features.
  32. */
  33. function setEditorCommandState(cmd, state) {
  34. try {
  35. editor.getDoc().execCommand(cmd, false, state);
  36. } catch (ex) {
  37. // Ignore
  38. }
  39. }
  40. /**
  41. * Returns current IE document mode.
  42. */
  43. function getDocumentMode() {
  44. var documentMode = editor.getDoc().documentMode;
  45. return documentMode ? documentMode : 6;
  46. }
  47. /**
  48. * Returns true/false if the event is prevented or not.
  49. *
  50. * @private
  51. * @param {Event} e Event object.
  52. * @return {Boolean} true/false if the event is prevented or not.
  53. */
  54. function isDefaultPrevented(e) {
  55. return e.isDefaultPrevented();
  56. }
  57. /**
  58. * Fixes a WebKit bug when deleting contents using backspace or delete key.
  59. * WebKit will produce a span element if you delete across two block elements.
  60. *
  61. * Example:
  62. * <h1>a</h1><p>|b</p>
  63. *
  64. * Will produce this on backspace:
  65. * <h1>a<span style="<all runtime styles>">b</span></p>
  66. *
  67. * This fixes the backspace to produce:
  68. * <h1>a|b</p>
  69. *
  70. * See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
  71. *
  72. * This fixes the following delete scenarios:
  73. * 1. Delete by pressing backspace key.
  74. * 2. Delete by pressing delete key.
  75. * 3. Delete by pressing backspace key with ctrl/cmd (Word delete).
  76. * 4. Delete by pressing delete key with ctrl/cmd (Word delete).
  77. * 5. Delete by drag/dropping contents inside the editor.
  78. * 6. Delete by using Cut Ctrl+X/Cmd+X.
  79. * 7. Delete by selecting contents and writing a character.'
  80. *
  81. * This code is a ugly hack since writing full custom delete logic for just this bug
  82. * fix seemed like a huge task. I hope we can remove this before the year 2030.
  83. */
  84. function cleanupStylesWhenDeleting() {
  85. var doc = editor.getDoc(), urlPrefix = 'data:text/mce-internal,';
  86. var MutationObserver = window.MutationObserver, olderWebKit, dragStartRng;
  87. // Add mini polyfill for older WebKits
  88. // TODO: Remove this when old Safari versions gets updated
  89. if (!MutationObserver) {
  90. olderWebKit = true;
  91. MutationObserver = function() {
  92. var records = [], target;
  93. function nodeInsert(e) {
  94. var target = e.relatedNode || e.target;
  95. records.push({target: target, addedNodes: [target]});
  96. }
  97. function attrModified(e) {
  98. var target = e.relatedNode || e.target;
  99. records.push({target: target, attributeName: e.attrName});
  100. }
  101. this.observe = function(node) {
  102. target = node;
  103. target.addEventListener('DOMSubtreeModified', nodeInsert, false);
  104. target.addEventListener('DOMNodeInsertedIntoDocument', nodeInsert, false);
  105. target.addEventListener('DOMNodeInserted', nodeInsert, false);
  106. target.addEventListener('DOMAttrModified', attrModified, false);
  107. };
  108. this.disconnect = function() {
  109. target.removeEventListener('DOMNodeInserted', nodeInsert);
  110. target.removeEventListener('DOMAttrModified', attrModified);
  111. target.removeEventListener('DOMSubtreeModified', nodeInsert, false);
  112. };
  113. this.takeRecords = function() {
  114. return records;
  115. };
  116. };
  117. }
  118. function customDelete(isForward) {
  119. var mutationObserver = new MutationObserver(function() {});
  120. Tools.each(editor.getBody().getElementsByTagName('*'), function(elm) {
  121. // Mark existing spans
  122. if (elm.tagName == 'SPAN') {
  123. elm.setAttribute('mce-data-marked', 1);
  124. }
  125. // Make sure all elements has a data-mce-style attribute
  126. if (!elm.hasAttribute('data-mce-style') && elm.hasAttribute('style')) {
  127. editor.dom.setAttrib(elm, 'style', elm.getAttribute('style'));
  128. }
  129. });
  130. // Observe added nodes and style attribute changes
  131. mutationObserver.observe(editor.getDoc(), {
  132. childList: true,
  133. attributes: true,
  134. subtree: true,
  135. attributeFilter: ['style']
  136. });
  137. editor.getDoc().execCommand(isForward ? 'ForwardDelete' : 'Delete', false, null);
  138. var rng = editor.selection.getRng();
  139. var caretElement = rng.startContainer.parentNode;
  140. Tools.each(mutationObserver.takeRecords(), function(record) {
  141. // Restore style attribute to previous value
  142. if (record.attributeName == "style") {
  143. var oldValue = record.target.getAttribute('data-mce-style');
  144. if (oldValue) {
  145. record.target.setAttribute("style", oldValue);
  146. } else {
  147. record.target.removeAttribute("style");
  148. }
  149. }
  150. // Remove all spans that isn't maked and retain selection
  151. Tools.each(record.addedNodes, function(node) {
  152. if (node.nodeName == "SPAN" && !node.getAttribute('mce-data-marked')) {
  153. var offset, container;
  154. if (node == caretElement) {
  155. offset = rng.startOffset;
  156. container = node.firstChild;
  157. }
  158. dom.remove(node, true);
  159. if (container) {
  160. rng.setStart(container, offset);
  161. rng.setEnd(container, offset);
  162. editor.selection.setRng(rng);
  163. }
  164. }
  165. });
  166. });
  167. mutationObserver.disconnect();
  168. // Remove any left over marks
  169. Tools.each(editor.dom.select('span[mce-data-marked]'), function(span) {
  170. span.removeAttribute('mce-data-marked');
  171. });
  172. }
  173. editor.on('keydown', function(e) {
  174. var isForward = e.keyCode == DELETE, isMeta = VK.metaKeyPressed(e);
  175. if (!isDefaultPrevented(e) && (isForward || e.keyCode == BACKSPACE)) {
  176. var rng = editor.selection.getRng(), container = rng.startContainer, offset = rng.startOffset;
  177. // Ignore non meta delete in the where there is text before/after the caret
  178. if (!isMeta && rng.collapsed && container.nodeType == 3) {
  179. if (isForward ? offset < container.data.length : offset > 0) {
  180. return;
  181. }
  182. }
  183. e.preventDefault();
  184. if (isMeta) {
  185. editor.selection.getSel().modify("extend", isForward ? "forward" : "backward", "word");
  186. }
  187. customDelete(isForward);
  188. }
  189. });
  190. editor.on('keypress', function(e) {
  191. if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode && !VK.metaKeyPressed(e)) {
  192. e.preventDefault();
  193. customDelete(true);
  194. editor.selection.setContent(String.fromCharCode(e.charCode));
  195. }
  196. });
  197. editor.addCommand('Delete', function() {
  198. customDelete();
  199. });
  200. editor.addCommand('ForwardDelete', function() {
  201. customDelete(true);
  202. });
  203. // Older WebKits doesn't properly handle the clipboard so we can't add the rest
  204. if (olderWebKit) {
  205. return;
  206. }
  207. editor.on('dragstart', function(e) {
  208. var selectionHtml;
  209. if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') {
  210. selection.select(e.target);
  211. }
  212. dragStartRng = selection.getRng();
  213. selectionHtml = editor.selection.getContent();
  214. // Safari doesn't support custom dataTransfer items so we can only use URL and Text
  215. if (selectionHtml.length > 0) {
  216. e.dataTransfer.setData('URL', 'data:text/mce-internal,' + escape(selectionHtml));
  217. }
  218. });
  219. editor.on('drop', function(e) {
  220. if (!isDefaultPrevented(e)) {
  221. var internalContent = e.dataTransfer.getData('URL');
  222. if (!internalContent || internalContent.indexOf(urlPrefix) == -1 || !doc.caretRangeFromPoint) {
  223. return;
  224. }
  225. internalContent = unescape(internalContent.substr(urlPrefix.length));
  226. if (doc.caretRangeFromPoint) {
  227. e.preventDefault();
  228. // Safari has a weird issue where drag/dropping images sometimes
  229. // produces a green plus icon. When this happens the caretRangeFromPoint
  230. // will return "null" even though the x, y coordinate is correct.
  231. // But if we detach the insert from the drop event we will get a proper range
  232. window.setTimeout(function() {
  233. var pointRng = doc.caretRangeFromPoint(e.x, e.y);
  234. if (dragStartRng) {
  235. selection.setRng(dragStartRng);
  236. dragStartRng = null;
  237. }
  238. customDelete();
  239. selection.setRng(pointRng);
  240. editor.insertContent(internalContent);
  241. }, 0);
  242. }
  243. }
  244. });
  245. editor.on('cut', function(e) {
  246. if (!isDefaultPrevented(e) && e.clipboardData) {
  247. e.preventDefault();
  248. e.clipboardData.clearData();
  249. e.clipboardData.setData('text/html', editor.selection.getContent());
  250. e.clipboardData.setData('text/plain', editor.selection.getContent({format: 'text'}));
  251. customDelete(true);
  252. }
  253. });
  254. }
  255. /**
  256. * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
  257. *
  258. * For example:
  259. * <p><b>|</b></p>
  260. *
  261. * Or:
  262. * <h1>|</h1>
  263. *
  264. * Or:
  265. * [<h1></h1>]
  266. */
  267. function emptyEditorWhenDeleting() {
  268. function serializeRng(rng) {
  269. var body = dom.create("body");
  270. var contents = rng.cloneContents();
  271. body.appendChild(contents);
  272. return selection.serializer.serialize(body, {format: 'html'});
  273. }
  274. function allContentsSelected(rng) {
  275. if (!rng.setStart) {
  276. if (rng.item) {
  277. return false;
  278. }
  279. var bodyRng = rng.duplicate();
  280. bodyRng.moveToElementText(editor.getBody());
  281. return RangeUtils.compareRanges(rng, bodyRng);
  282. }
  283. var selection = serializeRng(rng);
  284. var allRng = dom.createRng();
  285. allRng.selectNode(editor.getBody());
  286. var allSelection = serializeRng(allRng);
  287. return selection === allSelection;
  288. }
  289. editor.on('keydown', function(e) {
  290. var keyCode = e.keyCode, isCollapsed, body;
  291. // Empty the editor if it's needed for example backspace at <p><b>|</b></p>
  292. if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
  293. isCollapsed = editor.selection.isCollapsed();
  294. body = editor.getBody();
  295. // Selection is collapsed but the editor isn't empty
  296. if (isCollapsed && !dom.isEmpty(body)) {
  297. return;
  298. }
  299. // Selection isn't collapsed but not all the contents is selected
  300. if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) {
  301. return;
  302. }
  303. // Manually empty the editor
  304. e.preventDefault();
  305. editor.setContent('');
  306. if (body.firstChild && dom.isBlock(body.firstChild)) {
  307. editor.selection.setCursorLocation(body.firstChild, 0);
  308. } else {
  309. editor.selection.setCursorLocation(body, 0);
  310. }
  311. editor.nodeChanged();
  312. }
  313. });
  314. }
  315. /**
  316. * WebKit doesn't select all the nodes in the body when you press Ctrl+A.
  317. * IE selects more than the contents <body>[<p>a</p>]</body> instead of <body><p>[a]</p]</body> see bug #6438
  318. * This selects the whole body so that backspace/delete logic will delete everything
  319. */
  320. function selectAll() {
  321. editor.on('keydown', function(e) {
  322. if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) {
  323. e.preventDefault();
  324. editor.execCommand('SelectAll');
  325. }
  326. });
  327. }
  328. /**
  329. * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes.
  330. * The IME on Mac doesn't initialize when it doesn't fire a proper focus event.
  331. *
  332. * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until
  333. * you enter a character into the editor.
  334. *
  335. * It also happens when the first focus in made to the body.
  336. *
  337. * See: https://bugs.webkit.org/show_bug.cgi?id=83566
  338. */
  339. function inputMethodFocus() {
  340. if (!editor.settings.content_editable) {
  341. // Case 1 IME doesn't initialize if you focus the document
  342. dom.bind(editor.getDoc(), 'focusin', function() {
  343. selection.setRng(selection.getRng());
  344. });
  345. // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event
  346. dom.bind(editor.getDoc(), 'mousedown', function(e) {
  347. if (e.target == editor.getDoc().documentElement) {
  348. editor.getBody().focus();
  349. selection.setRng(selection.getRng());
  350. }
  351. });
  352. }
  353. }
  354. /**
  355. * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the
  356. * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is
  357. * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js
  358. * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other
  359. * browsers.
  360. *
  361. * It also fixes a bug on Firefox where it's impossible to delete HR elements.
  362. */
  363. function removeHrOnBackspace() {
  364. editor.on('keydown', function(e) {
  365. if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
  366. if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
  367. var node = selection.getNode();
  368. var previousSibling = node.previousSibling;
  369. if (node.nodeName == 'HR') {
  370. dom.remove(node);
  371. e.preventDefault();
  372. return;
  373. }
  374. if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") {
  375. dom.remove(previousSibling);
  376. e.preventDefault();
  377. }
  378. }
  379. }
  380. });
  381. }
  382. /**
  383. * Firefox 3.x has an issue where the body element won't get proper focus if you click out
  384. * side it's rectangle.
  385. */
  386. function focusBody() {
  387. // Fix for a focus bug in FF 3.x where the body element
  388. // wouldn't get proper focus if the user clicked on the HTML element
  389. if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4
  390. editor.on('mousedown', function(e) {
  391. if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") {
  392. var body = editor.getBody();
  393. // Blur the body it's focused but not correctly focused
  394. body.blur();
  395. // Refocus the body after a little while
  396. setTimeout(function() {
  397. body.focus();
  398. }, 0);
  399. }
  400. });
  401. }
  402. }
  403. /**
  404. * WebKit has a bug where it isn't possible to select image, hr or anchor elements
  405. * by clicking on them so we need to fake that.
  406. */
  407. function selectControlElements() {
  408. editor.on('click', function(e) {
  409. e = e.target;
  410. // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250
  411. // WebKit can't even do simple things like selecting an image
  412. // Needs tobe the setBaseAndExtend or it will fail to select floated images
  413. if (/^(IMG|HR)$/.test(e.nodeName)) {
  414. selection.getSel().setBaseAndExtent(e, 0, e, 1);
  415. }
  416. if (e.nodeName == 'A' && dom.hasClass(e, 'mce-item-anchor')) {
  417. selection.select(e);
  418. }
  419. editor.nodeChanged();
  420. });
  421. }
  422. /**
  423. * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
  424. *
  425. * Fixes do backspace/delete on this:
  426. * <p>bla[ck</p><p style="color:red">r]ed</p>
  427. *
  428. * Would become:
  429. * <p>bla|ed</p>
  430. *
  431. * Instead of:
  432. * <p style="color:red">bla|ed</p>
  433. */
  434. function removeStylesWhenDeletingAcrossBlockElements() {
  435. function getAttributeApplyFunction() {
  436. var template = dom.getAttribs(selection.getStart().cloneNode(false));
  437. return function() {
  438. var target = selection.getStart();
  439. if (target !== editor.getBody()) {
  440. dom.setAttrib(target, "style", null);
  441. each(template, function(attr) {
  442. target.setAttributeNode(attr.cloneNode(true));
  443. });
  444. }
  445. };
  446. }
  447. function isSelectionAcrossElements() {
  448. return !selection.isCollapsed() &&
  449. dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock);
  450. }
  451. editor.on('keypress', function(e) {
  452. var applyAttributes;
  453. if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) {
  454. applyAttributes = getAttributeApplyFunction();
  455. editor.getDoc().execCommand('delete', false, null);
  456. applyAttributes();
  457. e.preventDefault();
  458. return false;
  459. }
  460. });
  461. dom.bind(editor.getDoc(), 'cut', function(e) {
  462. var applyAttributes;
  463. if (!isDefaultPrevented(e) && isSelectionAcrossElements()) {
  464. applyAttributes = getAttributeApplyFunction();
  465. setTimeout(function() {
  466. applyAttributes();
  467. }, 0);
  468. }
  469. });
  470. }
  471. /**
  472. * Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5. It only fires the nodeChange
  473. * event every 50ms since it would other wise update the UI when you type and it hogs the CPU.
  474. */
  475. function selectionChangeNodeChanged() {
  476. var lastRng, selectionTimer;
  477. editor.on('selectionchange', function() {
  478. if (selectionTimer) {
  479. clearTimeout(selectionTimer);
  480. selectionTimer = 0;
  481. }
  482. selectionTimer = window.setTimeout(function() {
  483. if (editor.removed) {
  484. return;
  485. }
  486. var rng = selection.getRng();
  487. // Compare the ranges to see if it was a real change or not
  488. if (!lastRng || !RangeUtils.compareRanges(rng, lastRng)) {
  489. editor.nodeChanged();
  490. lastRng = rng;
  491. }
  492. }, 50);
  493. });
  494. }
  495. /**
  496. * Screen readers on IE needs to have the role application set on the body.
  497. */
  498. function ensureBodyHasRoleApplication() {
  499. document.body.setAttribute("role", "application");
  500. }
  501. /**
  502. * Backspacing into a table behaves differently depending upon browser type.
  503. * Therefore, disable Backspace when cursor immediately follows a table.
  504. */
  505. function disableBackspaceIntoATable() {
  506. editor.on('keydown', function(e) {
  507. if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) {
  508. if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) {
  509. var previousSibling = selection.getNode().previousSibling;
  510. if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") {
  511. e.preventDefault();
  512. return false;
  513. }
  514. }
  515. }
  516. });
  517. }
  518. /**
  519. * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this
  520. * logic adds a \n before the BR so that it will get rendered.
  521. */
  522. function addNewLinesBeforeBrInPre() {
  523. // IE8+ rendering mode does the right thing with BR in PRE
  524. if (getDocumentMode() > 7) {
  525. return;
  526. }
  527. // Enable display: none in area and add a specific class that hides all BR elements in PRE to
  528. // avoid the caret from getting stuck at the BR elements while pressing the right arrow key
  529. setEditorCommandState('RespectVisibilityInDesign', true);
  530. editor.contentStyles.push('.mceHideBrInPre pre br {display: none}');
  531. dom.addClass(editor.getBody(), 'mceHideBrInPre');
  532. // Adds a \n before all BR elements in PRE to get them visual
  533. parser.addNodeFilter('pre', function(nodes) {
  534. var i = nodes.length, brNodes, j, brElm, sibling;
  535. while (i--) {
  536. brNodes = nodes[i].getAll('br');
  537. j = brNodes.length;
  538. while (j--) {
  539. brElm = brNodes[j];
  540. // Add \n before BR in PRE elements on older IE:s so the new lines get rendered
  541. sibling = brElm.prev;
  542. if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') {
  543. sibling.value += '\n';
  544. } else {
  545. brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n';
  546. }
  547. }
  548. }
  549. });
  550. // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible
  551. serializer.addNodeFilter('pre', function(nodes) {
  552. var i = nodes.length, brNodes, j, brElm, sibling;
  553. while (i--) {
  554. brNodes = nodes[i].getAll('br');
  555. j = brNodes.length;
  556. while (j--) {
  557. brElm = brNodes[j];
  558. sibling = brElm.prev;
  559. if (sibling && sibling.type == 3) {
  560. sibling.value = sibling.value.replace(/\r?\n$/, '');
  561. }
  562. }
  563. }
  564. });
  565. }
  566. /**
  567. * Moves style width/height to attribute width/height when the user resizes an image on IE.
  568. */
  569. function removePreSerializedStylesWhenSelectingControls() {
  570. dom.bind(editor.getBody(), 'mouseup', function() {
  571. var value, node = selection.getNode();
  572. // Moved styles to attributes on IMG eements
  573. if (node.nodeName == 'IMG') {
  574. // Convert style width to width attribute
  575. if ((value = dom.getStyle(node, 'width'))) {
  576. dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, ''));
  577. dom.setStyle(node, 'width', '');
  578. }
  579. // Convert style height to height attribute
  580. if ((value = dom.getStyle(node, 'height'))) {
  581. dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, ''));
  582. dom.setStyle(node, 'height', '');
  583. }
  584. }
  585. });
  586. }
  587. /**
  588. * Removes a blockquote when backspace is pressed at the beginning of it.
  589. *
  590. * For example:
  591. * <blockquote><p>|x</p></blockquote>
  592. *
  593. * Becomes:
  594. * <p>|x</p>
  595. */
  596. function removeBlockQuoteOnBackSpace() {
  597. // Add block quote deletion handler
  598. editor.on('keydown', function(e) {
  599. var rng, container, offset, root, parent;
  600. if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) {
  601. return;
  602. }
  603. rng = selection.getRng();
  604. container = rng.startContainer;
  605. offset = rng.startOffset;
  606. root = dom.getRoot();
  607. parent = container;
  608. if (!rng.collapsed || offset !== 0) {
  609. return;
  610. }
  611. while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) {
  612. parent = parent.parentNode;
  613. }
  614. // Is the cursor at the beginning of a blockquote?
  615. if (parent.tagName === 'BLOCKQUOTE') {
  616. // Remove the blockquote
  617. editor.formatter.toggle('blockquote', null, parent);
  618. // Move the caret to the beginning of container
  619. rng = dom.createRng();
  620. rng.setStart(container, 0);
  621. rng.setEnd(container, 0);
  622. selection.setRng(rng);
  623. }
  624. });
  625. }
  626. /**
  627. * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc.
  628. */
  629. function setGeckoEditingOptions() {
  630. function setOpts() {
  631. editor._refreshContentEditable();
  632. setEditorCommandState("StyleWithCSS", false);
  633. setEditorCommandState("enableInlineTableEditing", false);
  634. if (!settings.object_resizing) {
  635. setEditorCommandState("enableObjectResizing", false);
  636. }
  637. }
  638. if (!settings.readonly) {
  639. editor.on('BeforeExecCommand MouseDown', setOpts);
  640. }
  641. }
  642. /**
  643. * Fixes a gecko link bug, when a link is placed at the end of block elements there is
  644. * no way to move the caret behind the link. This fix adds a bogus br element after the link.
  645. *
  646. * For example this:
  647. * <p><b><a href="#">x</a></b></p>
  648. *
  649. * Becomes this:
  650. * <p><b><a href="#">x</a></b><br></p>
  651. */
  652. function addBrAfterLastLinks() {
  653. function fixLinks() {
  654. each(dom.select('a'), function(node) {
  655. var parentNode = node.parentNode, root = dom.getRoot();
  656. if (parentNode.lastChild === node) {
  657. while (parentNode && !dom.isBlock(parentNode)) {
  658. if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) {
  659. return;
  660. }
  661. parentNode = parentNode.parentNode;
  662. }
  663. dom.add(parentNode, 'br', {'data-mce-bogus': 1});
  664. }
  665. });
  666. }
  667. editor.on('SetContent ExecCommand', function(e) {
  668. if (e.type == "setcontent" || e.command === 'mceInsertLink') {
  669. fixLinks();
  670. }
  671. });
  672. }
  673. /**
  674. * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by
  675. * default we want to change that behavior.
  676. */
  677. function setDefaultBlockType() {
  678. if (settings.forced_root_block) {
  679. editor.on('init', function() {
  680. setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block);
  681. });
  682. }
  683. }
  684. /**
  685. * Removes ghost selections from images/tables on Gecko.
  686. */
  687. function removeGhostSelection() {
  688. editor.on('Undo Redo SetContent', function(e) {
  689. if (!e.initial) {
  690. editor.execCommand('mceRepaint');
  691. }
  692. });
  693. }
  694. /**
  695. * Deletes the selected image on IE instead of navigating to previous page.
  696. */
  697. function deleteControlItemOnBackSpace() {
  698. editor.on('keydown', function(e) {
  699. var rng;
  700. if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) {
  701. rng = editor.getDoc().selection.createRange();
  702. if (rng && rng.item) {
  703. e.preventDefault();
  704. editor.undoManager.beforeChange();
  705. dom.remove(rng.item(0));
  706. editor.undoManager.add();
  707. }
  708. }
  709. });
  710. }
  711. /**
  712. * IE10 doesn't properly render block elements with the right height until you add contents to them.
  713. * This fixes that by adding a padding-right to all empty text block elements.
  714. * See: https://connect.microsoft.com/IE/feedback/details/743881
  715. */
  716. function renderEmptyBlocksFix() {
  717. var emptyBlocksCSS;
  718. // IE10+
  719. if (getDocumentMode() >= 10) {
  720. emptyBlocksCSS = '';
  721. each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) {
  722. emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty';
  723. });
  724. editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}');
  725. }
  726. }
  727. /**
  728. * Old IE versions can't retain contents within noscript elements so this logic will store the contents
  729. * as a attribute and the insert that value as it's raw text when the DOM is serialized.
  730. */
  731. function keepNoScriptContents() {
  732. if (getDocumentMode() < 9) {
  733. parser.addNodeFilter('noscript', function(nodes) {
  734. var i = nodes.length, node, textNode;
  735. while (i--) {
  736. node = nodes[i];
  737. textNode = node.firstChild;
  738. if (textNode) {
  739. node.attr('data-mce-innertext', textNode.value);
  740. }
  741. }
  742. });
  743. serializer.addNodeFilter('noscript', function(nodes) {
  744. var i = nodes.length, node, textNode, value;
  745. while (i--) {
  746. node = nodes[i];
  747. textNode = nodes[i].firstChild;
  748. if (textNode) {
  749. textNode.value = Entities.decode(textNode.value);
  750. } else {
  751. // Old IE can't retain noscript value so an attribute is used to store it
  752. value = node.attributes.map['data-mce-innertext'];
  753. if (value) {
  754. node.attr('data-mce-innertext', null);
  755. textNode = new Node('#text', 3);
  756. textNode.value = value;
  757. textNode.raw = true;
  758. node.append(textNode);
  759. }
  760. }
  761. }
  762. });
  763. }
  764. }
  765. /**
  766. * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode.
  767. */
  768. function fixCaretSelectionOfDocumentElementOnIe() {
  769. var doc = dom.doc, body = doc.body, started, startRng, htmlElm;
  770. // Return range from point or null if it failed
  771. function rngFromPoint(x, y) {
  772. var rng = body.createTextRange();
  773. try {
  774. rng.moveToPoint(x, y);
  775. } catch (ex) {
  776. // IE sometimes throws and exception, so lets just ignore it
  777. rng = null;
  778. }
  779. return rng;
  780. }
  781. // Fires while the selection is changing
  782. function selectionChange(e) {
  783. var pointRng;
  784. // Check if the button is down or not
  785. if (e.button) {
  786. // Create range from mouse position
  787. pointRng = rngFromPoint(e.x, e.y);
  788. if (pointRng) {
  789. // Check if pointRange is before/after selection then change the endPoint
  790. if (pointRng.compareEndPoints('StartToStart', startRng) > 0) {
  791. pointRng.setEndPoint('StartToStart', startRng);
  792. } else {
  793. pointRng.setEndPoint('EndToEnd', startRng);
  794. }
  795. pointRng.select();
  796. }
  797. } else {
  798. endSelection();
  799. }
  800. }
  801. // Removes listeners
  802. function endSelection() {
  803. var rng = doc.selection.createRange();
  804. // If the range is collapsed then use the last start range
  805. if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) {
  806. startRng.select();
  807. }
  808. dom.unbind(doc, 'mouseup', endSelection);
  809. dom.unbind(doc, 'mousemove', selectionChange);
  810. startRng = started = 0;
  811. }
  812. // Make HTML element unselectable since we are going to handle selection by hand
  813. doc.documentElement.unselectable = true;
  814. // Detect when user selects outside BODY
  815. dom.bind(doc, 'mousedown contextmenu', function(e) {
  816. if (e.target.nodeName === 'HTML') {
  817. if (started) {
  818. endSelection();
  819. }
  820. // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML
  821. htmlElm = doc.documentElement;
  822. if (htmlElm.scrollHeight > htmlElm.clientHeight) {
  823. return;
  824. }
  825. started = 1;
  826. // Setup start position
  827. startRng = rngFromPoint(e.x, e.y);
  828. if (startRng) {
  829. // Listen for selection change events
  830. dom.bind(doc, 'mouseup', endSelection);
  831. dom.bind(doc, 'mousemove', selectionChange);
  832. dom.getRoot().focus();
  833. startRng.select();
  834. }
  835. }
  836. });
  837. }
  838. /**
  839. * Fixes selection issues where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
  840. * this fix will lean the caret right into the closest inline element.
  841. */
  842. function normalizeSelection() {
  843. // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
  844. editor.on('keyup focusin mouseup', function(e) {
  845. if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
  846. selection.normalize();
  847. }
  848. }, true);
  849. }
  850. /**
  851. * Forces Gecko to render a broken image icon if it fails to load an image.
  852. */
  853. function showBrokenImageIcon() {
  854. editor.contentStyles.push(
  855. 'img:-moz-broken {' +
  856. '-moz-force-broken-image-icon:1;' +
  857. 'min-width:24px;' +
  858. 'min-height:24px' +
  859. '}'
  860. );
  861. }
  862. /**
  863. * iOS has a bug where it's impossible to type if the document has a touchstart event
  864. * bound and the user touches the document while having the on screen keyboard visible.
  865. *
  866. * The touch event moves the focus to the parent document while having the caret inside the iframe
  867. * this fix moves the focus back into the iframe document.
  868. */
  869. function restoreFocusOnKeyDown() {
  870. if (!editor.inline) {
  871. editor.on('keydown', function() {
  872. if (document.activeElement == document.body) {
  873. editor.getWin().focus();
  874. }
  875. });
  876. }
  877. }
  878. /**
  879. * IE 11 has an annoying issue where you can't move focus into the editor
  880. * by clicking on the white area HTML element. We used to be able to to fix this with
  881. * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection
  882. * object it's not possible anymore. So we need to hack in a ungly CSS to force the
  883. * body to be at least 150px. If the user clicks the HTML element out side this 150px region
  884. * we simply move the focus into the first paragraph. Not ideal since you loose the
  885. * positioning of the caret but goot enough for most cases.
  886. */
  887. function bodyHeight() {
  888. if (!editor.inline) {
  889. editor.contentStyles.push('body {min-height: 150px}');
  890. editor.on('click', function(e) {
  891. if (e.target.nodeName == 'HTML') {
  892. editor.getBody().focus();
  893. editor.selection.normalize();
  894. editor.nodeChanged();
  895. }
  896. });
  897. }
  898. }
  899. /**
  900. * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow.
  901. * You might then loose all your work so we need to block that behavior and replace it with our own.
  902. */
  903. function blockCmdArrowNavigation() {
  904. if (Env.mac) {
  905. editor.on('keydown', function(e) {
  906. if (VK.metaKeyPressed(e) && (e.keyCode == 37 || e.keyCode == 39)) {
  907. e.preventDefault();
  908. editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'word');
  909. }
  910. });
  911. }
  912. }
  913. /**
  914. * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin.
  915. */
  916. function disableAutoUrlDetect() {
  917. setEditorCommandState("AutoUrlDetect", false);
  918. }
  919. /**
  920. * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when
  921. * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync
  922. * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML
  923. * but not as the lastChild of the body. However is we add a BR element to the body then remove it
  924. * it doesn't seem to add these BR elements makes sence right?!
  925. *
  926. * Example of what happens: <body>text</body> becomes <body>text<br><br></body>
  927. */
  928. function doubleTrailingBrElements() {
  929. if (!editor.inline) {
  930. editor.on('focus blur beforegetcontent', function() {
  931. var br = editor.dom.create('br');
  932. editor.getBody().appendChild(br);
  933. br.parentNode.removeChild(br);
  934. }, true);
  935. }
  936. }
  937. /**
  938. * iOS 7.1 introduced two new bugs:
  939. * 1) It's possible to open links within a contentEditable area by clicking on them.
  940. * 2) If you hold down the finger it will display the link/image touch callout menu.
  941. */
  942. function tapLinksAndImages() {
  943. editor.on('click', function(e) {
  944. var elm = e.target;
  945. do {
  946. if (elm.tagName === 'A') {
  947. e.preventDefault();
  948. return;
  949. }
  950. } while ((elm = elm.parentNode));
  951. });
  952. editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}');
  953. }
  954. /**
  955. * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element.
  956. * For example this: <form><button></form>
  957. */
  958. function blockFormSubmitInsideEditor() {
  959. editor.on('init', function() {
  960. editor.dom.bind(editor.getBody(), 'submit', function(e) {
  961. e.preventDefault();
  962. });
  963. });
  964. }
  965. // All browsers
  966. disableBackspaceIntoATable();
  967. removeBlockQuoteOnBackSpace();
  968. emptyEditorWhenDeleting();
  969. normalizeSelection();
  970. // WebKit
  971. if (isWebKit) {
  972. cleanupStylesWhenDeleting();
  973. inputMethodFocus();
  974. selectControlElements();
  975. setDefaultBlockType();
  976. blockFormSubmitInsideEditor();
  977. // iOS
  978. if (Env.iOS) {
  979. selectionChangeNodeChanged();
  980. restoreFocusOnKeyDown();
  981. bodyHeight();
  982. tapLinksAndImages();
  983. } else {
  984. selectAll();
  985. }
  986. }
  987. // IE
  988. if (isIE && Env.ie < 11) {
  989. removeHrOnBackspace();
  990. ensureBodyHasRoleApplication();
  991. addNewLinesBeforeBrInPre();
  992. removePreSerializedStylesWhenSelectingControls();
  993. deleteControlItemOnBackSpace();
  994. renderEmptyBlocksFix();
  995. keepNoScriptContents();
  996. fixCaretSelectionOfDocumentElementOnIe();
  997. }
  998. if (Env.ie >= 11) {
  999. bodyHeight();
  1000. doubleTrailingBrElements();
  1001. }
  1002. if (Env.ie) {
  1003. selectAll();
  1004. disableAutoUrlDetect();
  1005. }
  1006. // Gecko
  1007. if (isGecko) {
  1008. removeHrOnBackspace();
  1009. focusBody();
  1010. removeStylesWhenDeletingAcrossBlockElements();
  1011. setGeckoEditingOptions();
  1012. addBrAfterLastLinks();
  1013. removeGhostSelection();
  1014. showBrokenImageIcon();
  1015. blockCmdArrowNavigation();
  1016. }
  1017. };
  1018. });