Selection.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. SC.loadPackage({ 'Selection': {
  2. comment: 'I represent the selection of the editing tool, holding one or more Elements. I selection can move its Elements, it shows the Widgets, that apply to all of the Elements in the current selection, and it can start the content editing mode of one of its Elements.',
  3. sharedProperties: {
  4. selectionTools: { initValue: '<div class="sg-editing-selection"><div class="sg-editing-selection-widget-menu-right"></div><div class="sg-editing-selection-widget-menu-bottom"></div></div>' },
  5. },
  6. properties: {
  7. node: { comment: 'My DOM node.' },
  8. elements: { comment: 'I hold an array of the currently selected elements.' },
  9. active: { comment: 'Wether a selection is active or not.' },
  10. widgetsRight: { comment: 'I hold an array of the standard widgets, which are applicable to all elements, and which are shown on the right side of the selection.'},
  11. widgetsBottom: { comment: 'I hold an array of the widgets, which are applicable to the elements in the current selection, and which are shown on the bottom of the selection.'},
  12. menuNodeRight: { comment: 'I hold the DOM node containing the right menu node.'},
  13. menuNodeBottom: { comment: 'I hold the DOM node containing the bottom menu node.'},
  14. activeWidget: { comment: 'I store either null or the active (selected) widget.'},
  15. lockWidget: { comment: 'My lockWidget plays a special role, since it appears only on multiple selections and must update its state.'}
  16. },
  17. methods: {
  18. init: {
  19. comment: 'I init myself.',
  20. code: function(){
  21. var node = (new DOMParser()).parseFromString(this.class.get('selectionTools'), 'text/html').body.firstChild;
  22. this.set({
  23. node: node,
  24. menuNodeRight: node.firstChild,
  25. menuNodeBottom: node.lastChild,
  26. widgetsRight: [],
  27. widgetsBottom: [],
  28. active: false,
  29. elements: [],
  30. activeWidget: null
  31. });
  32. var generalWidgets = ['WidgetLayerTop', 'WidgetLayerBottom', 'WidgetEditHTML', 'WidgetCopy', 'WidgetDelete'],
  33. generalWidgetsContainer = this.get('menuNodeRight'),
  34. widget = null;
  35. for(var i = 0, l = generalWidgets.length; i < l; i++){
  36. widget = SC.init(generalWidgets[i], this);
  37. this.get('widgetsRight').push(widget);
  38. generalWidgetsContainer.appendChild(widget.get('widgetMenu'));
  39. }
  40. this.set({ lockWidget: SC.init('WidgetLock', this) });
  41. }
  42. },
  43. updateWidgetMenu: {
  44. comment: 'I update the menu of widgets according to the current selection.',
  45. code: function(){
  46. // Update widget menu to the bottom
  47. var elements = this.get('elements'),
  48. elementsWidgetSets = [],
  49. currentWidgets = [],
  50. currentWidgetsClasses = null,
  51. currentWidgetsContainer = this.get('menuNodeBottom'),
  52. widget = null;
  53. while(currentWidgetsContainer.firstChild){
  54. currentWidgetsContainer.removeChild(currentWidgetsContainer.firstChild);
  55. }
  56. if(elements.length > 0){
  57. // find intersection of applicable widgets
  58. for(var i = 0, l = elements.length; i < l; i++){
  59. elementsWidgetSets.push( elements[i].class.get('applicableWidgets') );
  60. }
  61. if(elementsWidgetSets.length > 1){
  62. currentWidgetsClasses = elementsWidgetSets.shift().filter(function(v) {
  63. return elementsWidgetSets.every(function(a) {
  64. return a.indexOf(v) !== -1;
  65. });
  66. });
  67. }else{
  68. currentWidgetsClasses = elementsWidgetSets[0];
  69. }
  70. for(var i = 0, l = currentWidgetsClasses.length; i < l; i++){
  71. widget = SC.init(currentWidgetsClasses[i], this);
  72. currentWidgets.push(widget);
  73. currentWidgetsContainer.appendChild(widget.get('widgetMenu'));
  74. }
  75. }
  76. this.set({
  77. widgetsBottom: currentWidgets,
  78. activeWidget: null
  79. });
  80. // Update widget to the right
  81. this.do('updateLockGroup');
  82. // Finally draw red outline when multiple elements are selected
  83. if(elements.length === 1){
  84. this.get('node').classList.remove('sg-editing-selection-outline');
  85. }else{
  86. this.get('node').classList.add('sg-editing-selection-outline');
  87. }
  88. }
  89. },
  90. updateLockGroup: {
  91. comment: 'After adding and removing elements, I check wether I have multiple elements, and if so, if they form a locked group or not.',
  92. code: function(){
  93. var myElements = this.get('elements'),
  94. lockWidget = this.get('lockWidget'),
  95. menuNodeRight = this.get('menuNodeRight'),
  96. isGroup = true;
  97. if(myElements.length > 1){
  98. // check and add lockWidget
  99. if(lockWidget.get('widgetMenu').parentNode !== menuNodeRight){
  100. menuNodeRight.insertBefore(lockWidget.get('widgetMenu'), menuNodeRight.childNodes[0]);
  101. }
  102. // check group status of myElements
  103. for(var i = 0, l = myElements.length; i < l; i++){
  104. isGroup = isGroup && (myElements[i].get('group') !== null);
  105. }
  106. if(isGroup){
  107. for(var i = 1, l = myElements.length; i < l; i++){
  108. isGroup = isGroup && (myElements[i].get('group') === myElements[i-1].get('group'))
  109. }
  110. }
  111. lockWidget.set({ locked: isGroup })
  112. }else if(myElements.length > 0){
  113. // remove lockWidget
  114. if(lockWidget.get('widgetMenu').parentNode === menuNodeRight){
  115. menuNodeRight.removeChild(lockWidget.get('widgetMenu'));
  116. }
  117. }
  118. }
  119. },
  120. addElement: {
  121. comment: 'I add anElement to myself.',
  122. code: function(anElement){
  123. var elementsToAdd = [ anElement ],
  124. myElements = this.get('elements'),
  125. group = anElement.get('group');
  126. if(group !== null){
  127. for(var elements = SuperGlue.get('document').get('children'),
  128. i = 0, l = elements.length; i < l; i++){
  129. if(elements[i].get('group') === group){
  130. elementsToAdd.push(elements[i]);
  131. }
  132. }
  133. }
  134. for(var i = 0, l = elementsToAdd.length; i < l; i++){
  135. if(myElements.indexOf(elementsToAdd[i]) < 0){
  136. myElements.push(elementsToAdd[i]);
  137. }
  138. }
  139. if(myElements.length === 1){
  140. myElements[0].get('resizeHandles').set({ selected: true });
  141. myElements[0].get('resizeHandles').do('showResizeHandles');
  142. }else{
  143. for(var i = 0, l = myElements.length; i < l; i++){
  144. myElements[i].get('resizeHandles').set({ selected: false });
  145. }
  146. }
  147. if(!this.get('active')){
  148. SuperGlue.get('document').get('editingContainer').appendChild(this.get('node'));
  149. }
  150. this.do('updateDimensions');
  151. this.do('updateWidgetMenu');
  152. }
  153. },
  154. removeElement: {
  155. comment: 'I remove anElement from myself.',
  156. code: function(anElement){
  157. var myElements = this.get('elements'),
  158. elementsToRemove = [ anElement ],
  159. group = anElement.get('group');
  160. if(group !== null){
  161. for(var i = 0, l = myElements.length; i < l; i++){
  162. if(myElements[i].get('group') === group){
  163. elementsToRemove.push(myElements[i]);
  164. }
  165. }
  166. }
  167. for(var i = 0, l = elementsToRemove.length; i < l; i++){
  168. var elementIndex = myElements.indexOf(elementsToRemove[i]);
  169. if(elementIndex >= 0){
  170. myElements.splice(elementIndex, 1);
  171. elementsToRemove[i].get('resizeHandles').set({ selected: false, mouseOnElement: false });
  172. elementsToRemove[i].get('resizeHandles').do('hideResizeHandles', true);
  173. }
  174. }
  175. if(myElements.length === 0){
  176. SuperGlue.get('document').get('editingContainer').removeChild(this.get('node'));
  177. this.set({ active: false });
  178. }else{
  179. if(myElements.length === 1){
  180. myElements[0].get('resizeHandles').set({ selected: true });
  181. myElements[0].get('resizeHandles').do('showResizeHandles');
  182. }else{
  183. for(var i = 0, l = myElements.length; i < l; i++){
  184. myElements[i].get('resizeHandles').set({ selected: false });
  185. }
  186. }
  187. this.do('updateDimensions');
  188. this.do('updateWidgetMenu');
  189. }
  190. }
  191. },
  192. toggleSelectionFor: {
  193. comment: 'I check wether anElement belongs to me already, and then add or remove it.',
  194. code: function(anElement){
  195. if(this.get('elements').indexOf(anElement) > -1){
  196. this.do('removeElement', anElement);
  197. }else{
  198. this.do('addElement', anElement);
  199. }
  200. }
  201. },
  202. clearAll: {
  203. comment: 'I remove all my Elements.',
  204. code: function(){
  205. var elements = this.get('elements');
  206. for(var i = 0, l = elements.length; i < l; i++){
  207. elements[i].get('resizeHandles').set({ selected: false, mouseOnElement: false });
  208. elements[i].get('resizeHandles').do('hideResizeHandles', true)
  209. }
  210. var editingContainer = SuperGlue.get('document').get('editingContainer');
  211. if(this.get('node').parentNode === editingContainer){
  212. editingContainer.removeChild(this.get('node'));
  213. }
  214. this.set({
  215. elements: [],
  216. active: false
  217. });
  218. }
  219. },
  220. isEmpty: {
  221. comment: 'Am I empty?',
  222. code: function(){
  223. return this.get('elements').length === 0
  224. }
  225. },
  226. calculateDimensions: {
  227. comment: 'I return the overall dimension to fit in all my Elements in the form [top, left, width, height].',
  228. code: function(){
  229. if(this.do('isEmpty')){ return []; }
  230. var myElements = this.get('elements'),
  231. top = myElements[0].get('top'),
  232. left = myElements[0].get('left'),
  233. width = 0,
  234. height = 0,
  235. l = myElements.length;
  236. for(var i = 0; i < l; i++){
  237. top = myElements[i].get('top') < top ? myElements[i].get('top') : top;
  238. left = myElements[i].get('left') < left ? myElements[i].get('left') : left;
  239. width = (myElements[i].get('width') + myElements[i].get('left')) < width
  240. ? width
  241. : (myElements[i].get('width') + myElements[i].get('left'));
  242. height = (myElements[i].get('height') + myElements[i].get('top')) < height
  243. ? height
  244. : (myElements[i].get('height') + myElements[i].get('top'));
  245. }
  246. width -= left;
  247. height -= top;
  248. return [top, left, width, height];
  249. }
  250. },
  251. updateDimensions: {
  252. comment: 'I update the overall dimension of my DOM node to fit in all my Elements.',
  253. code: function(){
  254. if(this.do('isEmpty')){ return; }
  255. var nodeStyle = this.get('node').style,
  256. dimensions = this.do('calculateDimensions'),
  257. top = dimensions[0],
  258. left = dimensions[1],
  259. width = dimensions[2],
  260. height = dimensions[3];
  261. nodeStyle.top = top + 'px';
  262. nodeStyle.left = left + 'px';
  263. nodeStyle.width = width + 'px';
  264. nodeStyle.height = height + 'px';
  265. }
  266. },
  267. registerForSelection: {
  268. comment: 'I prepare an Element to be selectable. Used by Element>>init.',
  269. code: function(anElement){
  270. var self = this,
  271. myNode = this.get('node'),
  272. thisElement = anElement,
  273. elementNode = anElement.get('node'),
  274. elements = [],
  275. infiniteSpace = true,
  276. pageWidth = 0,
  277. virtualTop = 0,
  278. virtualLeft = 0,
  279. virtualWidth = 0,
  280. virtualHeight = 0,
  281. elementsOffsetsX = null,
  282. elementsOffsetsY = null,
  283. targetNotInSelection,
  284. isGroup,
  285. group,
  286. startX = 0,
  287. startY = 0,
  288. clickPrecisionXleft = 0,
  289. clickPrecisionXright = 0,
  290. clickPrecisionYtop = 0,
  291. clickPrecisionYbottom = 0,
  292. withinClickPrecision = true,
  293. widthMarkersVisible,
  294. gridVisible,
  295. onMouseDown = function(evt){
  296. if(evt.button !== 0) return;
  297. if(evt.shiftKey || evt.ctrlKey){
  298. document.addEventListener('mouseup', onMouseUpWithModifier, true);
  299. SuperGlue.get('document').set({ interactionInProgress: true });
  300. // UNDO
  301. }else{
  302. startX = evt.pageX;
  303. startY = evt.pageY;
  304. clickPrecisionXleft = startX - 6;
  305. clickPrecisionXright = startX + 6;
  306. clickPrecisionYtop = startY - 6;
  307. clickPrecisionYbottom = startY + 6;
  308. withinClickPrecision = true;
  309. isGroup = false;
  310. infiniteSpace = ! SuperGlue.get('document').get('layout').centered;
  311. pageWidth = SuperGlue.get('document').get('layout').width;
  312. if(SuperGlue.get('document').get('grid').get('active')){
  313. var gridSize = SuperGlue.get('document').get('grid').get('gridSize');
  314. pageWidth = Math.floor(pageWidth / gridSize) * gridSize;
  315. }
  316. elements = self.get('elements');
  317. if(elements.length !== 0){
  318. targetNotInSelection = true;
  319. for(var i = 0, l = elements.length; i < l; i++){
  320. if(elements[i].get('node') === thisElement.get('node')){
  321. targetNotInSelection = false;
  322. break;
  323. }
  324. }
  325. if(targetNotInSelection){
  326. self.do('clearAll');
  327. elements = self.get('elements');
  328. }
  329. }else{
  330. targetNotInSelection = elements.indexOf(thisElement) < 0;
  331. }
  332. if(elements.length === 0){
  333. group = thisElement.get('group');
  334. if(group !== null){
  335. isGroup = true;
  336. for(var allElements = SuperGlue.get('document').get('children'),
  337. i = 0, l = allElements.length; i < l; i++){
  338. if(allElements[i].get('group') === group){
  339. elements.push(allElements[i]);
  340. }
  341. }
  342. }
  343. }
  344. if(elements.length === 0){
  345. virtualTop = anElement.get('top');
  346. virtualLeft = anElement.get('left');
  347. virtualWidth = anElement.get('width');
  348. virtualHeight = anElement.get('height');
  349. }else{
  350. var dimensions = self.do('calculateDimensions');
  351. virtualTop = dimensions[0];
  352. virtualLeft = dimensions[1];
  353. virtualWidth = dimensions[2];
  354. virtualHeight = dimensions[3];
  355. elementsOffsetsX = [];
  356. elementsOffsetsY = [];
  357. for(var i = 0, l = elements.length; i < l; i++){
  358. elementsOffsetsX.push( elements[i].get('left') - virtualLeft );
  359. elementsOffsetsY.push( elements[i].get('top') - virtualTop );
  360. }
  361. }
  362. document.addEventListener('mousemove', onMouseMove, true);
  363. document.addEventListener('mouseup', onMouseUp, true);
  364. SuperGlue.get('document').set({ interactionInProgress: true });
  365. // UNDO
  366. }
  367. evt.stopPropagation();
  368. evt.preventDefault();
  369. },
  370. onMouseUpWithModifier = function(evt){
  371. self.do('toggleSelectionFor', thisElement);
  372. document.removeEventListener('mouseup', onMouseUpWithModifier, true);
  373. SuperGlue.get('document').set({ interactionInProgress: false });
  374. // UNDO
  375. evt.stopPropagation();
  376. evt.preventDefault();
  377. },
  378. onMouseUp = function(evt){
  379. document.removeEventListener('mousemove', onMouseMove, true);
  380. document.removeEventListener('mouseup', onMouseUp, true);
  381. var myDocument = SuperGlue.get('document');
  382. myDocument.set({ interactionInProgress: false });
  383. myDocument.do('afterLayoutHasChanged');
  384. if(withinClickPrecision){
  385. if( !targetNotInSelection
  386. && thisElement.class() === 'TextElement'
  387. ){
  388. thisElement.do('activateTextEditor');
  389. }else{
  390. self.do('clearAll');
  391. self.do('addElement', thisElement);
  392. }
  393. }else{
  394. SuperGlue.get('document').get('widthMarkers').set({ visible: widthMarkersVisible });
  395. SuperGlue.get('document').get('grid').set({ visible: gridVisible });
  396. if(isGroup){
  397. self.do('clearAll');
  398. }
  399. }
  400. // UNDO
  401. evt.stopPropagation();
  402. evt.preventDefault();
  403. },
  404. onMouseMove = function(evt){
  405. var diffX = evt.pageX - startX,
  406. diffY = evt.pageY - startY;
  407. if( withinClickPrecision &&
  408. ( evt.pageX < clickPrecisionXleft
  409. || evt.pageX > clickPrecisionXright
  410. || evt.pageY < clickPrecisionYtop
  411. || evt.pageY > clickPrecisionYbottom )
  412. ){
  413. var widthMarkers = SuperGlue.get('document').get('widthMarkers'),
  414. grid = SuperGlue.get('document').get('grid');
  415. widthMarkersVisible = widthMarkers.get('visible');
  416. gridVisible = grid.get('visible');
  417. widthMarkers.set({ visible: true });
  418. grid.set({ visible: true });
  419. withinClickPrecision = false;
  420. }
  421. if(!withinClickPrecision){
  422. virtualTop += diffY;
  423. virtualLeft += diffX;
  424. if(elements.length === 0){
  425. if(virtualTop > 0){
  426. thisElement.set({ top: virtualTop });
  427. }else{
  428. thisElement.set({ top: 0 });
  429. }
  430. if(virtualLeft > 0){
  431. if(infiniteSpace || (virtualLeft + virtualWidth < pageWidth) ){
  432. thisElement.set({ left: virtualLeft });
  433. }else{
  434. thisElement.set({ left: (pageWidth - virtualWidth) });
  435. }
  436. }else{
  437. thisElement.set({ left: 0 });
  438. }
  439. }else{
  440. if(virtualTop > 0){
  441. for(var i = 0, l = elements.length; i < l; i++){
  442. elements[i].set({
  443. top: elementsOffsetsY[i] + virtualTop
  444. });
  445. }
  446. self.do('updateDimensions');
  447. }else{
  448. myNode.style.top = '0px';
  449. for(var i = 0, l = elements.length; i < l; i++){
  450. elements[i].set({
  451. top: elementsOffsetsY[i]
  452. });
  453. }
  454. }
  455. if(virtualLeft > 0){
  456. if(infiniteSpace || (virtualLeft + virtualWidth < pageWidth) ){
  457. for(var i = 0, l = elements.length; i < l; i++){
  458. elements[i].set({
  459. left: elementsOffsetsX[i] + virtualLeft
  460. });
  461. }
  462. self.do('updateDimensions');
  463. }else{
  464. myNode.style.left = (pageWidth - virtualWidth) + 'px';
  465. for(var i = 0, l = elements.length; i < l; i++){
  466. elements[i].set({
  467. left: elementsOffsetsX[i] + (pageWidth - virtualWidth)
  468. });
  469. }
  470. }
  471. }else{
  472. myNode.style.left = '0px';
  473. for(var i = 0, l = elements.length; i < l; i++){
  474. elements[i].set({
  475. left: elementsOffsetsX[i]
  476. });
  477. }
  478. }
  479. }
  480. startX = evt.pageX;
  481. startY = evt.pageY;
  482. }
  483. evt.stopPropagation();
  484. evt.preventDefault();
  485. };
  486. elementNode.addEventListener('mousedown', onMouseDown, false);
  487. }
  488. }
  489. }
  490. }});