diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index a7d02cc2ab..aa01df35dc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -4,7 +4,7 @@ function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper, - editorState, $http, eventsService, overlayService, $location) { + editorState, $http, eventsService, overlayService, $location, localStorageService) { var evts = []; var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode; @@ -189,6 +189,13 @@ $scope.page.saveButtonState = "success"; $scope.page.buttonGroupState = "success"; })); + + evts.push(eventsService.on("content.saved", function(){ + // Clear out localstorage keys that start with tinymce__ + // When we save/perist a content node + // NOTE: clearAll supports a RegEx pattern of items to remove + localStorageService.clearAll(/^tinymce__/); + })); } /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 60bd6f80b3..03d70b4da5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -7,7 +7,7 @@ * A service containing all logic for all of the Umbraco TinyMCE plugins */ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, - $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService) { + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce @@ -203,7 +203,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } // Put temp location into localstorage (used to update the img with data-tmpimg later on) - localStorage.setItem(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); + localStorageService.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); // We set the img src url to be the same as we started // The Blob URI is stored in TinyMce's cache @@ -234,23 +234,43 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // Get img src var imgSrc = img.getAttribute("src"); - var tmpLocation = localStorage.getItem(`tinymce__${imgSrc}`); + var tmpLocation = localStorageService.get(`tinymce__${imgSrc}`) // Select the img & add new attr which we can search for // When its being persisted in RTE property editor // To create a media item & delete this tmp one etc tinymce.activeEditor.$(img).attr({ "data-tmpimg": tmpLocation }); - + // Resize the image to the max size configured // NOTE: no imagesrc passed into func as the src is blob://... // We will append ImageResizing Querystrings on perist to DB with node save sizeImageInEditor(editor, img); - - // We need to remove the image from the cache, otherwise we can't handle if we upload the exactly - // same image twice - tinymce.activeEditor.editorUpload.blobCache.removeByUri(imgSrc); }); + + }); + + // Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute + // This is most likely seen as a duplicate image that has already been uploaded + // editor.uploadImages() does not give us any indiciation that the image been uploaded already + var blobImageWithNoTmpImgAttribute = editor.dom.select("img[src^='blob:']:not([data-tmpimg])"); + + //For each of these selected items + blobImageWithNoTmpImgAttribute.forEach(imageElement => { + var blobSrcUri = editor.dom.getAttrib(imageElement, "src"); + + // Find the same image uploaded (Should be in LocalStorage) + // May already exist in the editor as duplicate image + // OR added to the RTE, deleted & re-added again + // So lets fetch the tempurl out of localstorage for that blob URI item + var tmpLocation = localStorageService.get(`tinymce__${blobSrcUri}`) + + if(tmpLocation){ + sizeImageInEditor(editor, imageElement); + editor.dom.setAttrib(imageElement, "data-tmpimg", tmpLocation); + } + }); + } }); } diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js index f813ec24a8..715861de0b 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js @@ -13,6 +13,8 @@ describe('RTE controller tests', function () { } } + beforeEach(module('LocalStorageModule')); + beforeEach(module('umbraco', function ($provide) { $provide.value('tinyMceAssets', []); })); diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index bc34dbdda7..58d3ed341e 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -1,5 +1,6 @@ using HtmlAgilityPack; using System; +using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Umbraco.Core; @@ -201,7 +202,11 @@ namespace Umbraco.Web.Templates var tmpImages = htmlDoc.DocumentNode.SelectNodes($"//img[@{TemporaryImageDataAttribute}]"); if (tmpImages == null || tmpImages.Count == 0) return html; - + + // An array to contain a list of URLs that + // we have already processed to avoid dupes + var uploadedImages = new Dictionary(); + foreach (var img in tmpImages) { // The data attribute contains the path to the tmp img to persist as a media item @@ -209,36 +214,46 @@ namespace Umbraco.Web.Templates if (string.IsNullOrEmpty(tmpImgPath)) continue; - + var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath); var fileName = Path.GetFileName(absoluteTempImagePath); var safeFileName = fileName.ToSafeFileName(); var mediaItemName = safeFileName.ToFriendlyName(); IMedia mediaFile; + GuidUdi udi; - if(mediaParentFolder == Guid.Empty) - mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); - else - mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - - var fileInfo = new FileInfo(absoluteTempImagePath); - - var fileStream = fileInfo.OpenReadWithRetry(); - if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fileStream) + if (uploadedImages.ContainsKey(tmpImgPath) == false) { - mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); - } + if (mediaParentFolder == Guid.Empty) + mediaFile = mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId); + else + mediaFile = mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId); - mediaService.Save(mediaFile, userId); + var fileInfo = new FileInfo(absoluteTempImagePath); + + var fileStream = fileInfo.OpenReadWithRetry(); + if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fileStream) + { + mediaFile.SetValue(contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream); + } + + mediaService.Save(mediaFile, userId); + + udi = mediaFile.GetUdi(); + } + else + { + // Already been uploaded & we have it's UDI + udi = uploadedImages[tmpImgPath]; + } // Add the UDI to the img element as new data attribute - var udi = mediaFile.GetUdi(); img.SetAttributeValue("data-udi", udi.ToString()); // Get the new persisted image url - var mediaTyped = Current.UmbracoHelper.Media(mediaFile.Id); + var mediaTyped = Current.UmbracoHelper.Media(udi.Guid); var location = mediaTyped.Url; // Find the width & height attributes as we need to set the imageprocessor QueryString @@ -255,19 +270,24 @@ namespace Umbraco.Web.Templates // Remove the data attribute (so we do not re-process this) img.Attributes.Remove(TemporaryImageDataAttribute); - // Delete folder & image now its saved in media - // The folder should contain one image - as a unique guid folder created - // for each image uploaded from TinyMceController - var folderName = Path.GetDirectoryName(absoluteTempImagePath); - try - { - Directory.Delete(folderName, true); - } - catch (Exception ex) + // Add to the dictionary to avoid dupes + if(uploadedImages.ContainsKey(tmpImgPath) == false) { - logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + uploadedImages.Add(tmpImgPath, udi); + + // Delete folder & image now its saved in media + // The folder should contain one image - as a unique guid folder created + // for each image uploaded from TinyMceController + var folderName = Path.GetDirectoryName(absoluteTempImagePath); + try + { + Directory.Delete(folderName, true); + } + catch (Exception ex) + { + logger.Error(typeof(TemplateUtilities), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath); + } } - } return htmlDoc.DocumentNode.OuterHtml;