-
+
-
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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: "",
+ icon: "",
+ name: "",
+ hasError: "<",
+ allowedTypes: "",
+ onNameClicked: "&?"
+ }
+ });
+
+ function MediaCardController($scope, $element, entityResource, mediaHelper, eventsService, localizationService) {
+
+ var unsubscribe = [];
+ var vm = this;
+ vm.paddingBottom = 100;// Square while loading.
+ vm.loading = false;
+
+ unsubscribe.push($scope.$watch("vm.mediaKey", (newValue, oldValue) => {
+ 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}">
-
-
-
+ ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable, '-filtered': item.filtered}">
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
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 @@
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 @@
+
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 @@
+
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
///