From 93875e644426e4f277458b1193014036dfa4a7a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mauro=20Ferr=C3=A3o?= Date: Mon, 13 Nov 2023 09:47:09 +0000 Subject: [PATCH] Layout Editor: Avoid too many viewer refreshes (#2220) * Layout Editor: Avoid too many viewer refreshes relates to xibosignageltd/xibo-private#547 * Fix to delete canvas widget relates to xibosignageltd/xibo-private#547 --- ui/src/editor-core/layer-manager.js | 2 +- ui/src/editor-core/widget.js | 25 ++- ui/src/layout-editor/layout.js | 25 +++ ui/src/layout-editor/main.js | 51 ++++- ui/src/layout-editor/viewer.js | 286 +++++++++++++++++++--------- ui/src/style/layout-editor.scss | 2 +- ui/src/templates/viewer.hbs | 8 +- 7 files changed, 293 insertions(+), 106 deletions(-) diff --git a/ui/src/editor-core/layer-manager.js b/ui/src/editor-core/layer-manager.js index 4c3dd6b2a0..6c768e4746 100644 --- a/ui/src/editor-core/layer-manager.js +++ b/ui/src/editor-core/layer-manager.js @@ -294,7 +294,7 @@ LayerManager.prototype.render = function(reset) { $viewerObject; // Select in viewer - lD.viewer.selectElement($auxTarget); + lD.viewer.selectObject($auxTarget); // Mark object with selected from manager class $auxTarget.addClass('selected-from-layer-manager'); diff --git a/ui/src/editor-core/widget.js b/ui/src/editor-core/widget.js index da2c85512e..a16caa7629 100644 --- a/ui/src/editor-core/widget.js +++ b/ui/src/editor-core/widget.js @@ -762,13 +762,32 @@ Widget.prototype.saveElements = function( // Remove region app.layout.deleteObject('region', this.parent.regionId) .then(() => { - reloadLayout(true); + // Remove object from structure + app.layout.removeFromStructure( + 'region', + this.parent.regionId, + 'canvas', + ); + + // Refresh layer manager + app.viewer.layerManager.render(); + + // Remove canvas from viewer + app.viewer.DOMObject.find('.designer-region-canvas').remove(); }); } else if (removeCurrentWidget) { // Remove widget app.layout.deleteObject('widget', this.widgetId) .then(() => { - reloadLayout(true); + // Remove object from structure + app.layout.removeFromStructure( + 'widget', + this.widgetId, + 'canvas', + ); + + // Refresh layer manager + app.viewer.layerManager.render(); }); } @@ -1083,7 +1102,7 @@ Widget.prototype.removeElement = function( lD.selectObject({ reloadViewer: false, }); - lD.viewer.selectElement(null, false, false); + lD.viewer.selectObject(null, false, false); } else if (lD.selectedObject.type != 'layout' && save) { // If we have a selected object other than layout, reload properties panel lD.propertiesPanel.render(lD.selectedObject); diff --git a/ui/src/layout-editor/layout.js b/ui/src/layout-editor/layout.js index 8f8cde84eb..060336311d 100644 --- a/ui/src/layout-editor/layout.js +++ b/ui/src/layout-editor/layout.js @@ -293,6 +293,31 @@ Layout.prototype.createDataStructure = function(data) { this.durationFormatted = lD.common.timeFormat(layoutDuration); }; +/** + * Remove object from data structure + * @param {string} objectType - Object type + * @param {string} objectId - Object ID + * @param {string} auxId - Object sub type or parent region (if widget) + */ +Layout.prototype.removeFromStructure = function(objectType, objectId, auxId) { + const self = this; + + if (objectType === 'region' && auxId === 'canvas') { + // Set canvas as empty object + self.canvas = {}; + } else if (objectType === 'region') { + delete self.regions[objectId]; + } else if (objectType === 'widget' && auxId === 'canvas') { + delete self.canvas.widgets[ + 'widget' + '_' + self.canvas.regionId + '_' + objectId + ]; + } else if (objectType === 'widget') { + delete self.regions[auxId].widgets[ + 'widget' + '_' + auxId + '_' + objectId + ]; + } +}; + /** * Calculate timeline values ( duration, loops ) * based on widget and region duration diff --git a/ui/src/layout-editor/main.js b/ui/src/layout-editor/main.js index f11594397b..c36c3aed13 100644 --- a/ui/src/layout-editor/main.js +++ b/ui/src/layout-editor/main.js @@ -618,6 +618,7 @@ lD.selectObject = * Refresh designer * @param {boolean} [reloadToolbar=false] - Update toolbar * @param {boolean} [reloadViewer=false] - Reload viewer + * @param {object} [reloadViewerTarget={}] - Reload viewer target * @param {boolean} [reloadPropertiesPanel=false] - Reload properties panel * @param {boolean} [reloadLayerManager=true] - Reload layer manager */ @@ -625,6 +626,7 @@ lD.refreshEditor = function( { reloadToolbar = false, reloadViewer = false, + reloadViewerTarget = {}, reloadPropertiesPanel = false, reloadLayerManager = true, } = {}, @@ -642,7 +644,7 @@ lD.refreshEditor = function( // Properties panel and viewer (reloadPropertiesPanel) && this.propertiesPanel.render(this.selectedObject); - (reloadViewer) && this.viewer.render(reloadViewer); + (reloadViewer) && this.viewer.render(reloadViewer, reloadViewerTarget); (reloadLayerManager) && this.viewer.layerManager.render(); }; @@ -685,18 +687,26 @@ lD.reloadData = function( // get Layout folder id lD.folderId = lD.layout.folderId; - // Select the same object ( that will refresh the layout too ) + // Select the same object const selectObjectId = (lD.selectedObject.type === 'element') ? lD.selectedObject.elementId : lD.selectedObject.id; + const $selectedDOMTarget = $('#' + selectObjectId); + lD.selectObject({ - target: $('#' + selectObjectId), + target: $selectedDOMTarget, forceSelect: true, refreshEditor: false, // Don't refresh the editor here reloadPropertiesPanel: false, }); + // Check if the selected object is a temporary one + const targetToRender = + $selectedDOMTarget.hasClass('viewer-temporary-object') ? + lD.selectedObject : + {}; + // Reload the form helper connection formHelpers.setup(lD, lD.layout); @@ -722,6 +732,7 @@ lD.reloadData = function( refreshEditor && lD.refreshEditor({ reloadToolbar: reloadToolbar, reloadViewer: reloadViewer, + reloadViewerTarget: targetToRender, reloadPropertiesPanel: reloadPropertiesPanel, }); @@ -1219,6 +1230,13 @@ lD.deleteObject = function( } else { lD.common.showLoadingScreen('deleteObject'); + // Hide in viewer + lD.viewer.toggleObject( + objectType, + objectId, + true, + ); + lD.layout.deleteObject( objectType, objectId, @@ -1253,7 +1271,7 @@ lD.deleteObject = function( reloadViewer: false, reloadPropertiesPanel: false, }); - lD.viewer.selectElement(); + lD.viewer.selectObject(); // Render properties panel with action tab lD.propertiesPanel.render( @@ -1262,10 +1280,16 @@ lD.deleteObject = function( true, // Open action tab ); } else { + // Remove widget from viewer + lD.viewer.removeObject( + objectType, + objectId, + ); + // Reload data ( if not a drawer widget) lD.reloadData(lD.layout, { - refreshEditor: true, + refreshEditor: false, }); } @@ -1276,6 +1300,13 @@ lD.deleteObject = function( // Show error returned or custom message to the user let errorMessage = ''; + // Show back in viewer + lD.viewer.toggleObject( + objectType, + objectId, + false, + ); + if (typeof error == 'string') { errorMessage = error; } else { @@ -2723,7 +2754,7 @@ lD.openPlaylistEditor = function(playlistId, region) { .removeClass('hidden'); // Deselect viewer element - lD.viewer.selectElement(); + lD.viewer.selectObject(); // Show playlist editor $playlistEditorPanel.removeClass('hidden'); @@ -3074,7 +3105,7 @@ lD.openContextMenu = function(obj, position = {x: 0, y: 0}) { lD.selectObject({ target: lD.viewer.DOMObject.find('#' + layoutObject.id), }); - lD.viewer.selectElement($viewerRegion); + lD.viewer.selectObject($viewerRegion); } else if (target.data('action') == 'Ungroup') { // Get widget const elementsWidget = @@ -3390,7 +3421,7 @@ lD.openGroupContextMenu = function(objs, position = {x: 0, y: 0}) { if (deletedIndex == $regionsToBeDeleted.length) { // Stop deleting and deselect all elements - lD.viewer.selectElement(); + lD.viewer.selectObject(); // Hide loader lD.common.hideLoadingScreen('deleteMultiObject'); @@ -3410,7 +3441,7 @@ lD.openGroupContextMenu = function(objs, position = {x: 0, y: 0}) { deleteNext(); } else { // If we're not deleting any region, deselect elements now - lD.viewer.selectElement(); + lD.viewer.selectObject(); } } else if (target.data('action') == 'Group') { // Group elements @@ -4766,7 +4797,7 @@ lD.editDrawerWidget = function(actionData, actionEditMode = true) { }); // Select element in viewer - lD.viewer.selectElement($target); + lD.viewer.selectObject($target); // 4. Open property panel with drawer widget or same object lD.propertiesPanel.render( diff --git a/ui/src/layout-editor/viewer.js b/ui/src/layout-editor/viewer.js index 3adf541b42..dab4f12ab7 100644 --- a/ui/src/layout-editor/viewer.js +++ b/ui/src/layout-editor/viewer.js @@ -27,6 +27,7 @@ const LayerManager = require('../editor-core/layer-manager.js'); const DateFormatHelper = require('../helpers/date-format-helper.js'); const viewerTemplate = require('../templates/viewer.hbs'); +const viewerRegionTemplate = require('../templates/viewer-region.hbs'); const viewerWidgetTemplate = require('../templates/viewer-widget.hbs'); const viewerLayoutPreview = require('../templates/viewer-layout-preview.hbs'); const viewerActionEditRegionTemplate = @@ -188,8 +189,11 @@ Viewer.prototype.getLayoutOrientation = function(width, height) { /** * Render viewer * @param {object} forceReload - Force reload + * @param {object} target - Reload only target */ -Viewer.prototype.render = function(forceReload = false) { +Viewer.prototype.render = function(forceReload = false, target = {}) { + const renderSingleObject = (!$.isEmptyObject(target)); + // Check background colour and set theme const hsvColor = (this.parent.layout.backgroundColor) ? @@ -222,90 +226,150 @@ Viewer.prototype.render = function(forceReload = false) { // Set reload to false this.reload = false; - // Render the viewer - this.DOMObject.html(viewerTemplate()); + if (renderSingleObject) { + const self = this; + const createCanvas = function() { + if ( + lD.layout.canvas && + self.DOMObject.find('.designer-region-canvas').length === 0 + ) { + self.DOMObject.find('.layout-live-preview').append( + `
+
`, + ); + } + }; - const $viewerContainer = this.DOMObject; + // Render single object + if ( + target.type === 'widget' || + target.type === 'region' + ) { + const regionId = (target.type === 'region') ? + target.id : + target.regionId; + const regionToRender = lD.layout.regions[regionId]; - // If preview is playing, refresh the bottombar - if (this.previewPlaying && this.parent.selectedObject.type == 'layout') { - this.parent.bottombar.render(this.parent.selectedObject); - } + // Add region template to viewer + this.DOMObject.find('#regions').append(viewerRegionTemplate( + regionToRender, + )); - // Show loading template - $viewerContainer.html(loadingTemplate()); + // If it's a zone, just update the region dimensions + if ( + target.type === 'region' && + target.subType === 'zone' + ) { + this.updateRegion(regionToRender); + } else { + this.renderRegion(regionToRender); + } + } else if (target.type === 'element') { + createCanvas(); - // Set preview play as false - this.previewPlaying = false; + // Render element + this.renderElement(target, lD.layout.canvas); + } else if (target.type === 'element-group') { + createCanvas(); - // Reset container properties - $viewerContainer.css('background', - (this.theme == 'dark') ? '#2c2d2e' : '#F3F8FF', - ); - $viewerContainer.css('border', 'none'); + // Render all elements from group + Object.values(target.elements).forEach((element) => { + self.renderElement(element, lD.layout.canvas); + }); + } + } else { + // Render full layout - // Apply viewer scale to the layout - this.containerObjectDimensions = - this.scaleObject(lD.layout, $viewerContainer); + // Render the viewer + this.DOMObject.html(viewerTemplate()); - this.orientation = this.getLayoutOrientation( - this.containerObjectDimensions.width, - this.containerObjectDimensions.height, - ); + const $viewerContainer = this.DOMObject; - // Apply viewer scale to the layout - const scaledLayout = lD.layout.scale($viewerContainer); - - const html = viewerTemplate({ - type: 'layout', - renderLayout: true, - containerStyle: 'layout-player', - dimensions: this.containerObjectDimensions, - layout: scaledLayout, - trans: viewerTrans, - theme: this.theme, - orientation: this.orientation, - }); + // If preview is playing, refresh the bottombar + if (this.previewPlaying && this.parent.selectedObject.type == 'layout') { + this.parent.bottombar.render(this.parent.selectedObject); + } - // Replace container html - $viewerContainer.html(html); + // Show loading template + $viewerContainer.html(loadingTemplate()); - // Render background image or color to the preview - if (lD.layout.backgroundImage === null) { - $viewerContainer.find('.viewer-object') - .css('background', lD.layout.backgroundColor); - } else { - // Get API link - let linkToAPI = urlsForApi.layout.downloadBackground.url; - // Replace ID in the link - linkToAPI = linkToAPI.replace(':id', lD.layout.layoutId); - - $viewerContainer.find('.viewer-object') - .css({ - background: - 'url(\'' + linkToAPI + '?preview=1&width=' + - (lD.layout.width * this.containerObjectDimensions.scale) + - '&height=' + - ( - lD.layout.height * - this.containerObjectDimensions.scale - ) + - '&proportional=0&layoutBackgroundId=' + - lD.layout.backgroundImage + '\') top center no-repeat', - backgroundSize: '100% 100%', - backgroundColor: lD.layout.backgroundColor, - }); - } + // Set preview play as false + this.previewPlaying = false; - // Render preview regions/widgets - for (const regionIndex in lD.layout.regions) { - if (lD.layout.regions.hasOwnProperty(regionIndex)) { - this.renderRegion(lD.layout.regions[regionIndex]); + // Reset container properties + $viewerContainer.css('background', + (this.theme == 'dark') ? '#2c2d2e' : '#F3F8FF', + ); + $viewerContainer.css('border', 'none'); + + // Apply viewer scale to the layout + this.containerObjectDimensions = + this.scaleObject(lD.layout, $viewerContainer); + + this.orientation = this.getLayoutOrientation( + this.containerObjectDimensions.width, + this.containerObjectDimensions.height, + ); + + // Apply viewer scale to the layout + const scaledLayout = lD.layout.scale($viewerContainer); + + const html = viewerTemplate({ + type: 'layout', + renderLayout: true, + containerStyle: 'layout-player', + dimensions: this.containerObjectDimensions, + layout: scaledLayout, + renderCanvas: (!$.isEmptyObject(lD.layout.canvas)), + trans: viewerTrans, + theme: this.theme, + orientation: this.orientation, + }); + + // Replace container html + $viewerContainer.html(html); + + // Render background image or color to the preview + if (lD.layout.backgroundImage === null) { + $viewerContainer.find('.viewer-object') + .css('background', lD.layout.backgroundColor); + } else { + // Get API link + let linkToAPI = urlsForApi.layout.downloadBackground.url; + // Replace ID in the link + linkToAPI = linkToAPI.replace(':id', lD.layout.layoutId); + + $viewerContainer.find('.viewer-object') + .css({ + background: + 'url(\'' + linkToAPI + '?preview=1&width=' + + (lD.layout.width * this.containerObjectDimensions.scale) + + '&height=' + + ( + lD.layout.height * + this.containerObjectDimensions.scale + ) + + '&proportional=0&layoutBackgroundId=' + + lD.layout.backgroundImage + '\') top center no-repeat', + backgroundSize: '100% 100%', + backgroundColor: lD.layout.backgroundColor, + }); } - } - // Render preview canvas if it's not an empty object - (!$.isEmptyObject(lD.layout.canvas)) && this.renderCanvas(lD.layout.canvas); + // Render viewer regions/widgets + for (const regionIndex in lD.layout.regions) { + if (lD.layout.regions.hasOwnProperty(regionIndex)) { + this.renderRegion(lD.layout.regions[regionIndex]); + } + } + + // Render viewer canvas if it's not an empty object + (!$.isEmptyObject(lD.layout.canvas)) && this.renderCanvas(lD.layout.canvas); + } // Handle UI interactions this.handleInteractions(); @@ -624,7 +688,7 @@ Viewer.prototype.handleInteractions = function() { reloadViewer: false, clickPosition: $(e.target).hasClass('layout') ? clickPosition : null, }); - self.selectElement(); + self.selectObject(); } else if ( $(e.target).hasClass('group-edit-btn') ) { @@ -674,7 +738,7 @@ Viewer.prototype.handleInteractions = function() { }); } - self.selectElement($(e.target), shiftIsPressed); + self.selectObject($(e.target), shiftIsPressed); } else if ( ( $(e.target).data('subType') === 'zone' || @@ -695,7 +759,7 @@ Viewer.prototype.handleInteractions = function() { target: $(e.target), }); } - self.selectElement($(e.target), shiftIsPressed); + self.selectObject($(e.target), shiftIsPressed); } else if ( $(e.target).find('.designer-widget').length > 0 && !$(e.target).find('.designer-widget').hasClass('selected') && @@ -711,7 +775,7 @@ Viewer.prototype.handleInteractions = function() { clickPosition: clickPosition, }); } - self.selectElement($(e.target), shiftIsPressed); + self.selectObject($(e.target), shiftIsPressed); } else if ( $(e.target).hasClass('designer-element') && !$(e.target).hasClass('selected') @@ -726,7 +790,7 @@ Viewer.prototype.handleInteractions = function() { clickPosition: clickPosition, }); } - self.selectElement($(e.target), shiftIsPressed); + self.selectObject($(e.target), shiftIsPressed); } else if ( $(e.target).hasClass('group-select-overlay') && !$(e.target).parent().hasClass('selected') @@ -741,7 +805,7 @@ Viewer.prototype.handleInteractions = function() { clickPosition: clickPosition, }); } - self.selectElement($(e.target).parent(), shiftIsPressed); + self.selectObject($(e.target).parent(), shiftIsPressed); } }, 200); } else { @@ -763,7 +827,7 @@ Viewer.prototype.handleInteractions = function() { lD.selectObject({ target: $(e.target), }); - self.selectElement($(e.target), shiftIsPressed); + self.selectObject($(e.target), shiftIsPressed); } else if ( $(e.target).hasClass('group-select-overlay') ) { @@ -773,7 +837,7 @@ Viewer.prototype.handleInteractions = function() { } else { // Move out from group editing lD.selectObject(); - self.selectElement(); + self.selectObject(); } } } @@ -977,7 +1041,7 @@ Viewer.prototype.renderRegion = function( // If region is selected, update moveable if (region.selected) { - this.selectElement($container); + this.selectObject($container); } // If there's no widget, return @@ -1094,7 +1158,7 @@ Viewer.prototype.renderRegion = function( // If widget is selected, update moveable for the region if (widget && widget.selected) { - this.selectElement($container); + this.selectObject($container); } // Select droppables in the region @@ -1304,6 +1368,12 @@ Viewer.prototype.updateRegion = _.throttle(function( if (redrawLayerManager) { lD.viewer.layerManager.render(); } + + // If region is selected, but not on the container, do it + if (region.selected && !$container.hasClass('selected')) { + lD.viewer.selectObject($container); + lD.viewer.updateMoveable(); + } }, drawThrottle); @@ -1401,7 +1471,7 @@ Viewer.prototype.renderElement = function( // If group is selected, add selected class if (group.selected) { - this.selectElement($groupContainer); + this.selectObject($groupContainer); } // If group has source, add it to the container @@ -2525,7 +2595,7 @@ Viewer.prototype.saveElementGroupProperties = function( * @param {boolean} multiSelect - Select another object * @param {boolean} removeEditFromGroup */ -Viewer.prototype.selectElement = function( +Viewer.prototype.selectObject = function( element = null, multiSelect = false, removeEditFromGroup = true, @@ -2598,7 +2668,6 @@ Viewer.prototype.updateMoveable = function( // Get selected element const $selectedElement = this.DOMObject.find('.selected'); - const multipleSelected = ($selectedElement.length > 1); // Update moveable if we have a selected element, and is not a drawerWidget @@ -3215,6 +3284,43 @@ Viewer.prototype.addActionEditArea = function( this.update(); }; +/** + * Remove object from viewer + * @param {string} objectType - Object type + * @param {string} objectId - Object ID + */ +Viewer.prototype.removeObject = function(objectType, objectId) { + // Remove from DOM + this.DOMObject + .find(`[data-type="${objectType}"][data-${objectType}-id="${objectId}"]`) + .remove(); + + // Update moveable + this.updateMoveable(); +}; + +/** + * Toggle visibility from object in viewer + * @param {string} objectType - Object type + * @param {string} objectId - Object ID + * @param {boolean} hide - Hide? + */ +Viewer.prototype.toggleObject = function(objectType, objectId, hide) { + const $viewerObj = this.DOMObject + .find(`[data-type="${objectType}"][data-${objectType}-id="${objectId}"]`); + + // If hide and it's selected, deselect from viewer + if ( + hide && + $viewerObj.hasClass('selected') + ) { + this.selectObject(); + } + + // Remove from DOM + $viewerObj.toggleClass('d-none', hide); +}; + /** * Remove new widget action element */ @@ -3230,9 +3336,14 @@ Viewer.prototype.removeActionEditArea = function() { * @param {object} data - Object data */ Viewer.prototype.saveTemporaryObject = function(objectId, objectType, data) { + // Remove selected from the viewer + this.selectObject(); + + // Select new object lD.selectedObject.id = objectId; lD.selectedObject.type = objectType; + // If it's an element, save also as elementId if (lD.selectedObject.type === 'element') { lD.selectedObject.elementId = objectId; @@ -3242,6 +3353,7 @@ Viewer.prototype.saveTemporaryObject = function(objectId, objectType, data) { $('
', { id: objectId, data: data, + class: 'viewer-temporary-object', }).appendTo(this.DOMObject); }; @@ -3260,10 +3372,10 @@ Viewer.prototype.editGroup = function( refreshEditor: false, reloadPropertiesPanel: false, }); - self.selectElement('#' + elementToSelectOnLoad); + self.selectObject('#' + elementToSelectOnLoad); } else { lD.selectObject(); - self.selectElement(); + self.selectObject(); } // If we're not editing yet, start diff --git a/ui/src/style/layout-editor.scss b/ui/src/style/layout-editor.scss index 6c02a67c85..a661e7cf7a 100644 --- a/ui/src/style/layout-editor.scss +++ b/ui/src/style/layout-editor.scss @@ -1159,7 +1159,7 @@ body.editor-opened { top: 0; left: 0; outline: 2px dashed $xibo-color-primary; - outline-offset: 1px; + outline-offset: -1px; z-index: $viewer-object-group-overlay-z-index; } diff --git a/ui/src/templates/viewer.hbs b/ui/src/templates/viewer.hbs index 30c8a7aaaa..4d29b7f55a 100644 --- a/ui/src/templates/viewer.hbs +++ b/ui/src/templates/viewer.hbs @@ -11,14 +11,14 @@ > {{!-- Render Layout --}}
- {{#with layout.canvas}} -
+ z-index: {{layout.canvas.zIndex}};">
- {{/with}} + {{/if}}
{{#each layout.regions}} {{> viewer-region}}