From 5f25826edd18e5bfb3e0ca6f42116ea1cdfb2ed0 Mon Sep 17 00:00:00 2001 From: Tymoteusz Motylewski <t.motylewski@gmail.com> Date: Thu, 7 Dec 2017 16:12:00 +0100 Subject: [PATCH] [BUGFIX] Improve SVG page tree - 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> --- .../Public/JavaScript/PageTree/PageTree.js | 147 +++++++++------ .../JavaScript/PageTree/PageTreeDragDrop.js | 177 +++++++++++++++--- .../JavaScript/PageTree/PageTreeElement.js | 31 ++- .../Resources/Public/JavaScript/SvgTree.js | 32 +++- .../Private/Language/locallang_core.xlf | 11 -- .../Private/Language/locallang_misc.xlf | 12 +- 6 files changed, 298 insertions(+), 112 deletions(-) diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js index edec6d293886..d6214dcfcf0b 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTree.js @@ -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(); } + }); }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeDragDrop.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeDragDrop.js index 254b8cb82e48..25c8c0bbc500 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeDragDrop.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeDragDrop.js @@ -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, diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js index a0900b4870d7..be22f694928c 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/PageTree/PageTreeElement.js @@ -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>'); - }); } }); }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/SvgTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/SvgTree.js index e2218631d483..6af870e72ffc 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/SvgTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/SvgTree.js @@ -55,6 +55,13 @@ define( exclusiveNodesIdentifiers: '', }; + /** + * Check if cursor is over svg + * + * @type {boolean} + */ + this.isOverSvg = false; + /** * Root <svg> element * @@ -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); diff --git a/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf b/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf index 7fe3fc16e154..f800511c17cf 100644 --- a/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf +++ b/typo3/sysext/lang/Resources/Private/Language/locallang_core.xlf @@ -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> diff --git a/typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf b/typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf index ea5de65e353d..d32c286f3e13 100644 --- a/typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf +++ b/typo3/sysext/lang/Resources/Private/Language/locallang_misc.xlf @@ -333,6 +333,12 @@ <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> @@ -414,12 +420,6 @@ <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> -- GitLab