From dc072293f9233bcab690b69d80ec02a919748856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 8 Oct 2020 16:36:25 +0200 Subject: [PATCH] New dialogs for asking about Preview mode --- src/Umbraco.Core/Constants-Web.cs | 4 + .../src/preview/preview.controller.js | 205 ++++++++++++++++-- .../src/websitepreview/websitepreview.js | 49 +++-- .../Umbraco/Views/Preview/Index.cshtml | 3 + .../config/umbracoSettings.config | 2 +- src/Umbraco.Web/Editors/PreviewController.cs | 17 +- 6 files changed, 240 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 64216ba571..0e956cfd6a 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -17,6 +17,10 @@ /// The preview cookie name /// public const string PreviewCookieName = "UMB_PREVIEW"; + /// + /// Client-side cookie that determins wether the user has accepted to be in Preview Mode when visiting the website. + /// + public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; public const string InstallerCookieName = "umb_installId"; } diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index 5ff8dd3633..412d0c9cc5 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -5,13 +5,13 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.services']) - .controller("previewController", function ($scope, $window, $location) { + .controller("previewController", function ($scope, $window, $location, $http) { $scope.currentCulture = { iso: '', title: '...', icon: 'icon-loading' } var cultures = []; $scope.tabbingActive = false; - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { @@ -89,7 +89,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi if (isInit === "true") { //do not continue, this is the first load of this new window, if this is passed in it means it's been //initialized by the content editor and then the content editor will actually re-load this window without - //this flag. This is a required trick to get around chrome popup mgr. + //this flag. This is a required trick to get around chrome popup mgr. return; } @@ -100,7 +100,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.valueAreLoaded = false; $scope.devices = [ - { name: "fullsize", css: "fullsize", icon: "icon-application-window-alt", title: "Browser" }, + { name: "fullsize", css: "fullsize", icon: "icon-application-window-alt", title: "Fit browser" }, { name: "desktop", css: "desktop shadow", icon: "icon-display", title: "Desktop" }, { name: "laptop - 1366px", css: "laptop shadow", icon: "icon-laptop", title: "Laptop" }, { name: "iPad portrait - 768px", css: "iPad-portrait shadow", icon: "icon-ipad", title: "Tablet portrait" }, @@ -136,17 +136,24 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.windowClickHandler = function () { closeOthers(); } + function windowBlurHandler() { closeOthers(); $scope.$digest(); } + window.addEventListener("blur", windowBlurHandler); - var win = $($window); - - win.on("blur", windowBlurHandler); + function windowVisibilityHandler(e) { + // When tab is visible again: + if(document.hidden === false) { + checkPreviewState(); + } + } + document.addEventListener("visibilitychange", windowVisibilityHandler); $scope.$on("$destroy", function () { - win.off("blur", handleBlwindowBlurHandlerur); + window.removeEventListener("blur", windowBlurHandler); + window.removeEventListener("visibilitychange", windowVisibilityHandler); }); @@ -162,6 +169,167 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.pageUrl = "frame?" + query; } } + function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(";"); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return null; + } + function setCookie(cname, cvalue, exminutes) { + var d = new Date(); + d.setTime(d.getTime() + (exminutes * 60 * 1000)); + document.cookie = cname + "=" + cvalue + ";expires="+d.toUTCString() + ";path=/"; + } + var hasPreviewDialog = false; + function checkPreviewState() { + if (getCookie("UMB_PREVIEW") === null) { + + if(hasPreviewDialog === true) return; + hasPreviewDialog = true; + + // Ask to re-enter preview mode? + + // This modal is also used in websitepreview.js + var modelStyles = ` + + /* Webfont: LatoLatin-Bold */ + @font-face { + font-family: 'Lato'; + src: url('https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap'); + font-style: normal; + font-weight: 700; + font-display: swap; + text-rendering: optimizeLegibility; + } + + .umbraco-preview-dialog { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + z-index: 99999999; + top:0; + bottom:0; + left:0; + right:0; + overflow: auto; + background-color: rgba(0,0,0,0.6); + } + + .umbraco-preview-dialog__modal { + background-color: #fff; + border-radius: 6px; + box-shadow: 0 3px 7px rgba(0,0,0,0.3); + margin: auto; + padding: 30px 40px; + width: 100%; + max-width: 540px; + font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; + font-size: 15px; + } + + .umbraco-preview-dialog__headline { + font-weight: 700; + font-size: 22px; + color: #1b264f; + margin-top:10px; + margin-bottom:20px; + } + .umbraco-preview-dialog__question { + margin-bottom:30px; + } + .umbraco-preview-dialog__modal > button { + display: inline-block; + cursor: pointer; + padding: 8px 18px; + text-align: center; + vertical-align: middle; + border-radius: 3px; + border:none; + font-family: inherit; + font-weight: 700; + font-size: 15px; + float:right; + margin-left:10px; + + color: #1b264f; + background-color: #f6f1ef; + } + .umbraco-preview-dialog__modal > button:hover { + color: #2152a3; + background-color: #f6f1ef; + } + .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue { + color: #fff; + background-color: #2bc37c; + } + .umbraco-preview-dialog__modal > button.umbraco-preview-dialog__continue:hover { + background-color: #39d38b; + } + `; + + var bodyEl = document.getElementsByTagName("BODY")[0]; + + var fragment = document.createElement("div"); + var shadowRoot = fragment.attachShadow({ mode: 'open' }); + + var style = document.createElement("style"); + style.innerHTML = modelStyles; + shadowRoot.appendChild(style); + + var con = document.createElement("div"); + con.className = "umbraco-preview-dialog"; + shadowRoot.appendChild(con); + + var modal = document.createElement("div"); + modal.className = "umbraco-preview-dialog__modal"; + modal.innerHTML = `
Preview content?
+
You have exited preview mode, do you want to continue previewing this content?
`; + con.appendChild(modal); + + var continueButton = document.createElement("button"); + continueButton.type = "button"; + continueButton.className = "umbraco-preview-dialog__continue"; + continueButton.innerHTML = "Preview content"; + continueButton.addEventListener("click", () => { + bodyEl.removeChild(fragment); + reenterPreviewMode(); + hasPreviewDialog = false; + }); + modal.appendChild(continueButton); + + bodyEl.appendChild(fragment); + continueButton.focus(); + + } + } + function reenterPreviewMode() { + //Re-enter Preview Mode + $http({ + method: 'POST', + url: '../preview/enterPreview', + params: { + id: $scope.pageId + } + }) + } + function getPageURL() { + var culture = $location.search().culture || getParameterByName("culture"); + var relativeUrl = "/" + $scope.pageId; + if (culture) { + relativeUrl += '?culture=' + culture; + } + return relativeUrl; + } + /*****************************************************************************/ /* Preview devices */ /*****************************************************************************/ @@ -171,18 +339,21 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.previewDevice = device; }; + /*****************************************************************************/ + /* Open website in preview mode */ + /*****************************************************************************/ + + $scope.openInBrowser = function () { + setCookie("UMB-WEBSITE-PREVIEW-ACCEPT", "true", 5); + window.open(getPageURL(), "_blank"); + }; + /*****************************************************************************/ /* Exit Preview */ /*****************************************************************************/ $scope.exitPreview = function () { - - var culture = $location.search().culture || getParameterByName("culture"); - var relativeUrl = "/" + $scope.pageId; - if (culture) { - relativeUrl += '?culture=' + culture; - } - window.top.location.href = "../preview/end?redir=" + encodeURIComponent(relativeUrl); + window.top.location.href = "../preview/end?redir=" + encodeURIComponent(getPageURL()); }; $scope.onFrameLoaded = function (iframe) { @@ -239,8 +410,8 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi var vm = this; vm.$postLink = function () { - var resultFrame = $element.find("#resultFrame"); - resultFrame.on("load", iframeReady); + var resultFrame = $element.find("#resultFrame").get(0); + resultFrame.addEventListener("load", iframeReady); }; function iframeReady() { diff --git a/src/Umbraco.Web.UI.Client/src/websitepreview/websitepreview.js b/src/Umbraco.Web.UI.Client/src/websitepreview/websitepreview.js index be5c989f68..cb76b47bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/websitepreview/websitepreview.js +++ b/src/Umbraco.Web.UI.Client/src/websitepreview/websitepreview.js @@ -30,20 +30,31 @@ return c.substring(name.length, c.length); } } - return ""; + return null; } function exitPreviewMode() { window.top.location.href = scriptElement.getAttribute("data-umbraco-path")+"/preview/end?redir=" + encodeURIComponent(window.location.pathname+window.location.search); } - function continuePreviewMode() { - setCookie("UMB-WEBSITE-PREVIEW-ACCEPT", "true", 5); + function continuePreviewMode(minutsToExpire) { + setCookie("UMB-WEBSITE-PREVIEW-ACCEPT", "true", minutsToExpire || 2); } var user = getCookie("UMB-WEBSITE-PREVIEW-ACCEPT"); if (user != "true") { + // This modal is also used in preview.js var modelStyles = ` + /* Webfont: LatoLatin-Bold */ + @font-face { + font-family: 'Lato'; + src: url('https://fonts.googleapis.com/css2?family=Lato:wght@700&display=swap'); + font-style: normal; + font-weight: 700; + font-display: swap; + text-rendering: optimizeLegibility; + } + .umbraco-preview-dialog { position: fixed; display: flex; @@ -68,6 +79,7 @@ max-width: 540px; font-family: Lato,Helvetica Neue,Helvetica,Arial,sans-serif; font-size: 15px; + line-height: 1.5; } .umbraco-preview-dialog__headline { @@ -75,19 +87,22 @@ font-size: 22px; color: #1b264f; margin-top:10px; - margin-bottom:10px; + margin-bottom:20px; } .umbraco-preview-dialog__question { margin-bottom:30px; } .umbraco-preview-dialog__modal > button { display: inline-block; + cursor: pointer; padding: 8px 18px; text-align: center; vertical-align: middle; border-radius: 3px; border:none; + font-family: inherit; font-weight: 700; + font-size: 15px; float:right; margin-left:10px; @@ -109,37 +124,37 @@ var bodyEl = document.getElementsByTagName("BODY")[0]; - var fragment = document.createDocumentFragment(); + var fragment = document.createElement("div"); + var shadowRoot = fragment.attachShadow({ mode: 'open' }); var style = document.createElement("style"); style.innerHTML = modelStyles; - fragment.appendChild(style); + shadowRoot.appendChild(style); var con = document.createElement("div"); con.className = "umbraco-preview-dialog"; - fragment.appendChild(con); + shadowRoot.appendChild(con); var modal = document.createElement("div"); modal.className = "umbraco-preview-dialog__modal"; - modal.innerHTML = `
Preview mode
-
Do you want to continue viewing latest saved version?
`; + modal.innerHTML = `
View published version?
+
You are in Preview Mode, do you want exit in order to view the published version of your website?
`; con.appendChild(modal); var continueButton = document.createElement("button"); continueButton.type = "button"; continueButton.className = "umbraco-preview-dialog__continue"; - continueButton.innerHTML = "Continue"; - continueButton.addEventListener("click", function() { - bodyEl.removeChild(style); - bodyEl.removeChild(con); - continuePreviewMode(); - }); + continueButton.innerHTML = "View published version"; + continueButton.addEventListener("click", exitPreviewMode); modal.appendChild(continueButton); var exitButton = document.createElement("button"); exitButton.type = "button"; - exitButton.innerHTML = "Exit preview mode"; - exitButton.addEventListener("click", exitPreviewMode); + exitButton.innerHTML = "Stay in preview mode"; + exitButton.addEventListener("click", function() { + bodyEl.removeChild(fragment); + continuePreviewMode(5); + }); modal.appendChild(exitButton); bodyEl.appendChild(fragment); diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml index 4b7bcaee87..8b7d871ba4 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Preview/Index.cshtml @@ -65,6 +65,9 @@ } + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 2287464d07..086210e5a5 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -64,7 +64,7 @@ pointer-events:none; left: 50%; transform: translate(-50%, 40px); - animation: umbraco-preview-badge--effect 10s 100ms ease both; + animation: umbraco-preview-badge--effect 10s 1.2s ease both; border-radius: 3px 3px 0 0; }} @keyframes umbraco-preview-badge--effect {{ diff --git a/src/Umbraco.Web/Editors/PreviewController.cs b/src/Umbraco.Web/Editors/PreviewController.cs index f148655acd..c57968af06 100644 --- a/src/Umbraco.Web/Editors/PreviewController.cs +++ b/src/Umbraco.Web/Editors/PreviewController.cs @@ -101,11 +101,7 @@ namespace Umbraco.Web.Editors [UmbracoAuthorize] public ActionResult Frame(int id, string culture) { - var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser; - - var previewToken = _publishedSnapshotService.EnterPreview(user, id); - - Response.Cookies.Set(new HttpCookie(Constants.Web.PreviewCookieName, previewToken)); + EnterPreview(id); // use a numeric url because content may not be in cache and so .Url would fail var query = culture.IsNullOrWhiteSpace() ? string.Empty : $"?culture={culture}"; @@ -113,7 +109,16 @@ namespace Umbraco.Web.Editors return null; } + public ActionResult EnterPreview(int id) + { + var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser; + var previewToken = _publishedSnapshotService.EnterPreview(user, id); + + Response.Cookies.Set(new HttpCookie(Constants.Web.PreviewCookieName, previewToken)); + + return null; + } public ActionResult End(string redir = null) { var previewToken = Request.GetPreviewCookieValue(); @@ -121,6 +126,8 @@ namespace Umbraco.Web.Editors service.ExitPreview(previewToken); System.Web.HttpContext.Current.ExpireCookie(Constants.Web.PreviewCookieName); + // Expire Client-side cookie that determins wether the user has accepted to be in Preview Mode when visiting the website. + System.Web.HttpContext.Current.ExpireCookie(Constants.Web.AcceptPreviewCookieName); if (Uri.IsWellFormedUriString(redir, UriKind.Relative) && redir.StartsWith("//") == false