diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c1d7103a1c6e..c8233c8d340f 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -118,6 +118,26 @@ public static class MediaTypes /// public const string Image = "Image"; + /// + /// MediaType alias for a video. + /// + public const string Video = "Video"; + + /// + /// MediaType alias for an audio. + /// + public const string Audio = "Audio"; + + /// + /// MediaType alias for an article. + /// + public const string Article = "Article"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphics = "VectorGraphics"; + /// /// MediaType alias indicating allowing auto-selection. /// diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index 673da8f9a360..f1af0ba99e6c 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -25,6 +25,10 @@ public static class DataTypes public const int DropDownSingle = -39; public const int DropDownMultiple = -42; public const int Upload = -90; + public const int UploadVideo = -100; + public const int UploadAudio = -101; + public const int UploadArticle = -102; + public const int UploadVectorGraphics = -103; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; @@ -42,7 +46,7 @@ public static class ReservedPreValueKeys /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. /// public static class Guids - { + { /// /// Guid for Content Picker as string @@ -88,6 +92,49 @@ public static class Guids public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + /// + /// Guid for Media Picker v3 as string + /// + public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); + + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + + /// + /// Guid for Media Picker v3 multiple + /// + public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); + + + /// + /// Guid for Media Picker v3 single-image as string + /// + public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + + /// + /// Guid for Media Picker v3 single-image + /// + public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); + + + /// + /// Guid for Media Picker v3 multi-image as string + /// + public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); + + /// /// Guid for Related Links as string /// @@ -307,6 +354,46 @@ public static class Guids /// public static readonly Guid UploadGuid = new Guid(Upload); + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; + + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new Guid(UploadVideo); + + /// + /// Guid for UploadAudio as string + /// + public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; + + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new Guid(UploadAudio); + + /// + /// Guid for UploadArticle as string + /// + public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; + + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new Guid(UploadArticle); + + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; + + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new Guid(UploadVectorGraphics); + /// /// Guid for Label as string @@ -367,8 +454,8 @@ public static class Guids /// Guid for Label decimal /// public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); - - + + } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index d5cc37c9a58e..e15c1e162bea 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -59,6 +59,26 @@ public static class Icons /// public const string MediaFile = "icon-document"; + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; + + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; + + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; + + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; + /// /// System media folder icon /// @@ -93,7 +113,7 @@ public static class Icons /// System packages icon /// public const string Packages = "icon-box"; - + /// /// System property editor icon /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 87739469d10b..f69570dc08ad 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -95,12 +95,17 @@ public static class Aliases /// ListView. /// public const string ListView = "Umbraco.ListView"; - + /// /// Media Picker. /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Media Picker v.3. + /// + public const string MediaPicker3 = "Umbraco.MediaPicker3"; + /// /// Multiple Media Picker. /// diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs index d3402e69f894..20ada8c0f4a3 100644 --- a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -8,7 +8,7 @@ public static partial class Constants public static class PropertyTypeGroups { /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for an Image PropertyTypeGroup object. /// public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; @@ -18,7 +18,27 @@ public static class PropertyTypeGroups public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for a Video PropertyTypeGroup object. + /// + public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; + + /// + /// Guid for an Audio PropertyTypeGroup object. + /// + public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; + + /// + /// Guid for an Article PropertyTypeGroup object. + /// + public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; + + /// + /// Guid for a VectorGraphics PropertyTypeGroup object. + /// + public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; + + /// + /// Guid for a Membership PropertyTypeGroup object. /// public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 44de6113486d..264733e5b9a8 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -107,7 +107,11 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); @@ -126,6 +130,10 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = "Vector Graphics (SVG)", NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); @@ -133,9 +141,15 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + } private void CreateLockData() @@ -160,6 +174,10 @@ private void CreateContentTypeData() _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.Video, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.Audio, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.Article, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphics, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } @@ -207,20 +225,44 @@ private void CreatePropertyTypeGroupData() { _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.File) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, ContentTypeNodeId = 1034, Text = "Video", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Video) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, ContentTypeNodeId = 1035, Text = "Audio", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Audio) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, ContentTypeNodeId = 1036, Text = "Article", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Article) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, ContentTypeNodeId = 1037, Text = "Vector Graphics", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics) }); //membership property group _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Membership) }); } private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + + + //membership property types _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); @@ -244,6 +286,10 @@ private void CreateContentChildTypeData() _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1032 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1033 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1034 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1035 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1036 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1037 }); } private void CreateDataTypeData() @@ -307,6 +353,63 @@ void InsertDataTypeDto(int id, string editorAlias, string dbType, string configu _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext", Configuration = "{\"multiPicker\":1}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1051, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1052, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": true}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1053, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1054, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" + }); } private void CreateRelationTypeData() diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index f460edbde70f..913aa4773ef2 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -62,6 +62,10 @@ public static T ConfigurationAs(this IDataType dataType) Constants.DataTypes.Guids.TextstringGuid, Constants.DataTypes.Guids.TextareaGuid, Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.UploadArticleGuid, + Constants.DataTypes.Guids.UploadAudioGuid, + Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Constants.DataTypes.Guids.UploadVideoGuid, Constants.DataTypes.Guids.LabelStringGuid, Constants.DataTypes.Guids.LabelDecimalGuid, Constants.DataTypes.Guids.LabelDateTimeGuid, diff --git a/src/Umbraco.Core/Models/MediaWithCrops.cs b/src/Umbraco.Core/Models/MediaWithCrops.cs new file mode 100644 index 000000000000..ef3205bd9436 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaWithCrops.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Core.Models +{ + /// + /// Model used in Razor Views for rendering + /// + public class MediaWithCrops + { + public IPublishedContent MediaItem { get; set; } + + public ImageCropperValue LocalCrops { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs index e7a14a26e2e7..68036dab4bc3 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs @@ -11,7 +11,7 @@ internal class ContentTypeDto public const string TableName = Constants.DatabaseSchema.Tables.ContentType; [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] + [PrimaryKeyColumn(IdentitySeed = 700)] public int PrimaryKey { get; set; } [Column("nodeId")] diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs index 572201c94a0e..f22e4453f420 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Dtos internal class PropertyTypeDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] + [PrimaryKeyColumn(IdentitySeed = 100)] public int Id { get; set; } [Column("dataTypeId")] diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index 270c8c3b0ba9..694ebfde2780 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; @@ -18,6 +19,8 @@ public class JsonValueConverter : PropertyValueConverterBase { private readonly PropertyEditorCollection _propertyEditors; + string[] ExcludedPropertyEditors = new string[] { Constants.PropertyEditors.Aliases.MediaPicker3 }; + /// /// Initializes a new instance of the class. /// @@ -28,13 +31,16 @@ public JsonValueConverter(PropertyEditorCollection propertyEditors) /// /// It is a converter for any value type that is "JSON" + /// Unless it's in the Excluded Property Editors list + /// The new MediaPicker 3 stores JSON but we want to use its own ValueConvertor /// /// /// public override bool IsConverter(IPublishedPropertyType propertyType) { return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor) - && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json); + && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json) + && ExcludedPropertyEditors.Contains(propertyType.EditorAlias) == false; } public override Type GetPropertyValueType(IPublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2ea5292d7327..0a453ad75f07 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -156,6 +156,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index b0c57b685b83..d2bbf3e86529 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ public void Resolves_Types() public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(40, types.Count()); + Assert.AreEqual(41, types.Count()); } /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index ca8ee29ee3a7..339b3d4931b4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -279,7 +279,7 @@ public void Can_Perform_GetAll_On_DataTypeDefinitionRepository() Assert.That(dataTypeDefinitions, Is.Not.Null); Assert.That(dataTypeDefinitions.Any(), Is.True); Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False); - Assert.That(dataTypeDefinitions.Length, Is.EqualTo(29)); + Assert.That(dataTypeDefinitions.Length, Is.EqualTo(37)); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index bb3286daedad..e048886dbe4f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -50,7 +50,7 @@ public void Can_Move() containerRepository.Save(container2); - var contentType = (IMediaType)MockedContentTypes.CreateVideoMediaType(); + var contentType = (IMediaType)MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container2.Id; repository.Save(contentType); @@ -133,7 +133,7 @@ public void Can_Create_Container_Containing_Media_Types() containerRepository.Save(container); - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -155,7 +155,7 @@ public void Can_Delete_Container_Containing_Media_Types() containerRepository.Save(container); - IMediaType contentType = MockedContentTypes.CreateVideoMediaType(); + IMediaType contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -183,7 +183,7 @@ public void Can_Perform_Add_On_MediaTypeRepository() var repository = CreateRepository(provider); // Act - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); repository.Save(contentType); @@ -210,7 +210,7 @@ public void Can_Perform_Update_On_MediaTypeRepository() { var repository = CreateRepository(provider); - var videoMediaType = MockedContentTypes.CreateVideoMediaType(); + var videoMediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(videoMediaType); @@ -249,7 +249,7 @@ public void Can_Perform_Delete_On_MediaTypeRepository() var repository = CreateRepository(provider); // Act - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -378,7 +378,7 @@ public void Can_Update_MediaType_With_PropertyType_Removed() { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -406,7 +406,7 @@ public void Can_Verify_PropertyTypes_On_Video_MediaType() { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index f801d02c5bc0..bf8450383720 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -1,29 +1,27 @@ -using System.Web; -using System.Xml.Linq; -using System.Xml.XPath; +using Examine; using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.UmbracoExamine; -using Umbraco.Web; using System.Linq; using System.Threading; +using System.Web; using System.Xml; -using Examine; +using System.Xml.Linq; +using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Strings; -using Umbraco.Examine; -using Current = Umbraco.Web.Composing.Current; -using Umbraco.Tests.Testing; using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Examine; using Umbraco.Tests.LegacyXmlPublishedCache; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Tests.UmbracoExamine; +using Umbraco.Web; namespace Umbraco.Tests.PublishedContent { @@ -94,6 +92,7 @@ public void Get_Property_Value_Uses_Converter() Name = "Rich Text", DataTypeId = -87 //tiny mce }); + var existing = ServiceContext.MediaTypeService.GetAll(); ServiceContext.MediaTypeService.Save(mType); var media = MockedMedia.CreateMediaImage(mType, -1); media.Properties["content"].SetValue("
This is some content
"); diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index 52f26ecb4d15..d5cec11211c1 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -163,7 +163,7 @@ public void Cannot_Save_Media_With_Empty_Name() { // Arrange var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia(string.Empty, -1, "video"); @@ -175,7 +175,7 @@ public void Cannot_Save_Media_With_Empty_Name() public void Ensure_Content_Xml_Created() { var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia("Test", -1, "video"); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index e3bb012dae0d..1b85787fee3a 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -378,13 +378,13 @@ public static ContentType CreateAllTypesContentType(string alias, string name) return contentType; } - public static MediaType CreateVideoMediaType() + public static MediaType CreateNewMediaType() { var mediaType = new MediaType(-1) { - Alias = "video", - Name = "Video", - Description = "ContentType used for videos", + Alias = "newMediaType", + Name = "New Media Type", + Description = "ContentType used for a new format", Icon = ".sprTreeDoc3", Thumbnail = "doc.png", SortOrder = 1, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js new file mode 100644 index 000000000000..63681a380a3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js @@ -0,0 +1,12 @@ +angular.module("umbraco.directives").directive('validWhen', function () { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, element, attr, ngModel) { + + attr.$observe("validWhen", function (newValue) { + ngModel.$setValidity("validWhen", newValue === "true"); + }); + } + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index f1f2cb38e84c..744e4280db74 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -6,10 +6,14 @@ **/ angular.module("umbraco.directives") .directive('umbImageCrop', - function ($timeout, cropperHelper) { + function ($timeout, $window, cropperHelper) { + + const MAX_SCALE = 4; + return { restrict: 'E', replace: true, + transclude: true, templateUrl: 'views/components/imaging/umb-image-crop.html', scope: { src: '=', @@ -17,24 +21,29 @@ angular.module("umbraco.directives") height: '@', crop: "=", center: "=", - maxSize: '@' + maxSize: '@?', + alias: '@?', + forceUpdate: '@?' }, link: function (scope, element, attrs) { + var unsubscribe = []; let sliderRef = null; - scope.width = 400; - scope.height = 320; + scope.loaded = false; + scope.width = 0; + scope.height = 0; scope.dimensions = { + element: {}, image: {}, cropper: {}, viewport: {}, - margin: 20, + margin: {}, scale: { - min: 0, - max: 3, + min: 1, + max: MAX_SCALE, current: 1 } }; @@ -45,10 +54,10 @@ angular.module("umbraco.directives") "tooltips": [false], "format": { to: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); }, from: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); } }, "range": { @@ -59,18 +68,23 @@ angular.module("umbraco.directives") scope.setup = function (slider) { sliderRef = slider; + updateSlider(); + }; - // Set slider handle position - sliderRef.noUiSlider.set(scope.dimensions.scale.current); + function updateSlider() { + if(sliderRef) { + // Update slider range min/max + sliderRef.noUiSlider.updateOptions({ + "range": { + "min": scope.dimensions.scale.min, + "max": scope.dimensions.scale.max + } + }); - // Update slider range min/max - sliderRef.noUiSlider.updateOptions({ - "range": { - "min": scope.dimensions.scale.min, - "max": scope.dimensions.scale.max - } - }); - }; + // Set slider handle position + sliderRef.noUiSlider.set(scope.dimensions.scale.current); + } + } scope.slide = function (values) { if (values) { @@ -84,77 +98,108 @@ angular.module("umbraco.directives") } }; + function onScroll(event) { + // cross-browser wheel delta + var delta = Math.max(-50, Math.min(50, (event.wheelDelta || -event.detail))); + + if(sliderRef) { + var currentScale =sliderRef.noUiSlider.get(); + + var newScale = Math.min(Math.max(currentScale + delta*.001*scope.dimensions.image.ratio, scope.dimensions.scale.min), scope.dimensions.scale.max); + sliderRef.noUiSlider.set(newScale); + scope.$evalAsync(() => { + scope.dimensions.scale.current = newScale; + }); + + if(event.preventDefault) { + event.preventDefault(); + } + } + } + + //live rendering of viewport and image styles - scope.style = function () { - return { - 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' - }; + function updateStyles() { + scope.maskStyle = { + 'height': (parseInt(scope.dimensions.cropper.height, 10)) + 'px', + 'width': (parseInt(scope.dimensions.cropper.width, 10)) + 'px', + 'top': (parseInt(scope.dimensions.margin.top, 10)) + 'px', + 'left': (parseInt(scope.dimensions.margin.left, 10)) + 'px' + } }; + updateStyles(); //elements var $viewport = element.find(".viewport"); var $image = element.find("img"); var $overlay = element.find(".overlay"); - var $container = element.find(".crop-container"); + + $overlay.bind("focus", function () { + $overlay.bind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + $overlay.bind("blur", function () { + $overlay.unbind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + //default constraints for drag n drop - var constraints = { left: { max: scope.dimensions.margin, min: scope.dimensions.margin }, top: { max: scope.dimensions.margin, min: scope.dimensions.margin } }; + var constraints = { left: { max: 0, min: 0 }, top: { max: 0, min: 0 } }; scope.constraints = constraints; //set constaints for cropping drag and drop var setConstraints = function () { - constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; - constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; + constraints.left.min = scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.cropper.height - scope.dimensions.image.height; }; - var setDimensions = function (originalImage) { - originalImage.width("auto"); - originalImage.height("auto"); + var setDimensions = function () { - var image = {}; - image.originalWidth = originalImage.width(); - image.originalHeight = originalImage.height(); - - image.width = image.originalWidth; - image.height = image.originalHeight; - image.left = originalImage[0].offsetLeft; - image.top = originalImage[0].offsetTop; - - scope.dimensions.image = image; + scope.dimensions.image.width = scope.dimensions.image.originalWidth; + scope.dimensions.image.height = scope.dimensions.image.originalHeight; //unscaled editor size - //var viewPortW = $viewport.width(); - //var viewPortH = $viewport.height(); - var _viewPortW = parseInt(scope.width, 10); - var _viewPortH = parseInt(scope.height, 10); - - //if we set a constraint we will scale it down if needed - if (scope.maxSize) { - var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, - _viewPortH, - scope.maxSize); - - //so if we have a max size, override the thumb sizes - _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; - } - - scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; - scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; - scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; - scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; + var _cropW = parseInt(scope.width, 10); + var _cropH = parseInt(scope.height, 10); + + var ratioCalculation = cropperHelper.scaleToMaxSize( + _cropW, + _cropH, + scope.dimensions.viewport.width - 40, + scope.dimensions.viewport.height - 40); + + //so if we have a max size, override the thumb sizes + _cropW = ratioCalculation.width; + _cropH = ratioCalculation.height; + + // set margins: + scope.dimensions.margin.left = (scope.dimensions.viewport.width - _cropW) * 0.5; + scope.dimensions.margin.top = (scope.dimensions.viewport.height - _cropH) * 0.5; + + scope.dimensions.cropper.width = _cropW; + scope.dimensions.cropper.height = _cropH; + updateStyles(); }; //resize to a given ratio var resizeImageToScale = function (ratio) { - //do stuff - var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; + + var prevWidth = scope.dimensions.image.width; + var prevHeight = scope.dimensions.image.height; + + scope.dimensions.image.width = scope.dimensions.image.originalWidth * ratio; + scope.dimensions.image.height = scope.dimensions.image.originalHeight * ratio; + + var difW = (scope.dimensions.image.width - prevWidth); + var difH = (scope.dimensions.image.height - prevHeight); + + // normalized focus point: + var focusNormX = (-scope.dimensions.image.left + scope.dimensions.cropper.width*.5) / prevWidth; + var focusNormY = (-scope.dimensions.image.top + scope.dimensions.cropper.height*.5) / prevHeight; + + scope.dimensions.image.left = scope.dimensions.image.left - difW * focusNormX; + scope.dimensions.image.top = scope.dimensions.image.top - difH * focusNormY; setConstraints(); validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); @@ -163,10 +208,10 @@ angular.module("umbraco.directives") //resize the image to a predefined crop coordinate var resizeImageToCrop = function () { scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, + runtimeCrop, { width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight }, scope.dimensions.cropper, - scope.dimensions.margin); + 0); var ratioCalculation = cropperHelper.calculateAspectRatioFit( scope.dimensions.image.originalWidth, @@ -178,25 +223,19 @@ angular.module("umbraco.directives") scope.dimensions.scale.current = scope.dimensions.image.ratio; // Update min and max based on original width/height + // Here we update the slider to use the scala of the current setup, i dont know why its made in this way but this is how it is. scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; + // TODO: Investigate wether we can limit users to not scale bigger than the amount of pixels in the source: + //scope.dimensions.scale.max = ratioCalculation.ratio * Math.min(MAX_SCALE, scope.dimensions.image.originalWidth/scope.dimensions.cropper.width); + scope.dimensions.scale.max = ratioCalculation.ratio * MAX_SCALE; + + updateSlider(); }; var validatePosition = function (left, top) { - if (left > constraints.left.max) { - left = constraints.left.max; - } - if (left <= constraints.left.min) { - left = constraints.left.min; - } - - if (top > constraints.top.max) { - top = constraints.top.max; - } - if (top <= constraints.top.min) { - top = constraints.top.min; - } + left = Math.min(Math.max(left, constraints.left.min), constraints.left.max); + top = Math.min(Math.max(top, constraints.top.min), constraints.top.max); if (scope.dimensions.image.left !== left) { scope.dimensions.image.left = left; @@ -209,36 +248,54 @@ angular.module("umbraco.directives") //sets scope.crop to the recalculated % based crop - var calculateCropBox = function () { - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + function calculateCropBox() { + runtimeCrop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, 0); }; + function saveCropBox() { + scope.crop = Utilities.copy(runtimeCrop); + } //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; + //var onStartDragPosition, top, left; + var dragStartPosition = {}; $overlay.draggable({ + start: function (event, ui) { + dragStartPosition.left = scope.dimensions.image.left; + dragStartPosition.top = scope.dimensions.image.top; + }, drag: function (event, ui) { scope.$apply(function () { - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); }); }, stop: function (event, ui) { scope.$apply(function () { //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); + saveCropBox(); }); } }); - var init = function (image) { - scope.loaded = false; + var runtimeCrop; + var init = function () { + + // store original size: + scope.dimensions.image.originalWidth = $image.width(); + scope.dimensions.image.originalHeight = $image.height(); + + // runtime Crop, should not be saved until we have interactions: + runtimeCrop = Utilities.copy(scope.crop); + + onViewportSizeChanged(); - //set dimensions on image, viewport, cropper etc - setDimensions(image); + scope.loaded = true; + }; + function setCrop() { //create a default crop if we haven't got one already var createDefaultCrop = !scope.crop; if (createDefaultCrop) { @@ -275,41 +332,67 @@ angular.module("umbraco.directives") resizeImageToCrop(); } } + } + + + function onViewportSizeChanged() { + scope.dimensions.viewport.width = $viewport.width(); + scope.dimensions.viewport.height = $viewport.height(); - //sets constaints for the cropper + setDimensions(); + setCrop(); setConstraints(); - scope.loaded = true; - }; + } // Watchers - scope.$watchCollection('[width, height]', function (newValues, oldValues) { + unsubscribe.push(scope.$watchCollection('[width, height, alias, forceUpdate]', function (newValues, oldValues) { // We have to reinit the whole thing if // one of the external params changes if (newValues !== oldValues) { - setDimensions($image); + runtimeCrop = Utilities.copy(scope.crop); + setDimensions(); + setCrop(); setConstraints(); } - }); + })); - var throttledResizing = _.throttle(function () { + var throttledScale = _.throttle(() => scope.$evalAsync(() => { resizeImageToScale(scope.dimensions.scale.current); calculateCropBox(); - }, 15); + saveCropBox(); + }), 16); // Happens when we change the scale - scope.$watch("dimensions.scale.current", function (newValue, oldValue) { + unsubscribe.push(scope.$watch("dimensions.scale.current", function (newValue, oldValue) { if (scope.loaded) { - throttledResizing(); + throttledScale(); } - }); + })); + // Init + + //if we have a max-size we will use it, to keep this backwards compatible. + // I dont see this max size begin usefull, as we should aim for responsive UI. + if (scope.maxSize) { + element.css("max-width", parseInt(scope.maxSize, 10) + "px"); + element.css("max-height", parseInt(scope.maxSize, 10) + "px"); + } + $image.on("load", function () { $timeout(function () { - init($image); + init(); }); }); + + windowResizeListener.register(onViewportSizeChanged); + + scope.$on('$destroy', function () { + $image.prop("src", ""); + windowResizeListener.unregister(onViewportSizeChanged); + unsubscribe.forEach(u => u()); + }) } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index fd9a236f87f3..277848811b53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -13,8 +13,8 @@ top: 0 }; - var htmlImage = null; //DOM element reference - var htmlOverlay = null; //DOM element reference + var imageElement = null; //DOM element reference + var focalPointElement = null; //DOM element reference var draggable = null; vm.loaded = false; @@ -22,33 +22,33 @@ vm.$onChanges = onChanges; vm.$postLink = postLink; vm.$onDestroy = onDestroy; - vm.style = style; + vm.style = {}; + vm.overlayStyle = {}; vm.setFocalPoint = setFocalPoint; /** Sets the css style for the Dot */ - function style() { - - if (vm.dimensions.width <= 0 || vm.dimensions.height <= 0) { - //this initializes the dimensions since when the image element first loads - //there will be zero dimensions - setDimensions(); - } - - return { + function updateStyle() { + vm.style = { 'top': vm.dimensions.top + 'px', 'left': vm.dimensions.left + 'px' }; + vm.overlayStyle = { + 'width': vm.dimensions.width + 'px', + 'height': vm.dimensions.height + 'px' + }; + }; - function setFocalPoint (event) { + function setFocalPoint(event) { $scope.$emit("imageFocalPointStart"); - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; + // We do this to get the right position, no matter the focalPoint was clicked. + var viewportPosition = imageElement[0].getBoundingClientRect(); + var offsetX = event.clientX - viewportPosition.left; + var offsetY = event.clientY - viewportPosition.top; calculateGravity(offsetX, offsetY); - - lazyEndEvent(); + $scope.$emit("imageFocalPointStop"); }; /** Initializes the component */ @@ -61,33 +61,30 @@ /** Called when the component has linked everything and the DOM is available */ function postLink() { //elements - htmlImage = $element.find("img"); - htmlOverlay = $element.find(".overlay"); + imageElement = $element.find("img"); + focalPointElement = $element.find(".focalPoint"); //Drag and drop positioning, using jquery ui draggable - draggable = htmlOverlay.draggable({ + draggable = focalPointElement.draggable({ containment: "parent", start: function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStart"); - }); + $scope.$emit("imageFocalPointStart"); }, - stop: function () { - $scope.$apply(function () { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); - - lazyEndEvent(); + stop: function (event, ui) { + + var offsetX = ui.position.left; + var offsetY = ui.position.top; + + $scope.$evalAsync(calculateGravity(offsetX, offsetY)); + + $scope.$emit("imageFocalPointStop"); + } }); - $(window).on('resize.umbImageGravity', function () { - $scope.$apply(function () { - resized(); - }); - }); + window.addEventListener('resize.umbImageGravity', onResizeHandler); + window.addEventListener('resize', onResizeHandler); + //if any ancestor directive emits this event, we need to resize $scope.$on("editors.content.splitViewChanged", function () { @@ -95,12 +92,12 @@ }); //listen for the image DOM element loading - htmlImage.on("load", function () { + imageElement.on("load", function () { $timeout(function () { vm.isCroppable = true; vm.hasDimensions = true; - + if (vm.src) { if (vm.src.endsWith(".svg")) { vm.isCroppable = false; @@ -117,6 +114,8 @@ } setDimensions(); + updateStyle(); + vm.loaded = true; if (vm.onImageLoaded) { vm.onImageLoaded({ @@ -129,16 +128,19 @@ } function onDestroy() { - $(window).off('resize.umbImageGravity'); - if (htmlOverlay) { + window.removeEventListener('resize.umbImageGravity', onResizeHandler); + window.removeEventListener('resize', onResizeHandler); + /* + if (focalPointElement) { // TODO: This should be destroyed but this will throw an exception: // "cannot call methods on draggable prior to initialization; attempted to call method 'destroy'" // I've tried lots of things and cannot get this to work, we weren't destroying before so hopefully // there's no mem leaks? - //htmlOverlay.draggable("destroy"); + focalPointElement.draggable("destroy"); } - if (htmlImage) { - htmlImage.off("load"); + */ + if (imageElement) { + imageElement.off("load"); } } @@ -146,14 +148,21 @@ function resized() { $timeout(function () { setDimensions(); + updateStyle(); }); + /* // Make sure we can find the offset values for the overlay(dot) before calculating // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) - if (htmlOverlay.is(':visible')) { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; + if (focalPointElement.is(':visible')) { + var offsetX = focalPointElement[0].offsetLeft; + var offsetY = focalPointElement[0].offsetTop; calculateGravity(offsetX, offsetY); } + */ + } + + function onResizeHandler() { + $scope.$evalAsync(resized); } /** Watches the one way binding changes */ @@ -163,17 +172,18 @@ && !Utilities.equals(changes.center.currentValue, changes.center.previousValue)) { //when center changes update the dimensions setDimensions(); + updateStyle(); } } /** Sets the width/height/left/top dimentions based on the image size and the "center" value */ function setDimensions() { - if (vm.isCroppable && htmlImage && vm.center) { - vm.dimensions.width = htmlImage.width(); - vm.dimensions.height = htmlImage.height(); - vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; - vm.dimensions.top = vm.center.top * vm.dimensions.height - 10; + if (vm.isCroppable && imageElement && vm.center) { + vm.dimensions.width = imageElement.width(); + vm.dimensions.height = imageElement.height(); + vm.dimensions.left = vm.center.left * vm.dimensions.width; + vm.dimensions.top = vm.center.top * vm.dimensions.height; } return vm.dimensions.width; @@ -185,31 +195,22 @@ * @param {any} offsetY */ function calculateGravity(offsetX, offsetY) { - vm.onValueChanged({ - left: (offsetX + 10) / vm.dimensions.width, - top: (offsetY + 10) / vm.dimensions.height + left: Math.min(Math.max(offsetX, 0), vm.dimensions.width) / vm.dimensions.width, + top: Math.min(Math.max(offsetY, 0), vm.dimensions.height) / vm.dimensions.height }); - - //vm.center.left = (offsetX + 10) / scope.dimensions.width; - //vm.center.top = (offsetY + 10) / scope.dimensions.height; }; - var lazyEndEvent = _.debounce(function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStop"); - }); - }, 2000); - } var umbImageGravityComponent = { templateUrl: 'views/components/imaging/umb-image-gravity.html', bindings: { src: "<", - center: "<", + center: "<", onImageLoaded: "&?", - onValueChanged: "&" + onValueChanged: "&", + disableFocalPoint: " 0) { setFlexValues(scope.items); } @@ -188,7 +188,7 @@ Use this directive to generate a thumbnail grid of media items. } } } - + /** * Returns wether a item should be selectable or not. */ @@ -203,9 +203,9 @@ Use this directive to generate a thumbnail grid of media items. } else { return scope.onlyFolders !== "true"; } - + return false; - + } function setOriginalSize(item, maxHeight) { @@ -255,7 +255,7 @@ Use this directive to generate a thumbnail grid of media items. } } - + function setFlexValues(mediaItems) { var flexSortArray = mediaItems; @@ -292,8 +292,11 @@ Use this directive to generate a thumbnail grid of media items. mediaItem.flexStyle = flexStyle; } } - + scope.clickItem = function(item, $event, $index) { + if (item.isFolder === true && item.filtered) { + scope.clickItemName(item, $event, $index); + } if (scope.onClick) { scope.onClick(item, $event, $index); $event.stopPropagation(); @@ -312,7 +315,7 @@ Use this directive to generate a thumbnail grid of media items. scope.onDetailsHover(item, $event, hover); } }; - + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { if (Utilities.isArray(newValue)) { activate(); @@ -333,7 +336,7 @@ Use this directive to generate a thumbnail grid of media items. //change sort scope.setSort = function (col) { if (scope.sortColumn === col) { - scope.sortReverse = !scope.sortReverse; + scope.sortReverse = !scope.sortReverse; } else { scope.sortColumn = col; @@ -345,9 +348,9 @@ Use this directive to generate a thumbnail grid of media items. } } scope.sortDirection = scope.sortReverse ? "desc" : "asc"; - + } - // sort function + // sort function scope.sortBy = function (item) { if (scope.sortColumn === "updateDate") { return [-item['isFolder'],item['updateDate']]; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index db1e38adc626..5492fee1a0ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -85,7 +85,7 @@ /** Called when the component has linked all elements, this is when the form controller is available */ function postLink() { - + } function initialize() { @@ -186,7 +186,7 @@ }); } } - + } } @@ -325,7 +325,8 @@ */ onFilesChanged: "&", onInit: "&", - required: "=" + required: "=", + acceptFileExt: ""; + return { restrict: "E", scope: { - rebuild: "=" + rebuild: "=", + acceptFileExt: "", - link: function (scope, el, attrs) { + template: "
"+innerTemplate+"
", + link: function (scope, el) { scope.$watch("rebuild", function (newVal, oldVal) { if (newVal && newVal !== oldVal) { //recompile it! - el.html(""); + el.html(innerTemplate); $compile(el.contents())(scope); } }); @@ -30,4 +35,4 @@ function umbSingleFileUpload($compile) { }; } -angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); \ No newline at end of file +angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index 5f8600c8c0fe..b07ab55436f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -2,8 +2,8 @@ * @ngdoc directive * @name umbraco.directives.directive:valServerMatch * @restrict A - * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data - * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against + * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data + * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against * a property validation key. The attribute value can be in multiple value types: * - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied. * - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either: diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 83fd3d08c233..901e5fa93cf7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -17,6 +17,7 @@ function clipboardService($window, notificationsService, eventsService, localSto TYPES.ELEMENT_TYPE = "elementType"; TYPES.BLOCK = "block"; TYPES.RAW = "raw"; + TYPES.MEDIA = "media"; var clearPropertyResolvers = {}; var pastePropertyResolvers = {}; @@ -70,6 +71,9 @@ function clipboardService($window, notificationsService, eventsService, localSto propMethod(data[p], TYPES.RAW); } } + clipboardTypeResolvers[TYPES.MEDIA] = function(data, propMethod) { + // no resolving needed for this type currently. + } var STORAGE_KEY = "umbClipboardService"; @@ -147,6 +151,8 @@ function clipboardService($window, notificationsService, eventsService, localSto return entry.type === type && ( + allowedAliases === null + || (entry.alias && allowedAliases.filter(allowedAlias => allowedAlias === entry.alias).length > 0) || (entry.aliases && entry.aliases.filter(entryAlias => allowedAliases.filter(allowedAlias => allowedAlias === entryAlias).length > 0).length === entry.aliases.length) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js index 256a1461db3f..1f860f237caa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js @@ -44,24 +44,23 @@ function cropperHelper(umbRequestHelper, $http) { return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; }, - scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { - - var retVal = {height: srcHeight, width: srcWidth}; + scaleToMaxSize : function(srcWidth, srcHeight, maxWidth, maxHeight) { - if(srcWidth > maxSize ||srcHeight > maxSize){ - var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; - ratio = Math.min(ratio[0], ratio[1]); - - retVal.height = srcHeight * ratio; - retVal.width = srcWidth * ratio; - } - - return retVal; + // fallback to maxHeight: + maxHeight = maxHeight || maxWidth; + + // get smallest ratio, if ratio exceeds 1 we will not scale(hence we parse 1 as the maximum allowed ratio) + var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight, 1); + + return { + width: srcWidth * ratio, + height:srcHeight * ratio + }; }, //returns a ng-style object with top,left,width,height pixel measurements //expects {left,right,top,bottom} - {width,height}, {width,height}, int - //offset is just to push the image position a number of pixels from top,left + //offset is just to push the image position a number of pixels from top,left convertToStyle : function(coordinates, originalSize, viewPort, offset){ var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); @@ -85,14 +84,14 @@ function cropperHelper(umbRequestHelper, $http) { return style; }, - + coordinatesToPixels : function(coordinates, originalSize, offset){ var coordinates_px = { x1: Math.floor(coordinates.x1 * originalSize.width), y1: Math.floor(coordinates.y1 * originalSize.height), x2: Math.floor(coordinates.x2 * originalSize.width), - y2: Math.floor(coordinates.y2 * originalSize.height) + y2: Math.floor(coordinates.y2 * originalSize.height) }; return coordinates_px; @@ -106,25 +105,18 @@ function cropperHelper(umbRequestHelper, $http) { var x2_px = image.width - (x1_px + width); var y2_px = image.height - (y1_px + height); - //crop coordinates in % var crop = {}; - crop.x1 = x1_px / image.width; - crop.y1 = y1_px / image.height; - crop.x2 = x2_px / image.width; - crop.y2 = y2_px / image.height; - - for(var coord in crop){ - if(crop[coord] < 0){ - crop[coord] = 0; - } - } + crop.x1 = Math.max(x1_px / image.width, 0); + crop.y1 = Math.max(y1_px / image.height, 0); + crop.x2 = Math.max(x2_px / image.width, 0); + crop.y2 = Math.max(y2_px / image.height, 0); return crop; }, alignToCoordinates : function(image, center, viewport){ - + var min_left = (image.width) - (viewport.width); var min_top = (image.height) - (viewport.height); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 359c3dd427ce..6f95608d7a8d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -215,6 +215,11 @@ @import "../views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less"; +@import "../views/components/mediacard/umb-media-card-grid.less"; +@import "../views/components/mediacard/umb-media-card.less"; +@import "../views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less"; +@import "../views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less"; +@import "../views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less index febee80a9778..ffe87277e660 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -20,7 +20,7 @@ > span { position: absolute; - color: @white; + color: @ui-active-type; background: @ui-active; padding: 1px 3px; font-size: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 5f79d65de161..71be01e6fff8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -34,21 +34,6 @@ } -.umb-media-grid__item.-unselectable { - &::before { - content: ""; - position: absolute; - z-index: 1; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: @baseBorderRadius; - background-color: rgba(230, 230, 230, .8); - pointer-events: none; - } -} - .umb-media-grid__item.-selectable, .umb-media-grid__item.-folder {// If folders isnt selectable, they opens if clicked, therefor... cursor: pointer; @@ -59,21 +44,12 @@ } .umb-media-grid__item.-folder { - &.-selectable { .media-grid-item-edit:hover .umb-media-grid__item-name, .media-grid-item-edit:focus .umb-media-grid__item-name { text-decoration: underline; } } - - &.-unselectable { - &:hover, &:focus { - .umb-media-grid__item-name { - text-decoration: underline; - } - } - } } @@ -85,8 +61,7 @@ } .umb-media-grid__item.-selected, .umb-media-grid__item.-selectable:hover { - &::before { - content: ""; + .umb-media-grid__item-select { position: absolute; z-index:2; top: -2px; @@ -100,15 +75,21 @@ } } .umb-media-grid__item.-selectable:hover { - &::before { + .umb-media-grid__item-select { opacity: .33; } } .umb-media-grid__item.-selected:hover { - &::before { + .umb-media-grid__item-select { opacity: .75; } } +.umb-media-grid__item.-filtered:not(.-folder) { + cursor:not-allowed; + * { + pointer-events: none; + } +} .umb-media-grid__item-file-icon { transform: translate(-50%,-50%); @@ -189,14 +170,25 @@ } } -.umb-media-grid__item-name { - cursor: pointer; +.umb-media-grid__item-overlay { + cursor: pointer; + + &:hover .umb-media-grid__item-name{ + text-decoration: underline; + } +} + +.umb-media-grid__item-overlay:not(.-selected) { + &:hover + .umb-media-grid__item-select { + display: none; + } } .umb-media-grid__item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 6ae92ffa4e99..cc5c17ba70cb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -2,19 +2,35 @@ .umb-range-slider.noUi-target { background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: none; border-radius: 20px; - height: 10px; - border: none; + height: 8px; + border: 1px solid @inputBorder; + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } +} +.umb-range-slider .noUi-connects { + cursor: pointer; + height: 20px; + top: -6px; +} +.umb-range-slider .noUi-tooltip { + padding: 2px 6px; } - .umb-range-slider .noUi-handle { + outline: none; + cursor: grab; border-radius: 100px; border: none; box-shadow: none; width: 20px !important; height: 20px !important; - background-color: @blueMid; + right: -10px !important; // half the handle width + background-color: @blueExtraDark; +} +.umb-range-slider .noUi-horizontal .noUi-handle { + top: -7px; } .umb-range-slider .noUi-handle::before { @@ -25,10 +41,6 @@ display: none; } -.umb-range-slider .noUi-handle { - right: -10px !important; // half the handle width -} - .umb-range-slider .noUi-marker-large.noUi-marker-horizontal { height: 10px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 9739a90dae7a..b046ca69d923 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -405,7 +405,7 @@ } } -.checkeredBackground(@backgroundColor: @gray-9, @fillColor: @black, @fillOpacity: 0.25) { +.checkeredBackground(@backgroundColor: @white, @fillColor: @black, @fillOpacity: 0.1) { background-image: url('data:image/svg+xml;charset=utf-8,\ \ \ diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index f5e652aa3d85..328ba2229baf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -463,9 +463,16 @@ .umb-cropper{ position: relative; + width: 100%; +} + +.umb-cropper .crop-container { + position: relative; + width: 100%; + padding-bottom: 9 / 16 * 100%; } -.umb-cropper img, .umb-cropper-gravity img{ +.umb-cropper img { position: relative; max-width: 100%; height: auto; @@ -477,75 +484,72 @@ max-width: none; } - .umb-cropper .overlay, .umb-cropper-gravity .overlay { - top: 0; - left: 0; + .umb-cropper .overlay { + position: absolute; + top: 0 !important; + bottom: 0; + left: 0 !important; + right: 0; cursor: move; z-index: @zindexCropperOverlay; - position: absolute; -} + border: 1px solid @inputBorder; + outline: none; -.umb-cropper .viewport{ - overflow: hidden; - position: relative; - margin: auto; - max-width: 100%; - height: auto; - } + &:focus { + border-color: @inputBorderFocus; + } +} -.umb-cropper-gravity .viewport{ +.umb-cropper .viewport { + position: absolute; overflow: hidden; - position: relative; width: 100%; height: 100%; -} + .checkeredBackground(); + contain: strict; + > img { + position: absolute; + } + } -.umb-cropper .viewport:after { - content: ""; +.umb-cropper .viewport .__mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: @zindexCropperOverlay - 1; - opacity: .75; - box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + box-shadow: 0 0 0 2000px rgba(255, 255, 255, .8); } - -.umb-cropper-gravity .overlay{ - width: 14px; - height: 14px; - text-align: center; - border-radius: 20px; - background: @pinkLight; - border: 3px solid @white; - opacity: 0.8; -} - -.umb-cropper-gravity .overlay i { - font-size: 26px; - line-height: 26px; - opacity: 0.8 !important; +.umb-cropper .viewport .__mask-info { + position: absolute; + bottom: -20px; + height: 20px; + right: 0; + z-index: @zindexCropperOverlay - 1; + font-size: 12px; + opacity: 0.7; + padding: 0px 6px; } -.umb-cropper .crop-container { - text-align: center; +.umb-cropper .crop-controls-wrapper { + display: flex; + height: 50px; + align-items: center; + background-color: #fff; + .btn:last-of-type { + margin-right: 10px; + } } .umb-cropper .crop-slider-wrapper { - padding: 10px; - border-top: 1px solid @gray-10; - margin-top: 10px; + flex: auto; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; - @media (min-width: 769px) { - padding: 10px 50px 10px 50px; - } - i { color: @gray-3; flex: 0 0 25px; @@ -558,11 +562,20 @@ } .crop-slider { - padding: 50px 15px 40px 15px; - width: 66.6%; + width: calc(100% - 100px); } } +.umb-cropper .crop-controls-wrapper__icon-left { + margin-right: 10px; + +} +.umb-cropper .crop-controls-wrapper__icon-right { + margin-left: 10px; + font-size: 22px; +} + +/* .umb-cropper-gravity .viewport, .umb-cropper-gravity, .umb-cropper-imageholder { display: inline-block; max-width: 100%; @@ -572,30 +585,51 @@ float: left; } + .umb-cropper-imageholder umb-image-gravity { + display:block; + } + */ + + .umb-crop-thumbnail-container { + img { + max-width: unset; + } + } + .cropList { display: inline-block; position: relative; vertical-align: top; + flex:0; } - .gravity-container { - border: 1px solid @gray-8; + .umb-cropper-gravity .gravity-container { + border: 1px solid @inputBorder; + box-sizing: border-box; line-height: 0; + width: 100%; + height: 100%; + overflow: hidden; + .checkeredBackground(); + contain: content; + + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } .viewport { - max-width: 600px; - .checkeredBackground(); + position: relative; + width: 100%; + height: 100%; - img { - display: block; - margin-left: auto; - margin-right: auto; - } + display: flex; + justify-content: center; + align-items: center; img { display: block; - margin-left: auto; - margin-right: auto; + max-width: 100%; + max-height: 100%; } &:hover { @@ -604,6 +638,62 @@ } } + + .umb-cropper-gravity img { + position: relative; + max-width: 100%; + height: auto; + top: 0; + left: 0; + } + + .umb-cropper-gravity .overlayViewport { + position: absolute; + top:0; + bottom:0; + left:0; + right:0; + contain: strict; + + display: flex; + justify-content: center; + align-items: center; + } + .umb-cropper-gravity .overlay { + position: relative; + display: block; + max-width: 100%; + max-height: 100%; + cursor: crosshair; + } + .umb-cropper-gravity .overlay .focalPoint { + position: absolute; + top: 0; + left: 0; + cursor: move; + z-index: @zindexCropperOverlay; + + width: 14px; + height: 14px; + // this element should have no width or height as its preventing the jQuery draggable-plugin to go all the way to the sides: + margin-left: -10px; + margin-top: -10px; + margin-right: -10px; + margin-bottom: -10px; + + text-align: center; + border-radius: 20px; + background: @pinkLight; + border: 3px solid @white; + opacity: 0.8; + } + + .umb-cropper-gravity .overlay .focalPoint i { + font-size: 26px; + line-height: 26px; + opacity: 0.8 !important; + } + .imagecropper { display: flex; align-items: flex-start; @@ -611,24 +701,13 @@ @media (max-width: 768px) { flex-direction: column; - float: left; - max-width: 100%; - } - - .viewport img { - .checkeredBackground(); } + } .imagecropper .umb-cropper__container { position: relative; - margin-bottom: 10px; - max-width: 100%; - border: 1px solid @gray-10; - - @media (min-width: 769px) { - width: 600px; - } + width: 100%; } .imagecropper .umb-cropper__container .button-drawer { diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index d21331f1069a..7d9431fcae6f 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -1,18 +1,18 @@ -/** +/** * @ngdoc controller - * @name Umbraco.MainController + * @name Umbraco.MainController * @function - * - * @description + * + * @description * The main application controller - * + * */ -function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, navigationService, eventsService, +function MainController($scope, $location, appState, treeService, notificationsService, + userService, historyService, updateChecker, navigationService, eventsService, tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { - + //the null is important because we do an explicit bool check on this in the view - $scope.authenticated = null; + $scope.authenticated = null; $scope.touchDevice = appState.getGlobalState("touchDevice"); $scope.infiniteMode = false; $scope.overlay = {}; @@ -27,14 +27,14 @@ function MainController($scope, $location, appState, treeService, notificationsS assetsService.loadJs(tinyJsAsset, $scope); }); - // 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) { enableTabbingActive(); } } - + function enableTabbingActive() { $scope.tabbingActive = true; $scope.$digest(); @@ -185,7 +185,7 @@ function MainController($scope, $location, appState, treeService, notificationsS evts.push(eventsService.on("appState.overlay", function (name, args) { $scope.overlay = args; })); - + // events for tours evts.push(eventsService.on("appState.tour.start", function (name, args) { $scope.tour = args; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js new file mode 100644 index 000000000000..6c8a0385365e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js @@ -0,0 +1,183 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.MediaEntryEditorController", + function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) { + + var unsubscribe = []; + var vm = this; + + vm.loading = true; + vm.model = $scope.model; + vm.mediaEntry = vm.model.mediaEntry; + vm.currentCrop = null; + + localizationService.localizeMany([ + vm.model.createFlow ? "general_cancel" : "general_close", + vm.model.createFlow ? "general_create" : "buttons_submitChanges" + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + + vm.title = ""; + + function init() { + + updateMedia(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaEntry.mediaKey) { + updateMedia(); + } + })); + } + + function updateMedia() { + + vm.loading = true; + entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + + localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) { + vm.title = data; + }); + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + }); + }); + } + + vm.onImageLoaded = onImageLoaded; + function onImageLoaded(isCroppable, hasDimensions) { + vm.isCroppable = isCroppable; + vm.hasDimensions = hasDimensions; + }; + + + vm.repickMedia = repickMedia; + function repickMedia() { + vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced); + } + + function onMediaReplaced() { + + // mark we have changes: + vm.imageCropperForm.$setDirty(); + + // un-select crop: + vm.currentCrop = null; + + // + updateMedia(); + } + + vm.openMedia = openMedia; + function openMedia() { + + var mediaEditor = { + id: vm.mediaEntry.mediaKey, + submit: function () { + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.mediaEditor(mediaEditor); + } + + + vm.focalPointChanged = function(left, top) { + //update the model focalpoint value + vm.mediaEntry.focalPoint = { + left: left, + top: top + }; + + //set form to dirty to track changes + setDirty(); + } + + + + vm.selectCrop = selectCrop; + function selectCrop(targetCrop) { + vm.currentCrop = targetCrop; + setDirty(); + // TODO: start watchin values of crop, first when changed set to dirty. + }; + + vm.deselectCrop = deselectCrop; + function deselectCrop() { + vm.currentCrop = null; + }; + + vm.resetCrop = resetCrop; + function resetCrop() { + if (vm.currentCrop) { + $scope.$evalAsync( () => { + vm.model.propertyEditor.resetCrop(vm.currentCrop); + vm.forceUpdateCrop = Math.random(); + }); + } + } + + function setDirty() { + vm.imageCropperForm.$setDirty(); + } + + + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + vm.model.submit(vm.model); + } + } + + vm.close = function () { + if (vm.model && vm.model.close) { + if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { + const confirm = { + title: localizations[0], + view: "default", + content: localizations[1], + submitButtonLabelKey: "general_discard", + submitButtonStyle: "danger", + closeButtonLabelKey: "prompt_stay", + submit: function () { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + } else { + vm.model.close(vm.model); + } + + } + } + + init(); + $scope.$on("$destroy", function () { + unsubscribe.forEach(x => x()); + }); + + } + ); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html new file mode 100644 index 000000000000..afa34518999b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -0,0 +1,118 @@ +
+ + + + + + + + +
+ +
+ This item is in the Recycle Bin +
+ +
+
+ + + + +
+ +
+ +
+ + + + + +
+ +
+ + + + + + + +
+ + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less new file mode 100644 index 000000000000..1de962f7e11f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less @@ -0,0 +1,122 @@ +.umb-media-entry-editor { + + .umb-cropper-imageholder { + position: relative; + width: 100%; + height: 100%; + } + .umb-cropper-gravity { + height: 100%; + } + .umb-cropper__container { + width: 100%; + height: 100%; + } + .umb-cropper { + height: 100%; + } + .umb-cropper .crop-container { + padding-bottom: 0; + height: calc(100% - 50px) + } + .umb-cropper .crop-controls-wrapper { + justify-content: center; + } + .umb-cropper .crop-slider-wrapper { + max-width: 500px; + } +} + +.umb-media-entry-editor__pane { + display: flex; + flex-flow: row-reverse; + height: 100%; + width: 100%; +} + +.umb-media-entry-editor__crops { + background-color: white; + overflow: auto; + + > button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + text-align: center; + padding: 4px 10px 0 10px; + border-bottom: 1px solid @gray-9; + box-sizing: border-box; + height: 120px; + width: 120px; + color: @ui-active-type; + + &:hover { + color: @ui-active-type-hover; + text-decoration: none; + } + + &:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } + + &::before { + content: ""; + position: absolute; + width: 0px; + max-height: 50px; + height: (100% - 16px); + top: auto; + bottom: auto; + background-color: @ui-light-active-border; + left: 0; + border-radius: 0 3px 3px 0; + opacity: 0; + transition: all .2s linear; + } + + &.--is-active { + color: @ui-light-active-type; + + &::before { + opacity: 1; + width: 4px; + } + } + &.--is-defined { + + } + + > .__icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; + } + + > .__text { + font-size: 12px; + line-height: 1em; + margin-top: 4px; + } + } +} + +.umb-media-entry-editor__imagecropper { + flex: auto; + height: 100%; +} + +.umb-media-entry-editor__imageholder { + display: block; + position: relative; + height: calc(100% - 50px); +} +.umb-media-entry-editor__imageholder-actions { + background-color: white; + height: 50px; + display: flex; + justify-content: center; +} + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index fec2e632c5f2..029dedf214a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService) { + function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService, clipboardService) { var vm = this; @@ -19,6 +19,8 @@ angular.module("umbraco") vm.enterSubmitFolder = enterSubmitFolder; vm.focalPointChanged = focalPointChanged; vm.changePagination = changePagination; + vm.onNavigationChanged = onNavigationChanged; + vm.clickClearClipboard = clickClearClipboard; vm.clickHandler = clickHandler; vm.clickItemName = clickItemName; @@ -27,7 +29,10 @@ angular.module("umbraco") vm.selectLayout = selectLayout; vm.showMediaList = false; + vm.navigation = []; + var dialogOptions = $scope.model; + vm.clipboardItems = dialogOptions.clipboardItems; $scope.disableFolderSelect = (dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== "0") ? true : false; $scope.disableFocalPoint = (dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== "0") ? true : false; @@ -100,10 +105,32 @@ angular.module("umbraco") function setTitle() { if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMedia") + localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) .then(function (data) { - $scope.model.title = data; + $scope.model.title = data[0]; + + + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-umb-media", + "active": true, + "view": "" + }]; + + if(vm.clipboardItems) { + vm.navigation.push({ + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "", + "disabled": vm.clipboardItems.length === 0 + }); + } + + vm.activeTab = vm.navigation[0]; }); + } } @@ -149,7 +176,7 @@ angular.module("umbraco") .then(function (node) { $scope.target = node; // Moving directly to existing node's folder - gotoFolder({ id: node.parentId }).then(function() { + gotoFolder({ id: node.parentId }).then(function () { selectMedia(node); $scope.target.url = mediaHelper.resolveFileFromEntity(node); $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); @@ -169,10 +196,10 @@ angular.module("umbraco") function upload(v) { var fileSelect = $(".umb-file-dropzone .file-select"); - if (fileSelect.length === 0){ + if (fileSelect.length === 0) { localizationService.localize('media_uploadNotAllowed').then(function (message) { notificationsService.warning(message); }); } - else{ + else { fileSelect.trigger("click"); } } @@ -395,6 +422,19 @@ angular.module("umbraco") }); }; + function onNavigationChanged(tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + }; + + function clickClearClipboard() { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true; + vm.clipboardItems = []; + dialogOptions.clickClearClipboard(); + }; + var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { if (vm.searchOptions.filter) { @@ -504,13 +544,7 @@ angular.module("umbraco") var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; for (var i = 0; i < data.length; i++) { - if (data[i].metaData.MediaPath !== null) { - data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); - data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); - } - if (data[i].metaData.UpdateDate !== null){ - data[i].updateDate = data[i].metaData.UpdateDate; - } + setDefaultData(data[i]); data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; } @@ -523,6 +557,16 @@ angular.module("umbraco") }); } + function setDefaultData(item) { + if (item.metaData.MediaPath !== null) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.image = mediaHelper.resolveFileFromEntity(item, false); + } + if (item.metaData.UpdateDate !== null) { + item.updateDate = item.metaData.UpdateDate; + } + } + function preSelectMedia() { for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { var folderImage = $scope.images[folderIndex]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index df0c8e3cef72..d1f0699b1373 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -3,13 +3,15 @@ - +
@@ -19,21 +21,20 @@
@@ -49,20 +50,20 @@
-
+
+ layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
+ layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
@@ -86,31 +87,29 @@ + class="umb-breadcrumbs__add-ancestor" + ng-show="model.showFolderInput" + ng-model="model.newFolderName" + ng-keydown="enterSubmitFolder($event)" + ng-blur="vm.submitFolder()" + focus-when="{{model.showFolderInput}}" />
- + - - +
- @@ -145,11 +142,30 @@
- - - + + + +
+ + +
+ +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html index 933551bbff8d..af692f8322e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html @@ -1,25 +1,36 @@
-
- -
-
+
+ +
+
{{width}}px x {{height}}px
+
+
+
-
- +
+
+ -
- - -
+
+ + +
- + +
+
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html index edd840a47f3d..10aa6a774ab6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html @@ -1,12 +1,17 @@
- +
- + -
+
+
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less new file mode 100644 index 000000000000..f7e576433570 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less @@ -0,0 +1,137 @@ +.umb-media-card-grid { + /* Grid Setup */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + grid-auto-rows: minmax(100px, auto); + grid-gap: 10px; + + justify-items: center; + align-items: center; +} +.umb-media-card-grid__cell { + position: relative; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.umb-media-card-grid--inline-create-button { + position: absolute; + height: 100%; + z-index: 1; + opacity: 0; + outline: none; + left: 0; + width: 12px; + margin-left: -7px; + padding-left: 6px; + margin-right: -6px; + transition: opacity 240ms; + + &::before { + content: ''; + position: absolute; + background: @blueMid; + background: linear-gradient(0deg, rgba(@blueMid,0) 0%, rgba(@blueMid,1) 50%, rgba(@blueMid,0) 100%); + border-left: 1px solid white; + border-right: 1px solid white; + border-radius: 2px; + left: 0; + top: 0; + bottom: 0; + width: 2px; + animation: umb-media-card-grid--inline-create-button_before 400ms ease-in-out alternate infinite; + transform: scaleX(.99); + transition: transform 240ms ease-out; + + @keyframes umb-media-card-grid--inline-create-button_before { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + + > .__plus { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + box-sizing: border-box; + width: 28px; + height: 28px; + margin-left: -18px; + margin-top: -18px - 8px; + border-radius: 3em; + font-size: 14px; + border: 2px solid @blueMid; + color: @blueMid; + background-color: rgba(255, 255, 255, .96); + box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); + transform: scale(0); + transition: transform 240ms ease-in; + + animation: umb-media-card-grid--inline-create-button__plus 400ms ease-in-out alternate infinite; + + @keyframes umb-media-card-grid--inline-create-button__plus { + 0% { color: rgba(@blueMid, 1); } + 100% { color: rgba(@blueMid, 0.8); } + } + + } + + &:focus { + > .__plus { + border-color: @ui-outline; + } + } + + &:hover, &:focus { + opacity: 1; + + &::before { + transform: scaleX(1); + } + > .__plus { + transform: scale(1); + transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + + } + } +} + +.umb-media-card-grid__create-button { + position: relative; + width: 100%; + padding-bottom: 100%; + + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + box-sizing: border-box; + border-radius: @baseBorderRadius; + + > div { + position: absolute; + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } +} + +.umb-media-card-grid__create-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} + +.umb-media-card-grid__create-button.--disabled, +.umb-media-card-grid__create-button.--disabled:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html new file mode 100644 index 000000000000..01ce31415ee1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html @@ -0,0 +1,47 @@ + +
+ +
+ +

+ + +

+ +

+ + +

+ + + {{vm.media.name}} + + + + + + + + +
+ + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less new file mode 100644 index 000000000000..de3840b4d786 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less @@ -0,0 +1,186 @@ +.umb-media-card, +umb-media-card { + position: relative; + display: inline-block; + width: 100%; + //background-color: white; + border-radius: @baseBorderRadius; + //box-shadow: 0 1px 2px rgba(0,0,0,.2); + overflow: hidden; + + transition: box-shadow 120ms; + + cursor: pointer; + + .umb-outline(); + + &:hover { + box-shadow: 0 1px 3px rgba(@ui-action-type-hover, .5); + } + + &.--isOpen { + &::after { + content: ""; + position: absolute; + border: 2px solid @ui-active-border; + border-radius: @baseBorderRadius; + top:0; + bottom: 0; + left: 0; + right: 0; + } + } + + &.--hasError { + border: 2px solid @errorBackground; + } + + &.--sortable-placeholder { + &::after { + content: ""; + position: absolute; + background-color:rgba(@ui-drop-area-color, .05); + border: 2px solid rgba(@ui-drop-area-color, .1); + border-radius: @baseBorderRadius; + box-shadow: 0 0 4px rgba(@ui-drop-area-color, 0.05); + top:0; + bottom: 0; + left: 0; + right: 0; + animation: umb-block-card--sortable-placeholder 400ms ease-in-out alternate infinite; + @keyframes umb-block-card--sortable-placeholder { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + box-shadow: none; + } + + .__status { + + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 2px; + + &.--error { + background-color: @errorBackground; + color: @errorText; + } + } + + .__showcase { + position: relative; + max-width: 100%; + min-height: 120px; + max-height: 240px; + text-align: center; + //padding-bottom: 10/16*100%; + //background-color: @gray-12; + + img { + object-fit: contain; + max-height: 240px; + } + + umb-file-icon { + width: 100%; + padding-bottom: 100%; + display: block; + .umb-file-icon { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .__info { + position: absolute; + text-align: left; + bottom: 0; + width: 100%; + background-color: #fff; + padding-top: 6px; + padding-bottom: 7px;// 7 + 1 to compentiate for the -1 substraction in margin-bottom. + + opacity: 0; + transition: opacity 120ms; + + &.--error { + opacity: 1; + background-color: @errorBackground; + .__name, .__subname { + color: @errorText; + } + } + + .__name { + font-weight: bold; + font-size: 13px; + color: @ui-action-type; + margin-left: 16px; + margin-bottom: -1px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .__subname { + color: @gray-4; + font-size: 12px; + margin-left: 16px; + margin-top: 1px; + margin-bottom: -1px; + line-height: 1.5em; + } + } + + &:hover, &:focus, &:focus-within { + .__info { + opacity: 1; + } + .__info:not(.--error) { + .__name { + color: @ui-action-type-hover; + } + } + } + + .__actions { + position: absolute; + top: 10px; + right: 10px; + font-size: 0; + background-color: rgba(255, 255, 255, .96); + border-radius: 16px; + padding-left: 5px; + padding-right: 5px; + + opacity: 0; + transition: opacity 120ms; + .__action { + position: relative; + display: inline-block; + padding: 5px; + font-size: 18px; + + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + } + &:hover, &:focus, &:focus-within { + .__actions { + opacity: 1; + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js new file mode 100644 index 000000000000..24b20367aa79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js @@ -0,0 +1,97 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .component("umbMediaCard", { + templateUrl: "views/components/mediacard/umb-media-card.html", + controller: MediaCardController, + controllerAs: "vm", + transclude: true, + bindings: { + mediaKey: " { + if(newValue !== oldValue) { + vm.updateThumbnail(); + } + })); + + function checkErrorState() { + + vm.notAllowed = (vm.media &&vm.allowedTypes && vm.allowedTypes.length > 0 && vm.allowedTypes.indexOf(vm.media.metaData.ContentTypeAlias) === -1); + + if ( + vm.hasError === true || vm.notAllowed === true || (vm.media && vm.media.trashed === true) + ) { + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + } else { + $element.removeClass("--hasError") + vm.mediaCardForm.$setValidity('error', true) + } + } + + vm.$onInit = function () { + + unsubscribe.push($scope.$watchGroup(["vm.media.trashed", "vm.hasError"], checkErrorState)); + + vm.updateThumbnail(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaKey) { + vm.updateThumbnail(); + } + })); + } + + + vm.$onDestroy = function () { + unsubscribe.forEach(x => x()); + } + + vm.updateThumbnail = function () { + + if(vm.mediaKey && vm.mediaKey !== "") { + vm.loading = true; + + entityResource.getById(vm.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + checkErrorState(); + vm.thumbnail = mediaHelper.resolveFileFromEntity(mediaEntity, true); + + vm.loading = false; + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + }); + }); + } + + } + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index f41390bce3a7..975405626772 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -6,39 +6,42 @@ ng-click="clickItem(item, $event, $index)" ng-repeat="item in items | filter:filterBy" ng-style="item.flexStyle" - ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable}"> -
- -
- -
{{item.name}}
-
+ ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable, '-filtered': item.filtered}"> - -
+ +
+ +
{{item.name}}
+
- - {{item.name}} + +
- - {{item.name}} + +
- - {{item.name}} + + {{item.name}} + + + {{item.name}} + + + {{item.name}} + + + + - - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index 41e24a6cda74..fadc0ac3b1d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -6,9 +6,9 @@

Click to upload

- +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index f41f22a1a9c9..88d112e2d638 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -2,29 +2,29 @@ * @ngdoc controller * @name Umbraco.Editors.Media.EditController * @function - * + * * @description * The controller for the media editor */ -function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, - entityResource, navigationService, notificationsService, localizationService, - serverValidationManager, contentEditingHelper, fileManager, formHelper, +function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, + entityResource, navigationService, notificationsService, localizationService, + serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, eventsService) { - + var evts = []; var nodeId = null; var create = false; var infiniteMode = $scope.model && $scope.model.infiniteMode; - // when opening the editor through infinite editing get the + // when opening the editor through infinite editing get the // node id from the model instead of the route param if(infiniteMode && $scope.model.id) { nodeId = $scope.model.id; } else { nodeId = $routeParams.id; } - - // when opening the editor through infinite editing get the + + // when opening the editor through infinite editing get the // create option from the model instead of the route param if(infiniteMode) { create = $scope.model.create; @@ -72,22 +72,22 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } function init() { - + var content = $scope.content; - + // we need to check whether an app is present in the current data, if not we will present the default app. var isAppPresent = false; - + // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { - + // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) content.apps.forEach(app => { if (app === $scope.app) { isAppPresent = true; } }); - + // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { content.apps.forEach(app => { @@ -98,9 +98,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } }); } - + } - + // if we still dont have a app, lets show the first one: if (isAppPresent === false) { content.apps[0].active = true; @@ -108,16 +108,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } editorState.set($scope.content); - + bindEvents(); } - + function bindEvents() { //bindEvents can be called more than once and we don't want to have multiple bound events for (var e in evts) { eventsService.unsubscribe(evts[e]); } - + evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { // if this media item uses the updated media type we need to reload the media item if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { @@ -131,7 +131,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat })); } $scope.page.submitButtonLabelKey = "buttons_save"; - + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -149,7 +149,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat //it's a child item, just sync the ui node to the parent navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), @@ -176,7 +176,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.save = function () { if (formHelper.submitForm({ scope: $scope })) { - + $scope.page.saveButtonState = "busy"; mediaResource.save($scope.content, create, fileManager.getFiles()) @@ -200,12 +200,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat editorState.set($scope.content); syncTreeNode($scope.content, data.path); - + $scope.page.saveButtonState = "success"; init(); } + eventsService.emit("editors.media.saved", {media: data}); + + return data; + }, function(err) { formHelper.resetForm({ scope: $scope, hasErrors: true }); @@ -213,16 +217,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); - + editorState.set($scope.content); $scope.page.saveButtonState = "error"; }); } else { - showValidationNotification(); + showValidationNotification(); } - + }; function loadMedia() { @@ -231,7 +235,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat .then(function (data) { $scope.content = data; - + if (data.isChildOfListView && data.trashed === false) { $scope.page.listViewPath = ($routeParams.page) ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page @@ -247,9 +251,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat serverValidationManager.notifyAndClearAllSubscriptions(); if(!infiniteMode) { - syncTreeNode($scope.content, data.path, true); + syncTreeNode($scope.content, data.path, true); } - + if ($scope.content.parentId && $scope.content.parentId !== -1 && $scope.content.parentId !== -21) { //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors(nodeId, "media") @@ -279,7 +283,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.appChanged = function (app) { $scope.app = app; - + // setup infinite mode if(infiniteMode) { $scope.page.submitButtonLabelKey = "buttons_saveAndClose"; @@ -296,7 +300,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $location.path($scope.page.listViewPath.split("?")[0]); } }; - + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html index d9d8cad9821b..6e67c947936f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html @@ -4,6 +4,7 @@ type="number" ng-model="model.value.min" placeholder="0" + min="0" ng-max="model.value.max" fix-number /> @@ -11,7 +12,7 @@ type="number" ng-model="model.value.max" placeholder="∞" - ng-min="model.value.min" + ng-min="model.value.min || 0" fix-number /> diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index dcc9add395d8..d02e626bfa2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -99,6 +99,11 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe eventsService.unsubscribe(evts[e]); } }); + + if ($scope.model.config.itemType) { + currentItemType = $scope.model.config.itemType; + init(); + } } angular.module('umbraco').controller("Umbraco.PrevalueEditors.TreeSourceTypePickerController", TreeSourceTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index 019a772fddfb..66ef23c7440f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -10,7 +10,7 @@ .umb-block-list__wrapper { position: relative; - max-width: 1024px; + .umb-property-editor--limit-width(); > .ui-sortable > .ui-sortable-helper > .umb-block-list__block > .umb-block-list__block--content > * { box-shadow: 0px 5px 10px 0 rgba(0,0,0,.2); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index c485f4bbc6e9..4f1016e68028 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -11,11 +11,14 @@ * */ function fileUploadController($scope, fileManager) { - + $scope.fileChanged = onFileChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + + $scope.fileExtensionsString = $scope.model.config.fileExtensions ? $scope.model.config.fileExtensions.map(x => "."+x.value).join(",") : ""; + /** * Called when the file selection value changes * @param {any} value @@ -38,12 +41,12 @@ files: [] }); } - + }; angular.module("umbraco") .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function (mediaHelper, umbRequestHelper, assetsService) { + .run(function (mediaHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index 522278e99ec4..36509e894796 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -4,6 +4,7 @@ property-alias="{{model.alias}}" value="model.value" required="model.validation.mandatory" - on-files-selected="fileChanged(value)"> + on-files-selected="fileChanged(value)" + accept-file-ext="fileExtensionsString">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 4df8f7e596d2..e9d9950bdd61 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -1,6 +1,6 @@ angular.module('umbraco') .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($scope, fileManager, $timeout) { + function ($scope, fileManager, $timeout, mediaHelper) { var config = Utilities.copy($scope.model.config); @@ -18,6 +18,8 @@ angular.module('umbraco') //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + $scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); /** * Called when the umgImageGravity component updates the focal point value * @param {any} left @@ -150,7 +152,7 @@ angular.module('umbraco') // we have a crop open already - close the crop (this will discard any changes made) close(); - // the crop editor needs a digest cycle to close down properly, otherwise its state + // the crop editor needs a digest cycle to close down properly, otherwise its state // is reused for the new crop... and that's really bad $timeout(function () { crop(targetCrop); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 241d61660eae..9dc1a3b91ad9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -13,11 +13,12 @@ on-files-selected="filesSelected(value, files)" on-files-changed="filesChanged(files)" on-init="fileUploaderInit(value, files)" - hide-selection="true"> + hide-selection="true" + accept-file-ext="acceptFileExt">
-
+
@@ -25,7 +26,6 @@ width="{{currentCrop.width}}" crop="currentCrop.coordinates" center="model.value.focalPoint" - max-size="450" src="imageSrc">
@@ -49,7 +49,7 @@
- +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index ca46f30bb7ed..c6320a7cf2b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService) { + function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService, clipboardService) { var vm = this; @@ -10,6 +10,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl vm.add = add; vm.remove = remove; + vm.copyItem = copyItem; vm.editItem = editItem; vm.showAdd = showAdd; @@ -53,7 +54,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() // compares and be completely sure it works. var found = medias.find(m => m.udi.toString() === id.toString() || m.id.toString() === id.toString()); - + var mediaItem = found || { name: vm.labels.deletedItem, @@ -67,33 +68,36 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl return mediaItem; }); - medias.forEach(media => { - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } + medias.forEach(media => appendMedia(media)); - // if there is no thumbnail, try getting one if the media is not a placeholder item - if (!media.thumbnail && media.id && media.metaData) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } + sync(); + }); + } + } - vm.mediaItems.push(media); + function appendMedia(media) { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } - if ($scope.model.config.idType === "udi") { - selectedIds.push(media.udi); - } else { - selectedIds.push(media.id); - } - }); + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } - sync(); - }); + vm.mediaItems.push(media); + + if ($scope.model.config.idType === "udi") { + selectedIds.push(media.udi); + } else { + selectedIds.push(media.id); } } function sync() { $scope.model.value = selectedIds.join(); removeAllEntriesAction.isDisabled = selectedIds.length === 0; + copyAllEntriesAction.isDisabled = removeAllEntriesAction.isDisabled; } function setDirty() { @@ -103,9 +107,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } function reloadUpdatedMediaItems(updatedMediaNodes) { - // because the images can be edited through the media picker we need to + // because the images can be edited through the media picker we need to // reload. We only reload the images that is already picked but has been updated. - // We have to get the entities from the server because the media + // We have to get the entities from the server because the media // can be edited without being selected vm.mediaItems.forEach(media => { if (updatedMediaNodes.indexOf(media.udi) !== -1) { @@ -129,7 +133,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl ]; localizationService.localizeMany(labelKeys) - .then(function(data) { + .then(function (data) { vm.labels.deletedItem = data[0]; vm.labels.trashed = data[1]; @@ -143,7 +147,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl else { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - } + } } // only allow users to add and edit media if they have access to the media section @@ -163,6 +167,50 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl setDirty(); } + function copyAllEntries() { + if($scope.mediaItems.length > 0) { + + // gather aliases + var aliases = $scope.mediaItems.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var data = $scope.mediaItems.map(mediaEntity => { return {"mediaKey": mediaEntity.key }}); + + localizationService.localize("clipboard_labelForArrayOfItems", [$scope.model.label]).then(function(localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, data, localizedLabel, "icon-thumbnail-list", $scope.model.id); + }); + } + } + + function copyItem(mediaItem) { + + var mediaEntry = {}; + mediaEntry.mediaKey = mediaItem.key; + + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaItem.metaData.ContentTypeAlias, mediaEntry, mediaItem.name, mediaItem.icon, mediaItem.udi); + } + + function pasteFromClipboard(pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + entityResource.getById(pasteEntry.mediaKey, "Media").then(function (mediaEntity) { + + if(disableFolderSelect === true && mediaEntity.metaData.ContentTypeAlias === "Folder") { + return; + } + + appendMedia(mediaEntity); + sync(); + }); + } + function editItem(item) { var mediaEditor = { id: item.id, @@ -174,7 +222,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if (model && model.mediaNode) { entityResource.getById(model.mediaNode.id, "Media") .then(function (mediaEntity) { - // if an image is selecting more than once + // if an image is selecting more than once // we need to update all the media items vm.mediaItems.forEach(media => { if (media.id === model.mediaNode.id) { @@ -200,6 +248,22 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, + clickPasteItem: function(item, mouseEvent) { + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (pasteFromClipboard(entry, item.type)) { + indexIncrementor++; + } + }); + } else { + pasteFromClipboard(item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + } + setDirty(); + }, submit: function (model) { editorService.close(); @@ -231,6 +295,21 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } } + + var allowedTypes = null; + if(onlyImages) { + allowedTypes = ["Image"]; // Media Type Image Alias. + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, allowedTypes); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + editorService.mediaPicker(mediaPicker); } @@ -262,6 +341,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }); } + var copyAllEntriesAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: ['Media'], + icon: "documents", + method: copyAllEntries, + isDisabled: true + } + var removeAllEntriesAction = { labelKey: 'clipboard_labelForRemoveAllEntries', labelTokens: [], @@ -269,9 +356,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl method: removeAllEntries, isDisabled: true }; - + if (multiPicker === true) { var propertyActions = [ + copyAllEntriesAction, removeAllEntriesAction ]; @@ -289,12 +377,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl cancel: ".unsortable", update: function () { setDirty(); - $timeout(function() { + $timeout(function () { // TODO: Instead of doing this with a timeout would be better to use a watch like we do in the // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. selectedIds = vm.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); - + sync(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html new file mode 100644 index 000000000000..5e67aafe3e0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js new file mode 100644 index 000000000000..922370a032ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js @@ -0,0 +1,110 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropConfigurationController", + function ($scope) { + + var unsubscribe = []; + + if (!$scope.model.value) { + $scope.model.value = []; + } + + $scope.setFocus = false; + + $scope.remove = function (crop, evt) { + evt.preventDefault(); + const i = $scope.model.value.indexOf(crop); + if (i > -1) { + $scope.model.value.splice(i, 1); + } + }; + + $scope.edit = function (crop, evt) { + evt.preventDefault(); + crop.editMode = true; + }; + + $scope.addNewCrop = function (evt) { + evt.preventDefault(); + + var crop = {}; + crop.editMode = true; + + $scope.model.value.push(crop); + $scope.validate(crop); + } + $scope.setChanges = function (crop) { + $scope.validate(crop); + if( + crop.hasWidthError !== true && + crop.hasHeightError !== true && + crop.hasAliasError !== true + ) { + crop.editMode = false; + window.dispatchEvent(new Event('resize.umbImageGravity')); + } + }; + $scope.useForAlias = function (crop) { + if (crop.alias == null || crop.alias === "") { + crop.alias = (crop.label || "").toCamelCase(); + } + }; + $scope.validate = function(crop) { + $scope.validateWidth(crop); + $scope.validateHeight(crop); + $scope.validateAlias(crop); + } + $scope.validateWidth = function (crop) { + crop.hasWidthError = !(Utilities.isNumber(crop.width) && crop.width > 0); + }; + $scope.validateHeight = function (crop) { + crop.hasHeightError = !(Utilities.isNumber(crop.height) && crop.height > 0); + }; + $scope.validateAlias = function (crop, $event) { + var exists = $scope.model.value.find( x => crop !== x && crop.alias === x.alias); + if (exists !== undefined || crop.alias === "") { + // alias is not valid + crop.hasAliasError = true; + } else { + // everything was good: + crop.hasAliasError = false; + } + + }; + + $scope.confirmChanges = function (crop, event) { + if (event.keyCode == 13) { + $scope.setChanges(crop, event); + event.preventDefault(); + } + }; + $scope.focusNextField = function (event) { + if (event.keyCode == 13) { + + var el = event.target; + + var inputs = Array.from(document.querySelectorAll("input:not(disabled)")); + var inputIndex = inputs.indexOf(el); + if (inputIndex > -1) { + var nextIndex = inputs.indexOf(el) +1; + + if(inputs.length > nextIndex) { + inputs[nextIndex].focus(); + event.preventDefault(); + } + } + } + }; + + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + tolerance: 'pointer' + }; + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html new file mode 100644 index 000000000000..46b9ddb15f64 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html @@ -0,0 +1,96 @@ +
    + +
    +
    +
    +
    + Label +
    +
    + Alias +
    +
    + Width +
    +
    + Height +
    +
    + Actions +
    +
    +
    +
    + +
    + +
    {{crop.label}}
    +
    {{crop.alias}}
    +
    {{crop.width}}px
    +
    {{crop.height}}px
    +
    + + +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + +
    + +
    +
    +
    + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less new file mode 100644 index 000000000000..5f5a2d468979 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less @@ -0,0 +1,40 @@ +.umb-mediapicker3-crops { + + input.ng-invalid.ng-touched { + border-color:@formErrorBorder; + color:@formErrorBorder + } + + .umb-table button { + position: relative; + color: @ui-action-discreet-type; + margin-right: 10px; + font-size: 14px; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + +} + +.umb-mediapicker3-crops__add { + + margin-top:10px; + + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + padding: 5px 15px; + box-sizing: border-box; + width: 100%; +} + +.umb-mediapicker3-crops__add:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html new file mode 100644 index 000000000000..aa9f50b7df17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -0,0 +1,71 @@ +
    + + + +
    + +
    + +
    + + + + +
    + + +
    +
    + +
    +
    + + + +
    + + + + +
    +
    + Minimum %0% entries, needs %1% more. +
    + > +
    +
    +
    + Maximum %0% entries, %1% too many. +
    + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less new file mode 100644 index 000000000000..d02c0b055c90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less @@ -0,0 +1,13 @@ +.umb-mediapicker3 { + + .umb-media-card-grid { + padding: 20px; + border: 1px solid @inputBorder; + box-sizing: border-box; + .umb-property-editor--limit-width(); + + &.--singleMode { + max-width: 202px; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js new file mode 100644 index 000000000000..675381d46e3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -0,0 +1,431 @@ +(function () { + "use strict"; + + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbMediaPicker3PropertyEditor + * @function + * + * @description + * The component for the Media Picker property editor. + */ + angular + .module("umbraco") + .component("umbMediaPicker3PropertyEditor", { + templateUrl: "views/propertyeditors/MediaPicker3/umb-media-picker3-property-editor.html", + controller: MediaPicker3Controller, + controllerAs: "vm", + bindings: { + model: "=" + }, + require: { + propertyForm: "^form", + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' + } + }); + + function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource) { + + var unsubscribe = []; + + // Property actions: + var copyAllMediasAction = null; + var removeAllMediasAction = null; + + var vm = this; + + vm.loading = true; + + vm.supportCopy = clipboardService.isSupported(); + + + vm.labels = {}; + + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { + vm.labels.grid_addElement = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + + vm.$onInit = function() { + + vm.validationLimit = vm.model.config.validationLimit || {}; + // If single-mode we only allow 1 item as the maximum: + if(vm.model.config.multiple === false) { + vm.validationLimit.max = 1; + } + vm.model.config.crops = vm.model.config.crops || []; + vm.singleMode = vm.validationLimit.max === 1; + vm.allowedTypes = vm.model.config.filter ? vm.model.config.filter.split(",") : null; + + copyAllMediasAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "documents", + method: requestCopyAllMedias, + isDisabled: true + }; + + removeAllMediasAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestRemoveAllMedia, + isDisabled: true + }; + + var propertyActions = []; + if(vm.supportCopy) { + propertyActions.push(copyAllMediasAction); + } + propertyActions.push(removeAllMediasAction); + + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + + if(vm.model.value === null || !Array.isArray(vm.model.value)) { + vm.model.value = []; + } + + vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + + userService.getCurrentUser().then(function (userData) { + + if (!vm.model.config.startNodeId) { + if (vm.model.config.ignoreUserStartNodes === true) { + vm.model.config.startNodeId = -1; + vm.model.config.startNodeIsVirtual = true; + } else { + vm.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + vm.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + } + + // only allow users to add and edit media if they have access to the media section + var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1; + vm.allowEdit = hasAccessToMedia; + vm.allowAdd = hasAccessToMedia; + + vm.loading = false; + }); + + }; + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + + vm.addMediaAt = addMediaAt; + function addMediaAt(createIndex, $event) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: vm.singleMode !== true, + clickPasteItem: function(item, mouseEvent) { + + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + mediaPicker.close(); + } + }, + submit: function (model) { + editorService.close(); + + var indexIncrementor = 0; + model.selection.forEach((entry) => { + var mediaEntry = {}; + mediaEntry.key = String.CreateGuid(); + mediaEntry.mediaKey = entry.key; + updateMediaEntryData(mediaEntry); + vm.model.value.splice(createIndex + indexIncrementor, 0, mediaEntry); + indexIncrementor++; + }); + + setDirty(); + }, + close: function () { + editorService.close(); + } + } + + if(vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + + editorService.mediaPicker(mediaPicker); + } + + // To be used by infinite editor. (defined here cause we need configuration from property editor) + function changeMediaFor(mediaEntry, onSuccess) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: false, + submit: function (model) { + editorService.close(); + + model.selection.forEach((entry) => {// only one. + mediaEntry.mediaKey = entry.key; + }); + + // reset focal and crops: + mediaEntry.crops = null; + mediaEntry.focalPoint = null; + updateMediaEntryData(mediaEntry); + + if(onSuccess) { + onSuccess(); + } + }, + close: function () { + editorService.close(); + } + } + + if(vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + editorService.mediaPicker(mediaPicker); + } + + function resetCrop(cropEntry) { + Object.assign(cropEntry, vm.model.config.crops.find( c => c.alias === cropEntry.alias)); + cropEntry.coordinates = null; + setDirty(); + } + + function updateMediaEntryData(mediaEntry) { + + mediaEntry.crops = mediaEntry.crops || []; + mediaEntry.focalPoint = mediaEntry.focalPoint || { + left: 0.5, + top: 0.5 + }; + + // Copy config and only transfer coordinates. + var newCrops = Utilities.copy(vm.model.config.crops); + newCrops.forEach(crop => { + var oldCrop = mediaEntry.crops.filter(x => x.alias === crop.alias).shift(); + if (oldCrop && oldCrop.height === crop.height && oldCrop.width === crop.width) { + crop.coordinates = oldCrop.coordinates; + } + }); + mediaEntry.crops = newCrops; + + } + + vm.removeMedia = removeMedia; + function removeMedia(media) { + var index = vm.model.value.indexOf(media); + if(index !== -1) { + vm.model.value.splice(index, 1); + } + } + function deleteAllMedias() { + vm.model.value = []; + } + + vm.activeMediaEntry = null; + function setActiveMedia(mediaEntryOrNull) { + vm.activeMediaEntry = mediaEntryOrNull; + } + + vm.editMedia = editMedia; + function editMedia(mediaEntry, options, $event) { + + if($event) + $event.stopPropagation(); + + options = options || {}; + + setActiveMedia(mediaEntry); + + var documentInfo = getDocumentNameAndIcon(); + + // make a clone to avoid editing model directly. + var mediaEntryClone = Utilities.copy(mediaEntry); + + var mediaEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + createFlow: options.createFlow === true, + documentName: documentInfo.name, + mediaEntry: mediaEntryClone, + propertyEditor: { + changeMediaFor: changeMediaFor, + resetCrop: resetCrop + }, + enableFocalPointSetter: vm.model.config.enableLocalFocalPoint || false, + view: "views/common/infiniteeditors/mediaEntryEditor/mediaEntryEditor.html", + size: "large", + submit: function(model) { + vm.model.value[vm.model.value.indexOf(mediaEntry)] = mediaEntryClone; + setActiveMedia(null) + editorService.close(); + }, + close: function(model) { + if(model.createFlow === true) { + // This means that the user cancelled the creation and we should remove the media item. + // TODO: remove new media item. + } + setActiveMedia(null) + editorService.close(); + } + }; + + // open property settings editor + editorService.open(mediaEditorModel); + } + + var getDocumentNameAndIcon = function() { + // get node name + var contentNodeName = "?"; + var contentNodeIcon = null; + if(vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + if(vm.umbVariantContentEditors) { + contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0]; + } else if (vm.umbElementEditorContent) { + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name; + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + + return { + name: contentNodeName, + icon: contentNodeIcon + } + } + + var requestCopyAllMedias = function() { + var mediaKeys = vm.model.value.map(x => x.mediaKey) + entityResource.getByIds(mediaKeys, "Media").then(function (entities) { + + // gather aliases + var aliases = entities.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var documentInfo = getDocumentNameAndIcon(); + + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function(localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || "icon-thumbnail-list", vm.model.id); + }); + }); + } + + vm.copyMedia = copyMedia; + function copyMedia(mediaEntry) { + entityResource.getById(mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key); + }); + } + function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return false; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + pasteEntry.key = String.CreateGuid(); + updateMediaEntryData(pasteEntry); + vm.model.value.splice(createIndex, 0, pasteEntry); + + + return true; + + } + + function requestRemoveAllMedia() { + localizationService.localizeMany(["mediaPicker_confirmRemoveAllMediaEntryMessage", "general_remove"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteAllMedias(); + overlayService.close(); + } + }); + }); + } + + + vm.sortableOptions = { + cursor: "grabbing", + handle: "umb-media-card", + cancel: "input,textarea,select,option", + classes: ".umb-media-card--dragging", + distance: 5, + tolerance: "pointer", + scroll: true, + update: function (ev, ui) { + setDirty(); + } + }; + + + function onAmountOfMediaChanged() { + + // enable/disable property actions + if (copyAllMediasAction) { + copyAllMediasAction.isDisabled = vm.model.value.length === 0; + } + if (removeAllMediasAction) { + removeAllMediasAction.isDisabled = vm.model.value.length === 0; + } + + // validate limits: + if (vm.propertyForm && vm.validationLimit) { + + var isMinRequirementGood = vm.validationLimit.min === null || vm.model.value.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); + + var isMaxRequirementGood = vm.validationLimit.max === null || vm.model.value.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); + } + } + + unsubscribe.push($scope.$watch(() => vm.model.value.length, onAmountOfMediaChanged)); + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js new file mode 100644 index 000000000000..b561784d9f06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .controller("Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController", + function Controller($scope) { + + var vm = this; + vm.plusPosY = 0; + + vm.onMouseMove = function($event) { + vm.plusPosY = $event.offsetY; + } + + }); + +})(); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 737181c66821..4abcdf8a40bf 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1100,6 +1100,17 @@ Mange hilsner fra Umbraco robotten Du har valgt et medie som er slettet eller lagt i papirkurven Du har valgt medier som er slettede eller lagt i papirkurven Slettet + Åben i mediebiblioteket + Skift medie + Nulstil medie beskæring + Rediger %0% på %1% + Annuller indsættelse? + + Du har foretaget ændringer til bruge af dette media. Er du sikker på at du vil annullere? + Fjern? + Fjern brugen af alle medier? + Udklipsholder + Ikke tilladt indtast eksternt link @@ -1845,6 +1856,7 @@ Mange hilsner fra Umbraco robotten Kopier %0% %0% fra %1% + Samling af %0% Fjern alle elementer Ryd udklipsholder diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 3f6c985a0fe5..cbb6902d744b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1353,6 +1353,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2377,6 +2388,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 87b58e506364..590a24839347 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1363,6 +1363,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2396,6 +2407,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 8d13ccd4d79d..7160a87351ab 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -704,7 +704,32 @@ public async Task PostAddFile() if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect) { - if (Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) + var mediaTypes = Services.MediaTypeService.GetAll(); + // Look up MediaTypes + foreach (var mediaTypeItem in mediaTypes) + { + var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile"); + if (fileProperty != null) { + var dataTypeKey = fileProperty.DataTypeKey; + var dataType = Services.DataTypeService.GetDataType(dataTypeKey); + + if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) { + var fileExtensions = fileExtensionsConfig.FileExtensions; + if (fileExtensions != null) + { + if (fileExtensions.Where(x => x.Value == ext).Count() != 0) + { + mediaType = mediaTypeItem.Alias; + break; + } + } + } + } + + } + + // If media type is still File then let's check if it's an image. + if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) { mediaType = Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index f39b267e18b0..766cb1e99f91 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -28,6 +28,11 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAli return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue) + { + return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + } + /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// @@ -375,5 +380,11 @@ public static string GetCropUrl( return imageUrlGenerator.GetImageUrl(options); } + + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue) + { + return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue); + + } } } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index dad2f9e3f335..51845946f133 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,7 +1,5 @@ using System; -using Newtonsoft.Json.Linq; using System.Globalization; -using System.Text; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Composing; @@ -32,6 +30,8 @@ public static class ImageCropperTemplateExtensions /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator, imageCropperValue); + /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// @@ -118,6 +118,13 @@ public static string GetCropUrl( ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, + string alias, + string cacheBusterValue = null) + => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue); + + + /// /// Gets the ImageProcessor URL from the image path. /// diff --git a/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs new file mode 100644 index 000000000000..859b3b35ebe6 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public class FileExtensionConfigItem : IFileExtensionConfigItem + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs new file mode 100644 index 000000000000..55f947797a26 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the file upload address value editor. + /// + public class FileUploadConfiguration : IFileExtensionsConfig + { + [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] + public List FileExtensions { get; set; } = new List(); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs new file mode 100644 index 000000000000..abbd19a79315 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the file upload value editor. + /// + public class FileUploadConfigurationEditor : ConfigurationEditor + { + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 052af18aa10a..a105d490be26 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -32,6 +32,10 @@ public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem _uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection); } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor(); + /// /// Creates the corresponding property value editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs new file mode 100644 index 000000000000..c4934540c793 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Marker interface for any editor configuration that supports defining file extensions + /// + public interface IFileExtensionsConfig + { + List FileExtensions { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs new file mode 100644 index 000000000000..682e8815659a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public interface IFileExtensionConfigItem + { + int Id { get; set; } + + string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs new file mode 100644 index 000000000000..4c3c6564a5da --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the media picker value editor. + /// + public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig + { + [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", + Description = "Limit to specific types")] + public string Filter { get; set; } + + [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] + public bool Multiple { get; set; } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); + + public class NumberRange + { + [JsonProperty("min")] + public int? Min { get; set; } + + [JsonProperty("max")] + public int? Max { get; set; } + } + + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi StartNodeId { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } + + [ConfigurationField("crops", "Image Crops", "views/propertyeditors/MediaPicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] + public CropConfiguration[] Crops { get; set; } + + public class CropConfiguration + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs new file mode 100644 index 000000000000..37063aa1536f --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the media picker value editor. + /// + public class MediaPicker3ConfigurationEditor : ConfigurationEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3ConfigurationEditor() + { + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + + Field(nameof(MediaPicker3Configuration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + Field(nameof(MediaPicker3Configuration.Filter)) + .Config = new Dictionary { { "itemType", "media" } }; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs new file mode 100644 index 000000000000..526b4830c8cd --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents a media picker property editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MediaPicker3, + EditorType.PropertyValue, + "Media Picker v3", + "mediapicker3", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Media, + Icon = Constants.Icons.MediaImage)] + public class MediaPicker3PropertyEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3PropertyEditor(ILogger logger) + : base(logger) + { + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MediaPicker3PropertyValueEditor(Attribute); + + internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference + { + /// + /// Note: no FromEditor() and ToEditor() methods + /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string + /// + public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (rawJson.IsNullOrWhiteSpace()) + yield break; + + var mediaWithCropsDtos = JsonConvert.DeserializeObject(rawJson); + + foreach (var mediaWithCropsDto in mediaWithCropsDtos) + { + yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey)); + } + } + + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs new file mode 100644 index 000000000000..f9b2ad75e169 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -0,0 +1,119 @@ +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase + { + + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public MediaPickerWithCropsValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + + /// + /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3 + /// + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); + + /// + /// Check if the raw JSON value is not an empty array + /// + public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; + + /// + /// What C# model type does the raw JSON return for Models & Views + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + // Check do we want to return IPublishedContent collection still or a NEW model ? + var isMultiple = IsMultipleDataType(propertyType.DataType); + return isMultiple + ? typeof(IEnumerable) + : typeof(MediaWithCrops); + } + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var mediaItems = new List(); + var isMultiple = IsMultipleDataType(propertyType.DataType); + if (inter == null) + { + return isMultiple ? mediaItems: null; + } + + var dtos = JsonConvert.DeserializeObject>(inter.ToString()); + + foreach(var media in dtos) + { + var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey); + if (item != null) + { + mediaItems.Add(new MediaWithCrops + { + MediaItem = item, + LocalCrops = new ImageCropperValue + { + Crops = media.Crops, + FocalPoint = media.FocalPoint, + Src = item.Url() + } + }); + } + } + + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + } + + /// + /// Is the media picker configured to pick multiple media items + /// + /// + /// + private bool IsMultipleDataType(PublishedDataType dataType) + { + var config = dataType.ConfigurationAs(); + return config.Multiple; + } + + private object FirstOrDefault(IList mediaItems) + { + return mediaItems.Count == 0 ? null : mediaItems[0]; + } + + + /// + /// Model/DTO that represents the JSON that the MediaPicker3 stores + /// + [DataContract] + internal class MediaWithCropsDto + { + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "mediaKey")] + public Guid MediaKey { get; set; } + + [DataMember(Name = "crops")] + public IEnumerable Crops { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6cbefa825e5..ff988cf5bf7d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -254,9 +254,17 @@ + + + + + + + + @@ -266,6 +274,7 @@ + diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 0f5b0557f4a1..592c88945bae 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -262,6 +262,32 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + ImageCropperValue imageCropperValue, + string cropAlias, + int? width = null, + int? height = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = true, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + if (imageCropperValue == null) return EmptyHtmlString; + + var imageUrl = imageCropperValue.Src; + var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + #endregion ///