[BUGFIX] Improve SVG page tree 33/54933/13
authorTymoteusz Motylewski <t.motylewski@gmail.com>
Thu, 7 Dec 2017 15:12:00 +0000 (16:12 +0100)
committerBenni Mack <benni@typo3.org>
Fri, 8 Dec 2017 11:12:16 +0000 (12:12 +0100)
- drag and drop doesn't work on firefox
- when request returns error or 500 code loader is still visible
- loader isn't visible on start
- SVG tree page is duplicate on change left actions menu
- loader is duplicate on change left action menu
- nodes on drag & drop are too sensitive

Releases: master
Resolves: #83224
Resolves: #83176
Resolves: #83177

Change-Id: I03acf2244fe860b7fafd6067d8dfb31ef5bca064
Reviewed-on: https://review.typo3.org/54933
Reviewed-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
Tested-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js
typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeDragDrop.js
typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js
typo3/sysext/backend/Resources/Public/JavaScript/SvgTree.js
typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf
typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf

index edec6d2..d6214dc 100644 (file)
@@ -100,6 +100,23 @@ define(['jquery',
       });
     };
 
+    /**
+     * Displays a notification message and refresh nodes
+     *
+     * @param error
+     */
+    PageTree.prototype.errorNotification = function (error) {
+      var title = TYPO3.lang.pagetree_networkErrorTitle;
+      var desc = TYPO3.lang.pagetree_networkErrorDesc;
+
+      if (error && (error.target.status || error.target.statusText)) {
+        title += ' - ' + (error.target.status || '')  + ' ' + (error.target.statusText || '');
+      }
+
+      Notification.error(title, desc);
+      this.loadData();
+    };
+
     PageTree.prototype.sendChangeCommand = function (data) {
       var _this = this;
       var params = '';
@@ -131,29 +148,34 @@ define(['jquery',
       d3.request(top.TYPO3.settings.ajaxUrls.record_process)
         .header('X-Requested-With', 'XMLHttpRequest')
         .header('Content-Type', 'application/x-www-form-urlencoded')
+        .on('error', function (error) {
+          _this.errorNotification(error);
+          throw error;
+        })
         .post(params, function (data) {
-          var response = JSON.parse(data.response);
-
-          if (response && response.hasErrors) {
-            if (response.messages) {
-              $.each(response.messages, function (id, message) {
-                Notification.error(
-                  message.title,
-                  message.message
-                );
-              });
+          if (data) {
+            var response = JSON.parse(data.response);
+
+            if (response && response.hasErrors) {
+              if (response.messages) {
+                $.each(response.messages, function (id, message) {
+                  Notification.error(
+                    message.title,
+                    message.message
+                  );
+                });
+              } else {
+                _this.errorNotification();
+              }
+
+              _this.nodesContainer.selectAll('.node').remove();
+              _this.update();
+              _this.nodesRemovePlaceholder();
             } else {
-              Notification.error(
-                'An error occurred',
-                'Try again later');
+              _this.loadData();
             }
-
-            _this.nodesContainer.selectAll('.node').remove();
-
-            _this.update();
-            _this.nodesRemovePlaceholder();
           } else {
-            _this.loadData();
+            _this.errorNotification();
           }
         });
     };
@@ -298,27 +320,33 @@ define(['jquery',
       d3.request(top.TYPO3.settings.ajaxUrls.page_tree_set_temporary_mount_point)
         .header('X-Requested-With', 'XMLHttpRequest')
         .header('Content-Type', 'application/x-www-form-urlencoded')
+        .on('error', function (error) {
+          _this.errorNotification(error);
+          throw error;
+        })
         .post(params, function (data) {
-          var response = JSON.parse(data.response);
-
-          if (response && response.hasErrors) {
-            if (response.messages) {
-              $.each(response.messages, function (id, message) {
-                Notification.error(
-                  message.title,
-                  message.message
-                );
-              });
+          if (data) {
+            var response = JSON.parse(data.response);
+
+            if (response && response.hasErrors) {
+              if (response.messages) {
+                $.each(response.messages, function (id, message) {
+                  Notification.error(
+                    message.title,
+                    message.message
+                  );
+                });
+              } else {
+                _this.errorNotification();
+              }
+
+              _this.update();
             } else {
-              Notification.error(
-                'An error occurred',
-                'Try again later');
+              _this.addMountPoint(response.mountPointPath);
+              _this.loadData();
             }
-
-            _this.update();
           } else {
-            _this.addMountPoint(response.mountPointPath);
-            _this.loadData();
+            _this.errorNotification();
           }
         });
     };
@@ -341,31 +369,38 @@ define(['jquery',
       d3.request(top.TYPO3.settings.ajaxUrls.record_process)
         .header('X-Requested-With', 'XMLHttpRequest')
         .header('Content-Type', 'application/x-www-form-urlencoded')
+        .on('error', function (error) {
+          _this.errorNotification(error);
+          throw error;
+        })
         .post(params, function (data) {
-          var response = JSON.parse(data.response);
-
-          if (response && response.hasErrors) {
-            if (response.messages) {
-              $.each(response.messages, function (id, message) {
-                Notification.error(
-                  message.title,
-                  message.message
-                );
-              });
+          if (data) {
+            var response = JSON.parse(data.response);
+
+            if (response && response.hasErrors) {
+              if (response.messages) {
+                $.each(response.messages, function (id, message) {
+                  Notification.error(
+                    message.title,
+                    message.message
+                  );
+                });
+              } else {
+                _this.errorNotification();
+              }
+
+              _this.nodesAddPlaceholder();
+              _this.loadData();
             } else {
-              Notification.error(
-                'An error occurred',
-                'Try again later');
+              node.name = node.newName;
+              _this.svg.select('.node-placeholder[data-uid="' + node.identifier + '"]').remove();
+              _this.update();
+              _this.nodesRemovePlaceholder();
             }
-
-            _this.nodesAddPlaceholder();
-            _this.loadData();
           } else {
-            node.name = node.newName;
-            _this.svg.select('.node-placeholder[data-uid="' + node.identifier + '"]').remove();
-            _this.update();
-            _this.nodesRemovePlaceholder();
+            _this.errorNotification();
           }
+
         });
     };
 
index 254b8cb..25c8c0b 100644 (file)
@@ -14,7 +14,7 @@
 /**
  * Module: TYPO3/CMS/Backend/PageTree/PageTreeDragDrop
  *
- * Provides drag&drop related funtionality for the SVG page tree
+ * Provides drag&drop related functionality for the SVG page tree
  */
 define([
     'jquery',
@@ -44,19 +44,34 @@ define([
       this.tree = svgTree;
     },
 
-    drag: function (node) {
+    /**
+     * Drag and drop for nodes
+     *
+     * Returns initialized d3.drag() function
+     */
+    drag: function () {
       var self = {};
       var _this = this;
       var tree = _this.tree;
 
-      //Returns deleting drop zone open 'transform' attribute value
+      /**
+       * Returns deleting drop zone open 'transform' attribute value
+       *
+       * @param node
+       * @returns {string}
+       */
       self.getDropZoneOpenTransform = function (node) {
         var svgWidth = parseFloat(tree.svg.style('width')) || 300;
 
         return 'translate(' + (svgWidth - 58 - node.x) + ', -10)';
       };
 
-      //Returns deleting drop zone close 'transform' attribute value
+      /**
+       * Returns deleting drop zone close 'transform' attribute value
+       *
+       * @param node
+       * @returns {string}
+       */
       self.getDropZoneCloseTransform = function (node) {
         var svgWidth = parseFloat(tree.svg.style('width')) || 300;
 
@@ -88,10 +103,10 @@ define([
             .attr('width', '50px')
             .attr('x', 0)
             .attr('y', 0)
-            .on('mouseover', function (node) {
+            .on('mouseover', function () {
               tree.nodeIsOverDelete = true;
             })
-            .on('mouseout', function (node) {
+            .on('mouseout', function () {
               tree.nodeIsOverDelete = false;
             });
 
@@ -101,16 +116,20 @@ define([
             .attr('dy', 15);
 
           _this.dropZoneDelete
-            .attr('transform', self.getDropZoneCloseTransform(node))
-            .transition(300)
-            .delay(300)
-            .attr('transform', self.getDropZoneOpenTransform(node))
-            .attr('data-open', 'true');
+            .attr('transform', self.getDropZoneCloseTransform(node));
         }
+
+        $.extend(self, _this.setDragStart());
       };
 
       self.dragDragged = function (node) {
-        if (tree.settings.isDragAnDrop !== true ||node.depth === 0) {
+        if (_this.isDragNodeDistanceMore(self, 10)) {
+          self.startDrag = true;
+        } else {
+          return false;
+        }
+
+        if (tree.settings.isDragAnDrop !== true || node.depth === 0) {
           return false;
         }
 
@@ -135,23 +154,48 @@ define([
             .addClass('nodes-wrapper--dragging');
         }
 
+        var left = 18;
+        var top = 15;
+
+        if (d3.event.sourceEvent && d3.event.sourceEvent.pageX) {
+          left += d3.event.sourceEvent.pageX;
+        }
+
+        if (d3.event.sourceEvent && d3.event.sourceEvent.pageY) {
+          top += d3.event.sourceEvent.pageY;
+        }
+
         $(document).find('.node-dd').css({
-          left: event.pageX + 18,
-          top: event.pageY + 15,
+          left: left,
+          top: top,
           display: 'block',
         });
 
         tree.settings.nodeDragPosition = false;
 
-        if (node.isOver || (tree.settings.nodeOver.node && tree.settings.nodeOver.node.parentsUid.indexOf(node.identifier) !== -1)) {
+        if (node.isOver
+          || (tree.settings.nodeOver.node && tree.settings.nodeOver.node.parentsUid.indexOf(node.identifier) !== -1)
+          || !tree.isOverSvg) {
+
           _this.addNodeDdClass({ $nodeDd: $nodeDd, $nodesWrap: $nodesWrap, className: 'nodrop' });
 
-          if (_this.dropZoneDelete && _this.dropZoneDelete.attr('data-open') !== 'true') {
+          if (!tree.isOverSvg) {
+            _this.tree.nodesBgContainer
+              .selectAll('.node-bg__border')
+              .style('display', 'none');
+          }
+
+          if (_this.dropZoneDelete && _this.dropZoneDelete.attr('data-open') !== 'true' && tree.isOverSvg) {
             _this.dropZoneDelete
               .transition(300)
               .attr('transform', self.getDropZoneOpenTransform(node))
               .attr('data-open', 'true');
           }
+        } else if (!tree.settings.nodeOver.node) {
+          _this.addNodeDdClass({ $nodeDd: $nodeDd, $nodesWrap: $nodesWrap, className: 'nodrop' });
+          _this.tree.nodesBgContainer
+            .selectAll('.node-bg__border')
+            .style('display', 'none');
         } else {
           if (_this.dropZoneDelete && _this.dropZoneDelete.attr('data-open') !== 'false') {
             _this.dropZoneDelete
@@ -165,7 +209,9 @@ define([
       };
 
       self.dragEnd = function (node) {
-        if (tree.settings.isDragAnDrop !== true || node.depth === 0) {
+        _this.setDragEnd();
+
+        if (!self.startDrag || tree.settings.isDragAnDrop !== true || node.depth === 0) {
           return false;
         }
 
@@ -206,9 +252,11 @@ define([
           !(node.isOver
             || (tree.settings.nodeOver.node && tree.settings.nodeOver.node.parentsUid.indexOf(node.identifier) !== -1)
             || !tree.settings.canNodeDrag
+            || !tree.isOverSvg
           )
         ) {
           var options = _this.changeNodePosition({ droppedNode: droppedNode });
+
           var modalText = options.position === 'in' ? TYPO3.lang['mess.move_into'] : TYPO3.lang['mess.move_after'];
           modalText = modalText.replace('%s', options.node.name).replace('%s', options.target.name);
 
@@ -286,14 +334,13 @@ define([
 
     changeNodeClasses: function () {
       var elementNodeBg = this.tree.svg.select('.node-over');
+      var $svg = $(this.tree.svg.node());
+      var $nodesWrap = $svg.find('.nodes-wrapper');
+      var $nodeDd = $svg.siblings('.node-dd');
+      var nodeBgBorder = this.tree.nodesBgContainer.selectAll('.node-bg__border');
 
-      if (elementNodeBg.size()) {
-        var $svg = $(this.tree.svg.node());
-        var $nodesWrap = $svg.find('.nodes-wrapper');
-        var $nodeDd = $svg.siblings('.node-dd');
-
+      if (elementNodeBg.size() && this.tree.isOverSvg) {
         //line between nodes
-        var nodeBgBorder = this.tree.nodesBgContainer.selectAll('.node-bg__border');
         if (nodeBgBorder.empty()) {
           nodeBgBorder = this.tree.nodesBgContainer
             .append('rect')
@@ -375,6 +422,16 @@ define([
           });
           this.tree.settings.nodeDragPosition = 'in';
         }
+      } else {
+        this.tree.nodesBgContainer
+          .selectAll('.node-bg__border')
+          .style('display', 'none');
+
+        this.addNodeDdClass({
+          $nodeDd: $nodeDd,
+          $nodesWrap: $nodesWrap,
+          className: 'nodrop',
+        });
       }
     },
 
@@ -406,6 +463,48 @@ define([
       }
     },
 
+    /**
+     * Check if node is dragged at least @distance
+     *
+     * @param {Object} data
+     * @param {Integer} distance
+     * @returns {boolean}
+     */
+    isDragNodeDistanceMore: function (data, distance) {
+      return (data.startDrag ||
+         (((data.startPageX - distance) > d3.event.sourceEvent.pageX) ||
+          ((data.startPageX + distance) < d3.event.sourceEvent.pageX) ||
+          ((data.startPageY - distance) > d3.event.sourceEvent.pageY) ||
+          ((data.startPageY + distance) < d3.event.sourceEvent.pageY)));
+    },
+
+    /**
+     * Sets the same parameters on start for method drag() and dragToolbar()
+     *
+     * @returns {{startPageX, startPageY, startDrag: boolean}}
+     */
+    setDragStart: function () {
+      $('body iframe').css({ 'pointer-events': 'none' });
+
+      return {
+        startPageX: d3.event.sourceEvent.pageX,
+        startPageY: d3.event.sourceEvent.pageY,
+        startDrag: false,
+      };
+    },
+
+    /**
+     * Sets the same parameters on end for method drag() and dragToolbar()
+     */
+    setDragEnd: function () {
+      $('body iframe').css({ 'pointer-events': '' });
+    },
+
+    /**
+     * Drag and drop for toolbar new elements
+     *
+     * Returns method from d3js
+     */
     dragToolbar: function () {
       var self = {};
       var _this = this;
@@ -417,9 +516,16 @@ define([
         self.tooltip = $(this).attr('tooltip');
         self.icon = $(this).data('tree-icon');
         self.isDragged = false;
+        $.extend(self, _this.setDragStart());
       };
 
       self.dragDragged = function () {
+        if (_this.isDragNodeDistanceMore(self, 10)) {
+          self.startDrag = true;
+        } else {
+          return;
+        }
+
         var $svg = $(_this.tree.svg.node());
 
         if (self.isDragged === false) {
@@ -433,9 +539,20 @@ define([
             .addClass('nodes-wrapper--dragging');
         }
 
+        var left = 18;
+        var top = 15;
+
+        if (d3.event.sourceEvent && d3.event.sourceEvent.pageX) {
+          left += d3.event.sourceEvent.pageX;
+        }
+
+        if (d3.event.sourceEvent && d3.event.sourceEvent.pageY) {
+          top += d3.event.sourceEvent.pageY;
+        }
+
         $(document).find('.node-dd').css({
-          left: event.pageX + 18,
-          top: event.pageY + 15,
+          left: left,
+          top: top,
           display: 'block',
         });
 
@@ -443,6 +560,12 @@ define([
       };
 
       self.dragEnd = function () {
+        _this.setDragEnd();
+
+        if (!self.startDrag) {
+          return;
+        }
+
         var $svg = $(_this.tree.svg.node());
         var $nodesBg = $svg.find('.nodes-bg');
 
@@ -477,11 +600,11 @@ define([
           .selectAll('.node-bg__border')
           .style('display', 'none');
 
-        if ((_this.tree.settings.isDragAnDrop !== true) || !_this.tree.settings.nodeOver.node) {
+        if (_this.tree.settings.isDragAnDrop !== true || !_this.tree.settings.nodeOver.node || !_this.tree.isOverSvg) {
           return false;
         }
 
-        if (_this.tree.settings.canNodeDrag && !((_this.tree.settings.isDragAnDrop !== true) || !_this.tree.settings.nodeOver.node)) {
+        if (_this.tree.settings.canNodeDrag) {
           var data = {
             type: self.id,
             name: self.name,
index a0900b4..be22f69 100644 (file)
@@ -31,9 +31,12 @@ define(['jquery',
           '<div>' +
             '<div id="svg-toolbar" class="svg-toolbar"></div>' +
               '<div id="typo3-pagetree-treeContainer">' +
-                '<div id="typo3-pagetree-tree" class="svg-tree-wrapper" style="height:1000px;"></div>' +
+                '<div id="typo3-pagetree-tree" class="svg-tree-wrapper" style="height:1000px;">' +
+                  '<div class="node-loader"></div>' +
+                '</div>' +
               '</div>' +
             '</div>' +
+          '<div class="svg-tree-loader"></div>' +
         '</div>',
     };
 
@@ -46,7 +49,23 @@ define(['jquery',
       $(document).ready(function () {
         var $element = $(selector);
         var tree = new PageTree();
-        $element.append(PageTreeElement.template);
+
+        if ($element.html().trim().length === 0) {
+          $element.append(PageTreeElement.template);
+        }
+
+        if ($('.node-loader').html().trim().length === 0) {
+          Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function (spinner) {
+            $('.node-loader').append(spinner);
+          });
+        }
+
+        if ($('.svg-tree-loader').html().trim().length === 0) {
+          Icons.getIcon('spinner-circle-light', Icons.sizes.large).done(function (spinner) {
+            $('.svg-tree-loader').append(spinner);
+          });
+        }
+
         var dataUrl = top.TYPO3.settings.ajaxUrls.page_tree_data;
         var configurationUrl = top.TYPO3.settings.ajaxUrls.page_tree_configuration;
 
@@ -63,14 +82,6 @@ define(['jquery',
           var pageTreeToolbar = new PageTreeToolbar();
           pageTreeToolbar.initialize('#typo3-pagetree-tree');
           $('#svg-toolbar').data('tree-show-toolbar', true);
-
-          Icons.getIcon('spinner-circle-light', Icons.sizes.small).done(function (spinner) {
-            $('#typo3-pagetree-tree').append('<div class="node-loader">' + spinner + '</div>');
-          });
-
-          Icons.getIcon('spinner-circle-light', Icons.sizes.large).done(function (spinner) {
-            $('.svg-tree').append('<div class="svg-tree-loader">' + spinner + '</div>');
-          });
         }
       });
     };
index e221863..6af870e 100644 (file)
@@ -56,6 +56,13 @@ define(
       };
 
       /**
+       * Check if cursor is over svg
+       *
+       * @type {boolean}
+       */
+      this.isOverSvg = false;
+
+      /**
        * Root <svg> element
        *
        * @type {Selection}
@@ -182,7 +189,13 @@ define(
           .select($wrapper[0]);
         this.svg = this.d3wrapper.append('svg')
           .attr('version', '1.1')
-          .attr('width', '100%');
+          .attr('width', '100%')
+          .on('mouseover', function () {
+            _this.isOverSvg = true;
+          })
+          .on('mouseout', function () {
+            _this.isOverSvg = false;
+          });
         this.container = this.svg
           .append('g')
           .attr('class', 'nodes-wrapper')
@@ -252,7 +265,22 @@ define(
         _this.nodesAddPlaceholder();
 
         d3.json(this.settings.dataUrl, function (error, json) {
-          if (error) throw error;
+          if (error) {
+            var title = TYPO3.lang.pagetree_networkErrorTitle;
+            var desc = TYPO3.lang.pagetree_networkErrorDesc;
+
+            if (error.target.status || error.target.statusText) {
+              title += ' - ' + (error.target.status || '')  + ' ' + (error.target.statusText || '');
+            }
+
+            Notification.error(
+              title,
+              desc);
+
+            _this.nodesRemovePlaceholder();
+            throw error;
+          }
+
           var nodes = Array.isArray(json) ? json : [];
           _this.setParametersNode(nodes);
           _this.dispatch.call('loadDataAfter', _this);
index 7fe3fc1..f800511 100644 (file)
@@ -1264,17 +1264,6 @@ Do you want to refresh it now?</source>
                        <trans-unit id="toolbarItems.sysinfo.typo3-version">
                                <source>TYPO3 Version</source>
                        </trans-unit>
-                       <trans-unit id="ExtDirect.namespaceError" xml:space="preserve">
-                               <source>Ext Direct error in "%s" with namespace: "%s"\n
-Try to clear the TYPO3 cache and / or use parameter no_cache=1 as parameter in URL typo3/index.php\n\n
-Check also the following points:\n
-- configuration in ext_localconf.php: registration key should be like "TYPO3.MyExtension.Sample"\n
-- URL typo3/index.php: namespace parameter should be like: "TYPO3.MyExtension"\n
-- javascript: method\'s name should be like: "TYPO3.MyExtension.Sample.myMethod"\n</source>
-                       </trans-unit>
-                       <trans-unit id="ExtDirect.noNamespace">
-                               <source>Ext Direct error in "%s": no namespace has been found.</source>
-                       </trans-unit>
                        <trans-unit id="extension.not.installed">
                                <source>Extension "%s" is not installed.</source>
                        </trans-unit>
index ea5de65..d32c286 100644 (file)
                        <trans-unit id="fileUpload_uploadSuccess">
                                <source>{0} was successfully uploaded!</source>
                        </trans-unit>
+                       <trans-unit id="pagetree_networkErrorTitle">
+                               <source>Page tree error</source>
+                       </trans-unit>
+                       <trans-unit id="pagetree_networkErrorDesc">
+                               <source>Got unexpected response from the server. Please check logs for details.</source>
+                       </trans-unit>
                        <trans-unit id="fileUpload_errorQueueLimitExceeded">
                                <source>Too many files selected</source>
                        </trans-unit>
                        <trans-unit id="liveSearch_helpDescriptionPages">
                                <source>#page:Home will search for all pages with the title "Home"</source>
                        </trans-unit>
-                       <trans-unit id="extDirect_timeoutHeader">
-                               <source>Connection Problem</source>
-                       </trans-unit>
-                       <trans-unit id="extDirect_timeoutMessage">
-                               <source>Sorry, but an error occurred while connecting to the server. Please check your network connection.</source>
-                       </trans-unit>
                        <trans-unit id="viewPort_tooltipModuleMenuSplit">
                                <source>Drag to resize the Modules Menu</source>
                        </trans-unit>