diff --git a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js index 95040bc71d..14cc4c584f 100644 --- a/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js +++ b/src/LiveDevelopment/BrowserScripts/RemoteFunctions.js @@ -1147,6 +1147,9 @@ function RemoteFunctions(config = {}) { NodeMoreOptionsBox.prototype = { _registerDragDrop: function() { + // disable dragging on all elements and then enable it on the current element + const allElements = document.querySelectorAll('[data-brackets-id]'); + allElements.forEach(el => el.setAttribute("draggable", false)); this.element.setAttribute("draggable", true); this.element.addEventListener("dragstart", (event) => { @@ -3006,6 +3009,8 @@ function RemoteFunctions(config = {}) { // Make the element editable element.setAttribute("contenteditable", "true"); element.focus(); + // to compare with the new text content, if same we don't make any changes in the editor area + const oldContent = element.textContent; // Move cursor to end if no existing selection const selection = window.getSelection(); @@ -3015,19 +3020,46 @@ function RemoteFunctions(config = {}) { dismissUIAndCleanupState(); + // flag to check if escape is pressed, if pressed we prevent onBlur from handling it as keydown already handles + let isEscapePressed = false; + function onBlur() { - finishEditing(element); + // Small delay so that keydown can handle things first + setTimeout(() => { + if (isEscapePressed) { + isEscapePressed = false; + finishEditingCleanup(element); + return; + } + + const newContent = element.textContent; + if (oldContent !== newContent) { + finishEditing(element); + } else { // if same content, we just cleanup things + finishEditingCleanup(element); + } + }, 10); } function onKeyDown(event) { if (event.key === "Escape") { + isEscapePressed = true; // Cancel editing event.preventDefault(); - finishEditing(element, false); // false means that the edit operation was cancelled + const newContent = element.textContent; + if (oldContent !== newContent) { + finishEditing(element, false); // false means that the edit operation was cancelled + } else { // no content change we can avoid sending details to the editor + finishEditingCleanup(element); + } } else if (event.key === "Enter" && !event.shiftKey) { + isEscapePressed = false; // Finish editing on Enter (unless Shift is held) event.preventDefault(); finishEditing(element); + } else if ((event.key === " " || event.key === "Spacebar") && element.tagName.toLowerCase() === 'button') { + event.preventDefault(); + document.execCommand("insertText", false, " "); } } @@ -3041,9 +3073,7 @@ function RemoteFunctions(config = {}) { }; } - // Function to finish editing and apply changes - // isEditSuccessful: this is a boolean value, defaults to true. false only when the edit operation is cancelled - function finishEditing(element, isEditSuccessful = true) { + function finishEditingCleanup(element) { if (!isElementEditable(element) || !element.hasAttribute("contenteditable")) { return; } @@ -3058,6 +3088,12 @@ function RemoteFunctions(config = {}) { element.removeEventListener("keydown", element._editListeners.keydown); delete element._editListeners; } + } + + // Function to finish editing and apply changes + // isEditSuccessful: this is a boolean value, defaults to true. false only when the edit operation is cancelled + function finishEditing(element, isEditSuccessful = true) { + finishEditingCleanup(element); const tagId = element.getAttribute("data-brackets-id"); window._Brackets_MessageBroker.send({ diff --git a/src/LiveDevelopment/main.js b/src/LiveDevelopment/main.js index 58e897b3e4..451427b2da 100644 --- a/src/LiveDevelopment/main.js +++ b/src/LiveDevelopment/main.js @@ -273,122 +273,6 @@ define(function main(require, exports, module) { return false; } - let $livePreviewPanel = null; // stores the live preview panel, need this as overlay is appended inside this - let $overlayContainer = null; // the overlay container - let shouldShowSyncErrorOverlay = true; // once user closes the overlay we don't show them again - let shouldShowConnectingOverlay = true; - let connectingOverlayTimer = null; // this is needed as we show the connecting overlay after 3s - let connectingOverlayTimeDuration = 3000; - - /** - * this function is responsible to check whether to show the overlay or not and how it should be shown - * because if user has closed the overlay manually, we don't show it again - * secondly, for connecting overlay we show that after a 3s timer, but sync error overlay is shown immediately - * @param {String} textMessage - the text that is written inside the overlay - * @param {Number} status - 1 for connect, 4 for sync error but we match it using MultiBrowserLiveDev - */ - function _handleOverlay(textMessage, status) { - if (!$livePreviewPanel) { - $livePreviewPanel = $("#panel-live-preview"); - } - - // remove any existing overlay & timer - _hideOverlay(); - - // to not show the overlays if user has already closed it before - if(status === MultiBrowserLiveDev.STATUS_CONNECTING && !shouldShowConnectingOverlay) { return; } - if(status === MultiBrowserLiveDev.STATUS_SYNC_ERROR && !shouldShowSyncErrorOverlay) { return; } - - // for connecting status, we delay showing the overlay by 3 seconds - if(status === MultiBrowserLiveDev.STATUS_CONNECTING) { - connectingOverlayTimer = setTimeout(() => { - _createAndShowOverlay(textMessage, status); - connectingOverlayTimer = null; - }, connectingOverlayTimeDuration); - return; - } - - // for sync error status, show immediately - _createAndShowOverlay(textMessage, status); - } - - /** - * this function is responsible to create & show the overlay. - * so overlay is shown when the live preview is connecting or live preview stopped because of some syntax error - * @param {String} textMessage - the text that is written inside the overlay - * @param {Number} status - 1 for connect, 4 for sync error but we match it using MultiBrowserLiveDev - */ - function _createAndShowOverlay(textMessage, status) { - if (!$livePreviewPanel) { - $livePreviewPanel = $("#panel-live-preview"); - } - - // create the overlay element - // styled inside the 'src/extensionsIntegrated/Phoenix-live-preview/live-preview.css' - $overlayContainer = $("
").addClass("live-preview-status-overlay"); // the wrapper for overlay element - const $message = $("
").addClass("live-preview-overlay-message").text(textMessage); - - // the close button at the right end of the overlay - const $close = $("
").addClass("live-preview-overlay-close") - .attr("title", Strings.LIVE_PREVIEW_HIDE_OVERLAY) - .on('click', () => { - if(status === MultiBrowserLiveDev.STATUS_CONNECTING) { - shouldShowConnectingOverlay = false; - } else if(status === MultiBrowserLiveDev.STATUS_SYNC_ERROR) { - shouldShowSyncErrorOverlay = false; - } - _hideOverlay(); - }); - const $closeIcon = $("").addClass("fas fa-times"); - - $close.append($closeIcon); - $overlayContainer.append($message); - $overlayContainer.append($close); - $livePreviewPanel.append($overlayContainer); - } - - /** - * responsible to hide the overlay - */ - function _hideOverlay() { - _clearConnectingOverlayTimer(); - if ($overlayContainer) { - $overlayContainer.remove(); - $overlayContainer = null; - } - } - - /** - * This is a helper function that just checks that if connectingOverlayTimer exists, we clear it - */ - function _clearConnectingOverlayTimer() { - if (connectingOverlayTimer) { - clearTimeout(connectingOverlayTimer); - connectingOverlayTimer = null; - } - } - - /** - * this function adds/remove the full-width class from the overlay container - * styled inside 'src/extensionsIntegrated/Phoenix-live-preview/live-preview.css' - * - * we need this because - * normally when live preview has a good width (more than 305px) then a 3px divider is shown at the left end - * so in that case we give the overlay a width of (100% - 3px), - * but when the live preview width is reduced - * then that divider line gets cut off, so in that case we make the width 100% for this overlay - * - * without this handling, a white gap appears on the left side, which is distracting - */ - function _setOverlayWidth() { - if(!$overlayContainer || !$livePreviewPanel.length) { return; } - if($livePreviewPanel.width() <= 305) { - $overlayContainer.addClass("full-width"); - } else { - $overlayContainer.removeClass("full-width"); - } - } - /** Initialize LiveDevelopment */ AppInit.appReady(function () { params.parse(); @@ -443,19 +327,6 @@ define(function main(require, exports, module) { exports.trigger(exports.EVENT_LIVE_PREVIEW_RELOAD, clientDetails); }); - MultiBrowserLiveDev.on(MultiBrowserLiveDev.EVENT_STATUS_CHANGE, function(event, status) { - if (status === MultiBrowserLiveDev.STATUS_CONNECTING) { - _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, status); - } else if (status === MultiBrowserLiveDev.STATUS_SYNC_ERROR) { - _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR, status); - } else { - _hideOverlay(); - } - }); - // to understand why we need this, pls read the _setOverlayWidth function - new ResizeObserver(_setOverlayWidth).observe($("#main-plugin-panel")[0]); - EditorManager.on("activeEditorChange", _hideOverlay); - // allow live preview to handle escape key event // Escape is mainly to hide boxes if they are visible WorkspaceManager.addEscapeKeyEventHandler("livePreview", _handleLivePreviewEscapeKey); diff --git a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css index 7e4d7cac49..4ff2a6b5f3 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css +++ b/src/extensionsIntegrated/Phoenix-live-preview/live-preview.css @@ -171,17 +171,22 @@ } .live-preview-overlay-message { - width: 100%; - color: #fff; + color: #ededed; background-color: #666; - padding: 0.2em; + padding: 0.35em; vertical-align: top; text-align: center; } .live-preview-overlay-close { position: absolute; - top: 4px; - right: 10px; + top: 7px; + right: 12px; font-size: 12px; + cursor: pointer; + color: #ededed; +} + +.live-preview-overlay-close:hover { + color: #fff; } diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 8595d4c937..4942e15225 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -59,6 +59,7 @@ define(function (require, exports, module) { Metrics = require("utils/Metrics"), LiveDevelopment = require("LiveDevelopment/main"), LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"), + MultiBrowserLiveDev = require("LiveDevelopment/LiveDevMultiBrowser"), NativeApp = require("utils/NativeApp"), StringUtils = require("utils/StringUtils"), FileSystem = require("filesystem/FileSystem"), @@ -89,6 +90,9 @@ define(function (require, exports, module) { // live preview mode pref const PREFERENCE_LIVE_PREVIEW_MODE = "livePreviewMode"; + // holds the dropdown instance + let $dropdown = null; + /** * Get the appropriate default mode based on whether edit features are active * @returns {string} "highlight" if edit features inactive, "edit" if active @@ -154,6 +158,13 @@ define(function (require, exports, module) { // so that when user unclicks the button we can revert back to the mode that was originally selected let modeThatWasSelected = null; + // live Preview overlay variables (overlays are shown when live preview is connecting or there's a syntax error) + let $overlayContainer = null; // the overlay container + let shouldShowSyncErrorOverlay = true; // once user closes the overlay we don't show them again + let shouldShowConnectingOverlay = true; + let connectingOverlayTimer = null; // this is needed as we show the connecting overlay after 3s + let connectingOverlayTimeDuration = 3000; + StaticServer.on(EVENT_EMBEDDED_IFRAME_WHO_AM_I, function () { if($iframe && $iframe[0]) { const iframeDom = $iframe[0]; @@ -169,6 +180,111 @@ define(function (require, exports, module) { editor.focus(); }); + /** + * this function is responsible to check whether to show the overlay or not and how it should be shown + * because if user has closed the overlay manually, we don't show it again + * secondly, for connecting overlay we show that after a 3s timer, but sync error overlay is shown immediately + * @param {String} textMessage - the text that is written inside the overlay + * @param {Number} status - 1 for connect, 4 for sync error but we match it using MultiBrowserLiveDev + */ + function _handleOverlay(textMessage, status) { + if (!$panel) { return; } + + // remove any existing overlay & timer + _hideOverlay(); + + // to not show the overlays if user has already closed it before + if(status === MultiBrowserLiveDev.STATUS_CONNECTING && !shouldShowConnectingOverlay) { return; } + if(status === MultiBrowserLiveDev.STATUS_SYNC_ERROR && !shouldShowSyncErrorOverlay) { return; } + + // for connecting status, we delay showing the overlay by 3 seconds + if(status === MultiBrowserLiveDev.STATUS_CONNECTING) { + connectingOverlayTimer = setTimeout(() => { + _createAndShowOverlay(textMessage, status); + connectingOverlayTimer = null; + }, connectingOverlayTimeDuration); + return; + } + + // for sync error status, show immediately + _createAndShowOverlay(textMessage, status); + } + + /** + * this function is responsible to create & show the overlay. + * so overlay is shown when the live preview is connecting or live preview stopped because of some syntax error + * @param {String} textMessage - the text that is written inside the overlay + * @param {Number} status - 1 for connect, 4 for sync error but we match it using MultiBrowserLiveDev + */ + function _createAndShowOverlay(textMessage, status) { + if (!$panel) { return; } + + // create the overlay element + // styled inside the 'live-preview.css' + $overlayContainer = $("
").addClass("live-preview-status-overlay"); // the wrapper for overlay element + const $message = $("
").addClass("live-preview-overlay-message").text(textMessage); + + // the close button at the right end of the overlay + const $close = $("
").addClass("live-preview-overlay-close") + .attr("title", Strings.LIVE_PREVIEW_HIDE_OVERLAY) + .on('click', () => { + if(status === MultiBrowserLiveDev.STATUS_CONNECTING) { + shouldShowConnectingOverlay = false; + } else if(status === MultiBrowserLiveDev.STATUS_SYNC_ERROR) { + shouldShowSyncErrorOverlay = false; + } + _hideOverlay(); + }); + const $closeIcon = $("").addClass("fas fa-times"); + + $close.append($closeIcon); + $overlayContainer.append($message); + $overlayContainer.append($close); + $panel.append($overlayContainer); + } + + /** + * responsible to hide the overlay + */ + function _hideOverlay() { + _clearConnectingOverlayTimer(); + if ($overlayContainer) { + $overlayContainer.remove(); + $overlayContainer = null; + } + } + + /** + * This is a helper function that just checks that if connectingOverlayTimer exists, we clear it + */ + function _clearConnectingOverlayTimer() { + if (connectingOverlayTimer) { + clearTimeout(connectingOverlayTimer); + connectingOverlayTimer = null; + } + } + + /** + * this function adds/remove the full-width class from the overlay container + * styled inside 'live-preview.css' + * + * we need this because + * normally when live preview has a good width (more than 305px) then a 3px divider is shown at the left end + * so in that case we give the overlay a width of (100% - 3px), + * but when the live preview width is reduced + * then that divider line gets cut off, so in that case we make the width 100% for this overlay + * + * without this handling, a white gap appears on the left side, which is distracting + */ + function _setOverlayWidth() { + if(!$overlayContainer || !$panel || !$panel.length) { return; } + if($panel.width() <= 305) { + $overlayContainer.addClass("full-width"); + } else { + $overlayContainer.removeClass("full-width"); + } + } + function _showProFeatureDialog() { const dialog = Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_INFO, @@ -309,7 +425,7 @@ define(function (require, exports, module) { // this is to take care of invalid values in the pref file const currentMode = ["preview", "highlight", "edit"].includes(rawMode) ? rawMode : _getDefaultMode(); - const dropdown = new DropdownButton.DropdownButton("", items, function(item, index) { + $dropdown = new DropdownButton.DropdownButton("", items, function(item, index) { if (item === Strings.LIVE_PREVIEW_MODE_PREVIEW) { // using empty spaces to keep content aligned return currentMode === "preview" ? `✓ ${item}` : `${'\u00A0'.repeat(4)}${item}`; @@ -333,10 +449,10 @@ define(function (require, exports, module) { }); // Append to document body for absolute positioning - $("body").append(dropdown.$button); + $("body").append($dropdown.$button); // Position the dropdown at the mouse coordinates - dropdown.$button.css({ + $dropdown.$button.css({ position: "absolute", left: event.pageX + "px", top: event.pageY + "px", @@ -344,14 +460,14 @@ define(function (require, exports, module) { }); // Add a custom class to override the max-height - dropdown.dropdownExtraClasses = "mode-context-menu"; + $dropdown.dropdownExtraClasses = "mode-context-menu"; - dropdown.showDropdown(); + $dropdown.showDropdown(); $(".mode-context-menu").css("max-height", "300px"); // handle the option selection - dropdown.on("select", function (e, item, index) { + $dropdown.on("select", function (e, item, index) { // here we just set the preference // as the preferences listener will automatically handle the required changes if (index === 0) { @@ -384,11 +500,28 @@ define(function (require, exports, module) { }); // Remove the button after the dropdown is hidden - dropdown.$button.css({ + $dropdown.$button.css({ display: "none" }); } + /** + * to close the overflow button's dropdown + */ + function _closeDropdown() { + if ($dropdown) { + if ($dropdown.$button) { + $dropdown.$button.remove(); + } + $dropdown = null; + } + } + + function _handleLPModeBtnClick(e) { + e.stopPropagation(); + $dropdown ? _closeDropdown() : _showModeSelectionDropdown(e); + } + function _getTrustProjectPage() { const trustProjectMessage = StringUtils.format(Strings.TRUST_PROJECT, path.basename(ProjectManager.getProjectRoot().fullPath)); @@ -692,7 +825,7 @@ define(function (require, exports, module) { _popoutLivePreview("firefox"); }); - $modeBtn.on("click", _showModeSelectionDropdown); + $modeBtn.on("click", _handleLPModeBtnClick); $previewBtn.on("click", _handlePreviewBtnClick); _showOpenBrowserIcons(); @@ -723,6 +856,10 @@ define(function (require, exports, module) { _loadPreview(true, true); Metrics.countEvent(Metrics.EVENT_TYPE.LIVE_PREVIEW, "reloadBtn", "click"); }); + + // Set up ResizeObserver for overlay width adjustments + // to understand why we're doing this read _setOverlayWidth function + new ResizeObserver(_setOverlayWidth).observe($panel[0]); } async function _loadPreview(force, isReload) { @@ -900,12 +1037,27 @@ define(function (require, exports, module) { } } - function _activeDocChanged() { + function _activeDocChanged(event, focusedEditor, lostEditor) { if(!LivePreviewSettings.isUsingCustomServer() && !LiveDevelopment.isActive() && (panel.isVisible() || StaticServer.hasActiveLivePreviews())) { // we do this only once after project switch if live preview for a doc is not active. LiveDevelopment.openLivePreview(); } + + // we hide the overlay when there's no editor or its a non-previewable file + if (!focusedEditor || !focusedEditor.document) { + _hideOverlay(); + return; + } + + const filePath = focusedEditor.document.file.fullPath; + const isPreviewable = utils.isPreviewableFile(filePath) || utils.isServerRenderedFile(filePath); + const customServeURL = LivePreviewSettings.isUsingCustomServer() && + LivePreviewSettings.getCustomServerConfig(filePath); + + if (!isPreviewable && !customServeURL) { + _hideOverlay(); + } } /** @@ -928,6 +1080,13 @@ define(function (require, exports, module) { console.error("Live preview URLs differ between phoenix live preview extension and core live preview", currentPreviewDetails, previewDetails); } + + const status = MultiBrowserLiveDev.status; + if (status === MultiBrowserLiveDev.STATUS_CONNECTING) { + _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, status); + } else if (status === MultiBrowserLiveDev.STATUS_SYNC_ERROR) { + _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR, status); + } } async function _currentFileChanged(_event, changedFile) { @@ -1046,6 +1205,18 @@ define(function (require, exports, module) { }); } + function _registerHandlers() { + // when clicked anywhere on the page we want to close the dropdown + $("html").on("click", function (e) { + if ($(e.target).closest("#livePreviewModeBtn").length) { return; } + _closeDropdown(); + }); + + $(document).on("click", "#livePreviewModeBtn", function (e) { + _handleLPModeBtnClick(e); + }); + } + AppInit.appReady(function () { if(Phoenix.isSpecRunnerWindow){ return; @@ -1088,6 +1259,7 @@ define(function (require, exports, module) { Menus.AFTER, Commands.FILE_LIVE_FILE_PREVIEW); fileMenu.addMenuDivider(Menus.BEFORE, Commands.FILE_LIVE_FILE_PREVIEW); + _registerHandlers(); // init live preview mode from saved preferences _initializeMode(); // listen for pref changes @@ -1138,6 +1310,16 @@ define(function (require, exports, module) { _loadPreview(true); }); + MultiBrowserLiveDev.on(MultiBrowserLiveDev.EVENT_STATUS_CHANGE, function(event, status) { + if (status === MultiBrowserLiveDev.STATUS_CONNECTING) { + _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, status); + } else if (status === MultiBrowserLiveDev.STATUS_SYNC_ERROR) { + _handleOverlay(Strings.LIVE_DEV_STATUS_TIP_SYNC_ERROR, status); + } else { + _hideOverlay(); + } + }); + function refreshPreview() { StaticServer.getPreviewDetails().then((previewDetails)=>{ _openReadmeMDIfFirstTime();