ControlSelection.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. /**
  2. * ControlSelection.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 control selection of elements. Controls are elements
  12. * that can be resized and needs to be selected as a whole. It adds custom resize handles
  13. * to all browser engines that support properly disabling the built in resize logic.
  14. *
  15. * @class tinymce.dom.ControlSelection
  16. */
  17. define("tinymce/dom/ControlSelection", [
  18. "tinymce/util/VK",
  19. "tinymce/util/Tools",
  20. "tinymce/Env"
  21. ], function(VK, Tools, Env) {
  22. return function(selection, editor) {
  23. var dom = editor.dom, each = Tools.each;
  24. var selectedElm, selectedElmGhost, resizeHandles, selectedHandle, lastMouseDownEvent;
  25. var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted;
  26. var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11;
  27. // Details about each resize handle how to scale etc
  28. resizeHandles = {
  29. // Name: x multiplier, y multiplier, delta size x, delta size y
  30. n: [0.5, 0, 0, -1],
  31. e: [1, 0.5, 1, 0],
  32. s: [0.5, 1, 0, 1],
  33. w: [0, 0.5, -1, 0],
  34. nw: [0, 0, -1, -1],
  35. ne: [1, 0, 1, -1],
  36. se: [1, 1, 1, 1],
  37. sw: [0, 1, -1, 1]
  38. };
  39. // Add CSS for resize handles, cloned element and selected
  40. var rootClass = '.mce-content-body';
  41. editor.contentStyles.push(
  42. rootClass + ' div.mce-resizehandle {' +
  43. 'position: absolute;' +
  44. 'border: 1px solid black;' +
  45. 'background: #FFF;' +
  46. 'width: 5px;' +
  47. 'height: 5px;' +
  48. 'z-index: 10000' +
  49. '}' +
  50. rootClass + ' .mce-resizehandle:hover {' +
  51. 'background: #000' +
  52. '}' +
  53. rootClass + ' img[data-mce-selected], hr[data-mce-selected] {' +
  54. 'outline: 1px solid black;' +
  55. 'resize: none' + // Have been talks about implementing this in browsers
  56. '}' +
  57. rootClass + ' .mce-clonedresizable {' +
  58. 'position: absolute;' +
  59. (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing
  60. 'opacity: .5;' +
  61. 'filter: alpha(opacity=50);' +
  62. 'z-index: 10000' +
  63. '}'
  64. );
  65. function isResizable(elm) {
  66. var selector = editor.settings.object_resizing;
  67. if (selector === false || Env.iOS) {
  68. return false;
  69. }
  70. if (typeof selector != 'string') {
  71. selector = 'table,img,div';
  72. }
  73. if (elm.getAttribute('data-mce-resize') === 'false') {
  74. return false;
  75. }
  76. return editor.dom.is(elm, selector);
  77. }
  78. function resizeGhostElement(e) {
  79. var deltaX, deltaY;
  80. // Calc new width/height
  81. deltaX = e.screenX - startX;
  82. deltaY = e.screenY - startY;
  83. // Calc new size
  84. width = deltaX * selectedHandle[2] + startW;
  85. height = deltaY * selectedHandle[3] + startH;
  86. // Never scale down lower than 5 pixels
  87. width = width < 5 ? 5 : width;
  88. height = height < 5 ? 5 : height;
  89. // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image
  90. if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) {
  91. width = Math.round(height / ratio);
  92. height = Math.round(width * ratio);
  93. }
  94. // Update ghost size
  95. dom.setStyles(selectedElmGhost, {
  96. width: width,
  97. height: height
  98. });
  99. // Update ghost X position if needed
  100. if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
  101. dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
  102. }
  103. // Update ghost Y position if needed
  104. if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
  105. dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
  106. }
  107. if (!resizeStarted) {
  108. editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH});
  109. resizeStarted = true;
  110. }
  111. }
  112. function endGhostResize() {
  113. resizeStarted = false;
  114. function setSizeProp(name, value) {
  115. if (value) {
  116. // Resize by using style or attribute
  117. if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) {
  118. dom.setStyle(selectedElm, name, value);
  119. } else {
  120. dom.setAttrib(selectedElm, name, value);
  121. }
  122. }
  123. }
  124. // Set width/height properties
  125. setSizeProp('width', width);
  126. setSizeProp('height', height);
  127. dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
  128. dom.unbind(editableDoc, 'mouseup', endGhostResize);
  129. if (rootDocument != editableDoc) {
  130. dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
  131. dom.unbind(rootDocument, 'mouseup', endGhostResize);
  132. }
  133. // Remove ghost and update resize handle positions
  134. dom.remove(selectedElmGhost);
  135. if (!isIE || selectedElm.nodeName == "TABLE") {
  136. showResizeRect(selectedElm);
  137. }
  138. editor.fire('ObjectResized', {target: selectedElm, width: width, height: height});
  139. editor.nodeChanged();
  140. }
  141. function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) {
  142. var position, targetWidth, targetHeight, e, rect, offsetParent = editor.getBody();
  143. unbindResizeHandleEvents();
  144. // Get position and size of target
  145. position = dom.getPos(targetElm, offsetParent);
  146. selectedElmX = position.x;
  147. selectedElmY = position.y;
  148. rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption
  149. targetWidth = rect.width || (rect.right - rect.left);
  150. targetHeight = rect.height || (rect.bottom - rect.top);
  151. // Reset width/height if user selects a new image/table
  152. if (selectedElm != targetElm) {
  153. detachResizeStartListener();
  154. selectedElm = targetElm;
  155. width = height = 0;
  156. }
  157. // Makes it possible to disable resizing
  158. e = editor.fire('ObjectSelected', {target: targetElm});
  159. if (isResizable(targetElm) && !e.isDefaultPrevented()) {
  160. each(resizeHandles, function(handle, name) {
  161. var handleElm, handlerContainerElm;
  162. function startDrag(e) {
  163. startX = e.screenX;
  164. startY = e.screenY;
  165. startW = selectedElm.clientWidth;
  166. startH = selectedElm.clientHeight;
  167. ratio = startH / startW;
  168. selectedHandle = handle;
  169. selectedElmGhost = selectedElm.cloneNode(true);
  170. dom.addClass(selectedElmGhost, 'mce-clonedresizable');
  171. selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
  172. selectedElmGhost.unSelectabe = true;
  173. dom.setStyles(selectedElmGhost, {
  174. left: selectedElmX,
  175. top: selectedElmY,
  176. margin: 0
  177. });
  178. selectedElmGhost.removeAttribute('data-mce-selected');
  179. editor.getBody().appendChild(selectedElmGhost);
  180. dom.bind(editableDoc, 'mousemove', resizeGhostElement);
  181. dom.bind(editableDoc, 'mouseup', endGhostResize);
  182. if (rootDocument != editableDoc) {
  183. dom.bind(rootDocument, 'mousemove', resizeGhostElement);
  184. dom.bind(rootDocument, 'mouseup', endGhostResize);
  185. }
  186. }
  187. if (mouseDownHandleName) {
  188. // Drag started by IE native resizestart
  189. if (name == mouseDownHandleName) {
  190. startDrag(mouseDownEvent);
  191. }
  192. return;
  193. }
  194. // Get existing or render resize handle
  195. handleElm = dom.get('mceResizeHandle' + name);
  196. if (!handleElm) {
  197. handlerContainerElm = editor.getBody();
  198. handleElm = dom.add(handlerContainerElm, 'div', {
  199. id: 'mceResizeHandle' + name,
  200. 'data-mce-bogus': true,
  201. 'class': 'mce-resizehandle',
  202. unselectable: true,
  203. style: 'cursor:' + name + '-resize; margin:0; padding:0'
  204. });
  205. // Hides IE move layer cursor
  206. // If we set it on Chrome we get this wounderful bug: #6725
  207. if (Env.ie) {
  208. handleElm.contentEditable = false;
  209. }
  210. } else {
  211. dom.show(handleElm);
  212. }
  213. if (!handle.elm) {
  214. dom.bind(handleElm, 'mousedown', function(e) {
  215. e.stopImmediatePropagation();
  216. e.preventDefault();
  217. startDrag(e);
  218. });
  219. handle.elm = handleElm;
  220. }
  221. /*
  222. var halfHandleW = handleElm.offsetWidth / 2;
  223. var halfHandleH = handleElm.offsetHeight / 2;
  224. // Position element
  225. dom.setStyles(handleElm, {
  226. left: Math.floor((targetWidth * handle[0] + selectedElmX) - halfHandleW + (handle[2] * halfHandleW)),
  227. top: Math.floor((targetHeight * handle[1] + selectedElmY) - halfHandleH + (handle[3] * halfHandleH))
  228. });
  229. */
  230. // Position element
  231. dom.setStyles(handleElm, {
  232. left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2),
  233. top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2)
  234. });
  235. });
  236. } else {
  237. hideResizeRect();
  238. }
  239. selectedElm.setAttribute('data-mce-selected', '1');
  240. }
  241. function hideResizeRect() {
  242. var name, handleElm;
  243. unbindResizeHandleEvents();
  244. if (selectedElm) {
  245. selectedElm.removeAttribute('data-mce-selected');
  246. }
  247. for (name in resizeHandles) {
  248. handleElm = dom.get('mceResizeHandle' + name);
  249. if (handleElm) {
  250. dom.unbind(handleElm);
  251. dom.remove(handleElm);
  252. }
  253. }
  254. }
  255. function updateResizeRect(e) {
  256. var controlElm;
  257. function isChildOrEqual(node, parent) {
  258. if (node) {
  259. do {
  260. if (node === parent) {
  261. return true;
  262. }
  263. } while ((node = node.parentNode));
  264. }
  265. }
  266. // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v
  267. each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) {
  268. img.removeAttribute('data-mce-selected');
  269. });
  270. controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
  271. controlElm = dom.getParent(controlElm, isIE ? 'table' : 'table,img,hr');
  272. if (isChildOrEqual(controlElm, editor.getBody())) {
  273. disableGeckoResize();
  274. if (isChildOrEqual(selection.getStart(), controlElm) && isChildOrEqual(selection.getEnd(), controlElm)) {
  275. if (!isIE || (controlElm != selection.getStart() && selection.getStart().nodeName !== 'IMG')) {
  276. showResizeRect(controlElm);
  277. return;
  278. }
  279. }
  280. }
  281. hideResizeRect();
  282. }
  283. function attachEvent(elm, name, func) {
  284. if (elm && elm.attachEvent) {
  285. elm.attachEvent('on' + name, func);
  286. }
  287. }
  288. function detachEvent(elm, name, func) {
  289. if (elm && elm.detachEvent) {
  290. elm.detachEvent('on' + name, func);
  291. }
  292. }
  293. function resizeNativeStart(e) {
  294. var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;
  295. pos = target.getBoundingClientRect();
  296. relativeX = lastMouseDownEvent.clientX - pos.left;
  297. relativeY = lastMouseDownEvent.clientY - pos.top;
  298. // Figure out what corner we are draging on
  299. for (name in resizeHandles) {
  300. corner = resizeHandles[name];
  301. cornerX = target.offsetWidth * corner[0];
  302. cornerY = target.offsetHeight * corner[1];
  303. if (Math.abs(cornerX - relativeX) < 8 && Math.abs(cornerY - relativeY) < 8) {
  304. selectedHandle = corner;
  305. break;
  306. }
  307. }
  308. // Remove native selection and let the magic begin
  309. resizeStarted = true;
  310. editor.getDoc().selection.empty();
  311. showResizeRect(target, name, lastMouseDownEvent);
  312. }
  313. function nativeControlSelect(e) {
  314. var target = e.srcElement;
  315. if (target != selectedElm) {
  316. detachResizeStartListener();
  317. if (target.id.indexOf('mceResizeHandle') === 0) {
  318. e.returnValue = false;
  319. return;
  320. }
  321. if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
  322. hideResizeRect();
  323. selectedElm = target;
  324. attachEvent(target, 'resizestart', resizeNativeStart);
  325. }
  326. }
  327. }
  328. function detachResizeStartListener() {
  329. detachEvent(selectedElm, 'resizestart', resizeNativeStart);
  330. }
  331. function unbindResizeHandleEvents() {
  332. for (var name in resizeHandles) {
  333. var handle = resizeHandles[name];
  334. if (handle.elm) {
  335. dom.unbind(handle.elm);
  336. delete handle.elm;
  337. }
  338. }
  339. }
  340. function disableGeckoResize() {
  341. try {
  342. // Disable object resizing on Gecko
  343. editor.getDoc().execCommand('enableObjectResizing', false, false);
  344. } catch (ex) {
  345. // Ignore
  346. }
  347. }
  348. function controlSelect(elm) {
  349. var ctrlRng;
  350. if (!isIE) {
  351. return;
  352. }
  353. ctrlRng = editableDoc.body.createControlRange();
  354. try {
  355. ctrlRng.addElement(elm);
  356. ctrlRng.select();
  357. return true;
  358. } catch (ex) {
  359. // Ignore since the element can't be control selected for example a P tag
  360. }
  361. }
  362. editor.on('init', function() {
  363. if (isIE) {
  364. // Hide the resize rect on resize and reselect the image
  365. editor.on('ObjectResized', function(e) {
  366. if (e.target.nodeName != 'TABLE') {
  367. hideResizeRect();
  368. controlSelect(e.target);
  369. }
  370. });
  371. attachEvent(editor.getBody(), 'controlselect', nativeControlSelect);
  372. editor.on('mousedown', function(e) {
  373. lastMouseDownEvent = e;
  374. });
  375. } else {
  376. disableGeckoResize();
  377. if (Env.ie >= 11) {
  378. // TODO: Drag/drop doesn't work
  379. editor.on('mouseup', function(e) {
  380. var nodeName = e.target.nodeName;
  381. if (/^(TABLE|IMG|HR)$/.test(nodeName)) {
  382. editor.selection.select(e.target, nodeName == 'TABLE');
  383. editor.nodeChanged();
  384. }
  385. });
  386. editor.dom.bind(editor.getBody(), 'mscontrolselect', function(e) {
  387. if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
  388. e.preventDefault();
  389. // This moves the selection from being a control selection to a text like selection like in WebKit #6753
  390. // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections.
  391. if (e.target.tagName == 'IMG') {
  392. window.setTimeout(function() {
  393. editor.selection.select(e.target);
  394. }, 0);
  395. }
  396. }
  397. });
  398. }
  399. }
  400. editor.on('nodechange mousedown mouseup ResizeEditor', updateResizeRect);
  401. // Update resize rect while typing in a table
  402. editor.on('keydown keyup', function(e) {
  403. if (selectedElm && selectedElm.nodeName == "TABLE") {
  404. updateResizeRect(e);
  405. }
  406. });
  407. // Hide rect on focusout since it would float on top of windows otherwise
  408. //editor.on('focusout', hideResizeRect);
  409. });
  410. editor.on('remove', unbindResizeHandleEvents);
  411. function destroy() {
  412. selectedElm = selectedElmGhost = null;
  413. if (isIE) {
  414. detachResizeStartListener();
  415. detachEvent(editor.getBody(), 'controlselect', nativeControlSelect);
  416. }
  417. }
  418. return {
  419. isResizable: isResizable,
  420. showResizeRect: showResizeRect,
  421. hideResizeRect: hideResizeRect,
  422. updateResizeRect: updateResizeRect,
  423. controlSelect: controlSelect,
  424. destroy: destroy
  425. };
  426. };
  427. });