Selection.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  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. for(var widgetsBottom = this.get('widgetsBottom'), i = 0, l = widgetsBottom.length;
  54. i < l; i++){
  55. widgetsBottom[i].set({ isWidgetActive: false });
  56. currentWidgetsContainer.removeChild(widgetsBottom[i].get('widgetMenu'));
  57. }
  58. if(elements.length > 0){
  59. // find intersection of applicable widgets
  60. for(var i = 0, l = elements.length; i < l; i++){
  61. elementsWidgetSets.push( elements[i].class.get('applicableWidgets') );
  62. }
  63. if(elementsWidgetSets.length > 1){
  64. currentWidgetsClasses = elementsWidgetSets.shift().filter(function(v) {
  65. return elementsWidgetSets.every(function(a) {
  66. return a.indexOf(v) !== -1;
  67. });
  68. });
  69. }else{
  70. currentWidgetsClasses = elementsWidgetSets[0];
  71. }
  72. for(var i = 0, l = currentWidgetsClasses.length; i < l; i++){
  73. widget = SC.init(currentWidgetsClasses[i], this);
  74. currentWidgets.push(widget);
  75. currentWidgetsContainer.appendChild(widget.get('widgetMenu'));
  76. }
  77. }
  78. this.set({
  79. widgetsBottom: currentWidgets,
  80. activeWidget: null
  81. });
  82. // Update widget to the right
  83. this.do('updateLockGroup');
  84. // Finally draw red outline when multiple elements are selected
  85. if(elements.length === 1){
  86. this.get('node').classList.remove('sg-editing-selection-outline');
  87. }else{
  88. this.get('node').classList.add('sg-editing-selection-outline');
  89. }
  90. }
  91. },
  92. updateLockGroup: {
  93. comment: 'After adding and removing elements, I check wether I have multiple elements, and if so, if they form a locked group or not.',
  94. code: function(){
  95. var myElements = this.get('elements'),
  96. lockWidget = this.get('lockWidget'),
  97. menuNodeRight = this.get('menuNodeRight'),
  98. isGroup = true;
  99. if(myElements.length > 1){
  100. // check and add lockWidget
  101. if(lockWidget.get('widgetMenu').parentNode !== menuNodeRight){
  102. menuNodeRight.insertBefore(lockWidget.get('widgetMenu'), menuNodeRight.childNodes[0]);
  103. }
  104. // check group status of myElements
  105. for(var i = 0, l = myElements.length; i < l; i++){
  106. isGroup = isGroup && (myElements[i].get('group') !== null);
  107. }
  108. if(isGroup){
  109. for(var i = 1, l = myElements.length; i < l; i++){
  110. isGroup = isGroup && (myElements[i].get('group') === myElements[i-1].get('group'))
  111. }
  112. }
  113. lockWidget.set({ locked: isGroup })
  114. }else if(myElements.length > 0){
  115. // remove lockWidget
  116. if(lockWidget.get('widgetMenu').parentNode === menuNodeRight){
  117. menuNodeRight.removeChild(lockWidget.get('widgetMenu'));
  118. }
  119. }
  120. }
  121. },
  122. addElement: {
  123. comment: 'I add anElement to myself.',
  124. code: function(anElement){
  125. var elementsToAdd = [ anElement ],
  126. myElements = this.get('elements'),
  127. group = anElement.get('group');
  128. if(group !== null){
  129. for(var elements = SuperGlue.get('document').get('children'),
  130. i = 0, l = elements.length; i < l; i++){
  131. if(elements[i].get('group') === group){
  132. elementsToAdd.push(elements[i]);
  133. }
  134. }
  135. }
  136. for(var i = 0, l = elementsToAdd.length; i < l; i++){
  137. if(myElements.indexOf(elementsToAdd[i]) < 0){
  138. myElements.push(elementsToAdd[i]);
  139. }
  140. }
  141. if(myElements.length === 1){
  142. myElements[0].get('resizeHandles').set({ selected: true });
  143. myElements[0].get('resizeHandles').do('showResizeHandles');
  144. }else{
  145. for(var i = 0, l = myElements.length; i < l; i++){
  146. myElements[i].get('resizeHandles').set({ selected: false });
  147. }
  148. }
  149. if(!this.get('active')){
  150. SuperGlue.get('document').get('editingContainer').appendChild(this.get('node'));
  151. }
  152. this.do('updateDimensions');
  153. this.do('updateWidgetMenu');
  154. }
  155. },
  156. removeElement: {
  157. comment: 'I remove anElement from myself.',
  158. code: function(anElement){
  159. var myElements = this.get('elements'),
  160. elementsToRemove = [ anElement ],
  161. group = anElement.get('group');
  162. if(group !== null){
  163. for(var i = 0, l = myElements.length; i < l; i++){
  164. if(myElements[i].get('group') === group){
  165. elementsToRemove.push(myElements[i]);
  166. }
  167. }
  168. }
  169. for(var i = 0, l = elementsToRemove.length; i < l; i++){
  170. var elementIndex = myElements.indexOf(elementsToRemove[i]);
  171. if(elementIndex >= 0){
  172. myElements.splice(elementIndex, 1);
  173. elementsToRemove[i].get('resizeHandles').set({ selected: false, mouseOnElement: false });
  174. elementsToRemove[i].get('resizeHandles').do('hideResizeHandles', true);
  175. }
  176. }
  177. if(myElements.length === 0){
  178. SuperGlue.get('document').get('editingContainer').removeChild(this.get('node'));
  179. this.set({ active: false });
  180. }else{
  181. if(myElements.length === 1){
  182. myElements[0].get('resizeHandles').set({ selected: true });
  183. myElements[0].get('resizeHandles').do('showResizeHandles');
  184. }else{
  185. for(var i = 0, l = myElements.length; i < l; i++){
  186. myElements[i].get('resizeHandles').set({ selected: false });
  187. }
  188. }
  189. this.do('updateDimensions');
  190. this.do('updateWidgetMenu');
  191. }
  192. }
  193. },
  194. toggleSelectionFor: {
  195. comment: 'I check wether anElement belongs to me already, and then add or remove it.',
  196. code: function(anElement){
  197. if(this.get('elements').indexOf(anElement) > -1){
  198. this.do('removeElement', anElement);
  199. }else{
  200. this.do('addElement', anElement);
  201. }
  202. }
  203. },
  204. clearAll: {
  205. comment: 'I remove all my Elements.',
  206. code: function(){
  207. var elements = this.get('elements');
  208. for(var i = 0, l = elements.length; i < l; i++){
  209. elements[i].get('resizeHandles').set({ selected: false, mouseOnElement: false });
  210. elements[i].get('resizeHandles').do('hideResizeHandles', true)
  211. }
  212. var widgetsBottom = this.get('widgetsBottom');
  213. for(var i = 0, l = widgetsBottom.length; i < l; i++){
  214. widgetsBottom[i].set({ isWidgetActive: false });
  215. }
  216. var editingContainer = SuperGlue.get('document').get('editingContainer');
  217. if(this.get('node').parentNode === editingContainer){
  218. editingContainer.removeChild(this.get('node'));
  219. }
  220. this.set({
  221. elements: [],
  222. active: false
  223. });
  224. }
  225. },
  226. isEmpty: {
  227. comment: 'Am I empty?',
  228. code: function(){
  229. return this.get('elements').length === 0
  230. }
  231. },
  232. calculateDimensions: {
  233. comment: 'I return the overall dimension to fit in all my Elements in the form [top, left, width, height].',
  234. code: function(){
  235. if(this.do('isEmpty')){ return []; }
  236. var myElements = this.get('elements'),
  237. top = myElements[0].get('top'),
  238. left = myElements[0].get('left'),
  239. width = 0,
  240. height = 0,
  241. l = myElements.length;
  242. for(var i = 0; i < l; i++){
  243. top = myElements[i].get('top') < top ? myElements[i].get('top') : top;
  244. left = myElements[i].get('left') < left ? myElements[i].get('left') : left;
  245. width = (myElements[i].get('width') + myElements[i].get('left')) < width
  246. ? width
  247. : (myElements[i].get('width') + myElements[i].get('left'));
  248. height = (myElements[i].get('height') + myElements[i].get('top')) < height
  249. ? height
  250. : (myElements[i].get('height') + myElements[i].get('top'));
  251. }
  252. width -= left;
  253. height -= top;
  254. return [top, left, width, height];
  255. }
  256. },
  257. updateDimensions: {
  258. comment: 'I update the overall dimension of my DOM node to fit in all my Elements.',
  259. code: function(){
  260. if(this.do('isEmpty')){ return; }
  261. var nodeStyle = this.get('node').style,
  262. dimensions = this.do('calculateDimensions'),
  263. top = dimensions[0],
  264. left = dimensions[1],
  265. width = dimensions[2],
  266. height = dimensions[3];
  267. nodeStyle.top = top + 'px';
  268. nodeStyle.left = left + 'px';
  269. nodeStyle.width = width + 'px';
  270. nodeStyle.height = height + 'px';
  271. }
  272. },
  273. registerForSelection: {
  274. comment: 'I prepare an Element to be selectable. Used by Element>>init.',
  275. code: function(anElement){
  276. var self = this,
  277. myNode = this.get('node'),
  278. thisElement = anElement,
  279. elementNode = anElement.get('node'),
  280. elements = [],
  281. infiniteSpace = true,
  282. pageWidth = 0,
  283. virtualTop = 0,
  284. virtualLeft = 0,
  285. virtualWidth = 0,
  286. virtualHeight = 0,
  287. elementsOffsetsX = null,
  288. elementsOffsetsY = null,
  289. targetNotInSelection,
  290. isGroup,
  291. group,
  292. startX = 0,
  293. startY = 0,
  294. clickPrecisionXleft = 0,
  295. clickPrecisionXright = 0,
  296. clickPrecisionYtop = 0,
  297. clickPrecisionYbottom = 0,
  298. withinClickPrecision = true,
  299. widthMarkersVisible,
  300. gridVisible,
  301. onMouseDown = function(evt){
  302. if(evt.button !== 0) return;
  303. if(evt.shiftKey || evt.ctrlKey){
  304. document.addEventListener('mouseup', onMouseUpWithModifier, true);
  305. SuperGlue.get('document').set({ interactionInProgress: true });
  306. // UNDO
  307. }else{
  308. startX = evt.pageX;
  309. startY = evt.pageY;
  310. clickPrecisionXleft = startX - 6;
  311. clickPrecisionXright = startX + 6;
  312. clickPrecisionYtop = startY - 6;
  313. clickPrecisionYbottom = startY + 6;
  314. withinClickPrecision = true;
  315. isGroup = false;
  316. infiniteSpace = ! SuperGlue.get('document').get('layout').centered;
  317. pageWidth = SuperGlue.get('document').get('layout').width;
  318. if(SuperGlue.get('document').get('grid').get('active')){
  319. var gridSize = SuperGlue.get('document').get('grid').get('gridSize');
  320. pageWidth = Math.floor(pageWidth / gridSize) * gridSize;
  321. }
  322. elements = self.get('elements');
  323. if(elements.length !== 0){
  324. targetNotInSelection = true;
  325. for(var i = 0, l = elements.length; i < l; i++){
  326. if(elements[i].get('node') === thisElement.get('node')){
  327. targetNotInSelection = false;
  328. break;
  329. }
  330. }
  331. if(targetNotInSelection){
  332. self.do('clearAll');
  333. elements = self.get('elements');
  334. }
  335. }else{
  336. targetNotInSelection = elements.indexOf(thisElement) < 0;
  337. }
  338. if(elements.length === 0){
  339. group = thisElement.get('group');
  340. if(group !== null){
  341. isGroup = true;
  342. for(var allElements = SuperGlue.get('document').get('children'),
  343. i = 0, l = allElements.length; i < l; i++){
  344. if(allElements[i].get('group') === group){
  345. elements.push(allElements[i]);
  346. }
  347. }
  348. }
  349. }
  350. if(elements.length === 0){
  351. virtualTop = anElement.get('top');
  352. virtualLeft = anElement.get('left');
  353. virtualWidth = anElement.get('width');
  354. virtualHeight = anElement.get('height');
  355. }else{
  356. var dimensions = self.do('calculateDimensions');
  357. virtualTop = dimensions[0];
  358. virtualLeft = dimensions[1];
  359. virtualWidth = dimensions[2];
  360. virtualHeight = dimensions[3];
  361. elementsOffsetsX = [];
  362. elementsOffsetsY = [];
  363. for(var i = 0, l = elements.length; i < l; i++){
  364. elementsOffsetsX.push( elements[i].get('left') - virtualLeft );
  365. elementsOffsetsY.push( elements[i].get('top') - virtualTop );
  366. }
  367. }
  368. document.addEventListener('mousemove', onMouseMove, true);
  369. document.addEventListener('mouseup', onMouseUp, true);
  370. SuperGlue.get('document').set({ interactionInProgress: true });
  371. // UNDO
  372. }
  373. evt.stopPropagation();
  374. evt.preventDefault();
  375. },
  376. onMouseUpWithModifier = function(evt){
  377. self.do('toggleSelectionFor', thisElement);
  378. document.removeEventListener('mouseup', onMouseUpWithModifier, true);
  379. SuperGlue.get('document').set({ interactionInProgress: false });
  380. // UNDO
  381. evt.stopPropagation();
  382. evt.preventDefault();
  383. },
  384. onMouseUp = function(evt){
  385. document.removeEventListener('mousemove', onMouseMove, true);
  386. document.removeEventListener('mouseup', onMouseUp, true);
  387. var myDocument = SuperGlue.get('document');
  388. myDocument.set({ interactionInProgress: false });
  389. myDocument.do('afterLayoutHasChanged');
  390. if(withinClickPrecision){
  391. if( !targetNotInSelection
  392. && thisElement.class() === 'TextElement'
  393. ){
  394. thisElement.do('activateTextEditor');
  395. }else{
  396. self.do('clearAll');
  397. self.do('addElement', thisElement);
  398. }
  399. }else{
  400. (function(elements, thisElement){
  401. var savedDimensions = []
  402. if(elements.length === 0){
  403. savedDimensions.push({
  404. top: thisElement.get('top'),
  405. left: thisElement.get('left'),
  406. width: thisElement.get('width'),
  407. height: thisElement.get('height')
  408. })
  409. }else{
  410. for(var i = 0, l = elements.length; i < l; i++){
  411. savedDimensions.push({
  412. top: elements[i].get('top'),
  413. left: elements[i].get('left'),
  414. width: elements[i].get('width'),
  415. height: elements[i].get('height')
  416. })
  417. }
  418. }
  419. SuperGlue.get('history').do('actionHasSucceeded', function(){
  420. if(elements.length === 0){
  421. thisElement.set({
  422. top: savedDimensions[0].top,
  423. left: savedDimensions[0].left,
  424. width: savedDimensions[0].width,
  425. height: savedDimensions[0].height
  426. })
  427. }else{
  428. for(var i = 0, l = elements.length; i < l; i++){
  429. elements[i].set({
  430. top: savedDimensions[i].top,
  431. left: savedDimensions[i].left,
  432. width: savedDimensions[i].width,
  433. height: savedDimensions[i].height
  434. })
  435. }
  436. }
  437. })
  438. }).call(this, elements, thisElement)
  439. SuperGlue.get('document').get('widthMarkers').set({ visible: widthMarkersVisible });
  440. SuperGlue.get('document').get('grid').set({ visible: gridVisible });
  441. if(isGroup){
  442. self.do('clearAll');
  443. }
  444. }
  445. // UNDO
  446. evt.stopPropagation();
  447. evt.preventDefault();
  448. },
  449. onMouseMove = function(evt){
  450. var diffX = evt.pageX - startX,
  451. diffY = evt.pageY - startY;
  452. if( withinClickPrecision &&
  453. ( evt.pageX < clickPrecisionXleft
  454. || evt.pageX > clickPrecisionXright
  455. || evt.pageY < clickPrecisionYtop
  456. || evt.pageY > clickPrecisionYbottom )
  457. ){
  458. var widthMarkers = SuperGlue.get('document').get('widthMarkers'),
  459. grid = SuperGlue.get('document').get('grid');
  460. widthMarkersVisible = widthMarkers.get('visible');
  461. gridVisible = grid.get('visible');
  462. widthMarkers.set({ visible: true });
  463. grid.set({ visible: true });
  464. (function(elements, thisElement){
  465. var savedDimensions = []
  466. if(elements.length === 0){
  467. savedDimensions.push({
  468. top: thisElement.get('top'),
  469. left: thisElement.get('left'),
  470. width: thisElement.get('width'),
  471. height: thisElement.get('height')
  472. })
  473. }else{
  474. for(var i = 0, l = elements.length; i < l; i++){
  475. savedDimensions.push({
  476. top: elements[i].get('top'),
  477. left: elements[i].get('left'),
  478. width: elements[i].get('width'),
  479. height: elements[i].get('height')
  480. })
  481. }
  482. }
  483. SuperGlue.get('history').do('actionHasStarted', function(){
  484. if(elements.length === 0){
  485. thisElement.set({
  486. top: savedDimensions[0].top,
  487. left: savedDimensions[0].left,
  488. width: savedDimensions[0].width,
  489. height: savedDimensions[0].height
  490. })
  491. }else{
  492. for(var i = 0, l = elements.length; i < l; i++){
  493. elements[i].set({
  494. top: savedDimensions[i].top,
  495. left: savedDimensions[i].left,
  496. width: savedDimensions[i].width,
  497. height: savedDimensions[i].height
  498. })
  499. }
  500. }
  501. })
  502. }).call(this, elements, thisElement)
  503. withinClickPrecision = false;
  504. }
  505. if(!withinClickPrecision){
  506. virtualTop += diffY;
  507. virtualLeft += diffX;
  508. if(elements.length === 0){
  509. if(virtualTop > 0){
  510. thisElement.set({ top: virtualTop });
  511. }else{
  512. thisElement.set({ top: 0 });
  513. }
  514. if(virtualLeft > 0){
  515. if(infiniteSpace || (virtualLeft + virtualWidth < pageWidth) ){
  516. thisElement.set({ left: virtualLeft });
  517. }else{
  518. thisElement.set({ left: (pageWidth - virtualWidth) });
  519. }
  520. }else{
  521. thisElement.set({ left: 0 });
  522. }
  523. }else{
  524. if(virtualTop > 0){
  525. for(var i = 0, l = elements.length; i < l; i++){
  526. elements[i].set({
  527. top: elementsOffsetsY[i] + virtualTop
  528. });
  529. }
  530. self.do('updateDimensions');
  531. }else{
  532. myNode.style.top = '0px';
  533. for(var i = 0, l = elements.length; i < l; i++){
  534. elements[i].set({
  535. top: elementsOffsetsY[i]
  536. });
  537. }
  538. }
  539. if(virtualLeft > 0){
  540. if(infiniteSpace || (virtualLeft + virtualWidth < pageWidth) ){
  541. for(var i = 0, l = elements.length; i < l; i++){
  542. elements[i].set({
  543. left: elementsOffsetsX[i] + virtualLeft
  544. });
  545. }
  546. self.do('updateDimensions');
  547. }else{
  548. myNode.style.left = (pageWidth - virtualWidth) + 'px';
  549. for(var i = 0, l = elements.length; i < l; i++){
  550. elements[i].set({
  551. left: elementsOffsetsX[i] + (pageWidth - virtualWidth)
  552. });
  553. }
  554. }
  555. }else{
  556. myNode.style.left = '0px';
  557. for(var i = 0, l = elements.length; i < l; i++){
  558. elements[i].set({
  559. left: elementsOffsetsX[i]
  560. });
  561. }
  562. }
  563. }
  564. startX = evt.pageX;
  565. startY = evt.pageY;
  566. }
  567. evt.stopPropagation();
  568. evt.preventDefault();
  569. };
  570. elementNode.addEventListener('mousedown', onMouseDown, false);
  571. }
  572. }
  573. }
  574. }});