Better crop mechanism for the grid image editor (#8023)

This commit is contained in:
Lars-Erik Aabech
2020-07-24 16:47:09 +02:00
committed by GitHub
parent 5d5c912d4d
commit e8bb3b01aa
10 changed files with 452 additions and 325 deletions

View File

@@ -5,38 +5,38 @@
* @function
**/
angular.module("umbraco.directives")
.directive('umbImageCrop',
function ($timeout, cropperHelper) {
return {
restrict: 'E',
replace: true,
templateUrl: 'views/components/imaging/umb-image-crop.html',
scope: {
src: '=',
width: '@',
height: '@',
crop: "=",
center: "=",
maxSize: '@'
},
.directive('umbImageCrop',
function ($timeout, cropperHelper) {
return {
restrict: 'E',
replace: true,
templateUrl: 'views/components/imaging/umb-image-crop.html',
scope: {
src: '=',
width: '@',
height: '@',
crop: "=",
center: "=",
maxSize: '@'
},
link: function(scope, element, attrs) {
link: function (scope, element, attrs) {
let sliderRef = null;
scope.width = 400;
scope.height = 320;
scope.width = 400;
scope.height = 320;
scope.dimensions = {
image: {},
cropper:{},
viewport:{},
margin: 20,
scale: {
min: 0,
max: 3,
current: 1
}
scope.dimensions = {
image: {},
cropper: {},
viewport: {},
margin: 20,
scale: {
min: 0,
max: 3,
current: 1
}
};
scope.sliderOptions = {
@@ -84,211 +84,232 @@ angular.module("umbraco.directives")
}
};
//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'
};
};
//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'
};
};
//elements
var $viewport = element.find(".viewport");
var $image = element.find("img");
var $overlay = element.find(".overlay");
var $container = element.find(".crop-container");
//elements
var $viewport = element.find(".viewport");
var $image = element.find("img");
var $overlay = element.find(".overlay");
var $container = element.find(".crop-container");
//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} };
scope.constraints = constraints;
//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 } };
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;
};
//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;
};
var setDimensions = function(originalImage){
originalImage.width("auto");
originalImage.height("auto");
var setDimensions = function (originalImage) {
originalImage.width("auto");
originalImage.height("auto");
var image = {};
image.originalWidth = originalImage.width();
image.originalHeight = originalImage.height();
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;
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 = image;
//unscaled editor size
//var viewPortW = $viewport.width();
//var viewPortH = $viewport.height();
var _viewPortW = parseInt(scope.width, 10);
var _viewPortH = parseInt(scope.height, 10);
//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);
//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;
}
//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;
};
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;
};
//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;
//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;
setConstraints();
validatePosition(scope.dimensions.image.left, scope.dimensions.image.top);
};
setConstraints();
validatePosition(scope.dimensions.image.left, scope.dimensions.image.top);
};
//resize the image to a predefined crop coordinate
var resizeImageToCrop = function(){
scope.dimensions.image = cropperHelper.convertToStyle(
scope.crop,
{width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight},
scope.dimensions.cropper,
scope.dimensions.margin);
//resize the image to a predefined crop coordinate
var resizeImageToCrop = function () {
scope.dimensions.image = cropperHelper.convertToStyle(
scope.crop,
{ width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight },
scope.dimensions.cropper,
scope.dimensions.margin);
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
scope.dimensions.image.originalWidth,
scope.dimensions.image.originalHeight,
scope.dimensions.cropper.width,
scope.dimensions.cropper.height,
true);
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
scope.dimensions.image.originalWidth,
scope.dimensions.image.originalHeight,
scope.dimensions.cropper.width,
scope.dimensions.cropper.height,
true);
scope.dimensions.scale.current = scope.dimensions.image.ratio;
scope.dimensions.scale.current = scope.dimensions.image.ratio;
// Update min and max based on original width/height
scope.dimensions.scale.min = ratioCalculation.ratio;
// Update min and max based on original width/height
scope.dimensions.scale.min = ratioCalculation.ratio;
scope.dimensions.scale.max = 2;
};
};
var validatePosition = function(left, top){
if(left > constraints.left.max)
{
left = constraints.left.max;
}
var validatePosition = function (left, top) {
if (left > constraints.left.max) {
left = constraints.left.max;
}
if(left <= constraints.left.min){
left = constraints.left.min;
}
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;
}
if (top > constraints.top.max) {
top = constraints.top.max;
}
if (top <= constraints.top.min) {
top = constraints.top.min;
}
if(scope.dimensions.image.left !== left){
scope.dimensions.image.left = left;
}
if (scope.dimensions.image.left !== left) {
scope.dimensions.image.left = left;
}
if(scope.dimensions.image.top !== top){
scope.dimensions.image.top = top;
}
};
if (scope.dimensions.image.top !== top) {
scope.dimensions.image.top = top;
}
};
//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);
};
//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);
};
//Drag and drop positioning, using jquery ui draggable
var onStartDragPosition, top, left;
$overlay.draggable({
drag: function(event, ui) {
scope.$apply(function(){
validatePosition(ui.position.left, ui.position.top);
});
},
stop: function(event, ui){
scope.$apply(function(){
//make sure that every validates one more time...
validatePosition(ui.position.left, ui.position.top);
//Drag and drop positioning, using jquery ui draggable
var onStartDragPosition, top, left;
$overlay.draggable({
drag: function (event, ui) {
scope.$apply(function () {
validatePosition(ui.position.left, ui.position.top);
});
},
stop: function (event, ui) {
scope.$apply(function () {
//make sure that every validates one more time...
validatePosition(ui.position.left, ui.position.top);
calculateCropBox();
scope.dimensions.image.rnd = Math.random();
});
}
});
calculateCropBox();
scope.dimensions.image.rnd = Math.random();
});
}
});
var init = function(image){
scope.loaded = false;
var init = function (image) {
scope.loaded = false;
//set dimensions on image, viewport, cropper etc
setDimensions(image);
//set dimensions on image, viewport, cropper etc
setDimensions(image);
//create a default crop if we haven't got one already
//create a default crop if we haven't got one already
var createDefaultCrop = !scope.crop;
if (createDefaultCrop) {
calculateCropBox();
}
resizeImageToCrop();
resizeImageToCrop();
//if we're creating a new crop, make sure to zoom out fully
if (createDefaultCrop) {
scope.dimensions.scale.current = scope.dimensions.scale.min;
resizeImageToScale(scope.dimensions.scale.min);
resizeImageToScale(scope.dimensions.scale.min);
if (scope.center) {
// Move image to focal point if set
// Repeating a few calls here, but logic is too difficult to follow elsewhere
var x1 = Math.min(
Math.max(
scope.center.left * scope.dimensions.image.width - scope.dimensions.cropper.width / 2,
0
),
scope.dimensions.image.width - scope.dimensions.cropper.width
);
var y1 = Math.min(
Math.max(
scope.center.top * scope.dimensions.image.height - scope.dimensions.cropper.height / 2,
0
),
scope.dimensions.image.height - scope.dimensions.cropper.height
);
scope.dimensions.image.left = x1;
scope.dimensions.image.top = y1;
calculateCropBox();
resizeImageToCrop();
}
}
//sets constaints for the cropper
setConstraints();
scope.loaded = true;
};
//sets constaints for the cropper
setConstraints();
scope.loaded = true;
};
// Watchers
scope.$watchCollection('[width, height]', function(newValues, oldValues){
// We have to reinit the whole thing if
// one of the external params changes
if(newValues !== oldValues){
setDimensions($image);
setConstraints();
}
});
// Watchers
scope.$watchCollection('[width, height]', function (newValues, oldValues) {
// We have to reinit the whole thing if
// one of the external params changes
if (newValues !== oldValues) {
setDimensions($image);
setConstraints();
}
});
var throttledResizing = _.throttle(function(){
var throttledResizing = _.throttle(function () {
resizeImageToScale(scope.dimensions.scale.current);
calculateCropBox();
}, 15);
calculateCropBox();
}, 15);
// Happens when we change the scale
// Happens when we change the scale
scope.$watch("dimensions.scale.current", function (newValue, oldValue) {
if (scope.loaded) {
throttledResizing();
}
});
if (scope.loaded) {
throttledResizing();
}
});
// Init
$image.on("load", function(){
$timeout(function(){
init($image);
});
});
}
};
});
// Init
$image.on("load", function () {
$timeout(function () {
init($image);
});
});
}
};
});

View File

@@ -17,13 +17,11 @@ angular.module("umbraco")
vm.changeSearch = changeSearch;
vm.submitFolder = submitFolder;
vm.enterSubmitFolder = enterSubmitFolder;
vm.focalPointChanged = focalPointChanged;
vm.changePagination = changePagination;
vm.clickHandler = clickHandler;
vm.clickItemName = clickItemName;
vm.gotoFolder = gotoFolder;
vm.shouldShowUrl = shouldShowUrl;
var dialogOptions = $scope.model;
@@ -131,6 +129,7 @@ angular.module("umbraco")
} else {
// if a target is specified, go look it up - generally this target will just contain ids not the actual full
// media object so we need to look it up
var originalTarget = $scope.target;
var id = $scope.target.udi ? $scope.target.udi : $scope.target.id;
var altText = $scope.target.altText;
@@ -140,13 +139,16 @@ angular.module("umbraco")
entityResource.getById(id, "Media")
.then(function (node) {
$scope.target = node;
if (ensureWithinStartNode(node)) {
// Moving directly to existing node's folder
gotoFolder({ id: node.parentId }).then(function() {
selectMedia(node);
$scope.target.url = mediaHelper.resolveFileFromEntity(node);
$scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true);
$scope.target.altText = altText;
$scope.target.focalPoint = originalTarget.focalPoint;
$scope.target.coordinates = originalTarget.coordinates;
openDetailsDialog();
}
});
}, gotoStartNode);
} else {
// No ID set - then this is going to be a tmpimg that has not been uploaded
@@ -346,25 +348,31 @@ angular.module("umbraco")
}
function openDetailsDialog() {
localizationService.localize("defaultdialogs_editSelectedMedia").then(function (data) {
vm.mediaPickerDetailsOverlay = {
show: true,
title: data,
disableFocalPoint: $scope.disableFocalPoint,
submit: function (model) {
$scope.model.selection.push($scope.target);
$scope.model.submit($scope.model);
const dialog = {
view: "views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html",
size: "small",
cropSize: $scope.cropSize,
target: $scope.target,
disableFocalPoint: $scope.disableFocalPoint,
submit: function (model) {
console.log("model", model);
vm.mediaPickerDetailsOverlay.show = false;
vm.mediaPickerDetailsOverlay = null;
},
close: function (oldModel) {
vm.mediaPickerDetailsOverlay.show = false;
vm.mediaPickerDetailsOverlay = null;
$scope.model.selection.push($scope.target);
$scope.model.submit($scope.model);
close();
}
};
editorService.close();
},
close: function () {
editorService.close();
//close();
}
};
localizationService.localize("defaultdialogs_editSelectedMedia").then(value => {
dialog.title = value;
editorService.open(dialog);
});
};
@@ -515,40 +523,6 @@ angular.module("umbraco")
}
}
/**
* Called when the umbImageGravity component updates the focal point value
* @param {any} left
* @param {any} top
*/
function focalPointChanged(left, top) {
// update the model focalpoint value
$scope.target.focalPoint = {
left: left,
top: top
};
}
function setUpdatedMediaNodes(item) {
// add udi to list of updated media items so we easily can update them in other editors
if ($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) {
$scope.model.updatedMediaNodes.push(item.udi);
}
}
function shouldShowUrl() {
if (!$scope.target) {
return false;
}
if ($scope.target.id) {
return false;
}
if ($scope.target.url && $scope.target.url.toLower().indexOf("blob:") === 0) {
return false;
}
return true;
}
function submit() {
if ($scope.model && $scope.model.submit) {
$scope.model.submit($scope.model);

View File

@@ -1,5 +1,5 @@
<div ng-controller="Umbraco.Editors.MediaPickerController as vm">
<umb-editor-view >
<umb-editor-view>
<umb-editor-header
name="model.title"
@@ -132,64 +132,6 @@
</div>
<umb-overlay ng-if="vm.mediaPickerDetailsOverlay.show" model="vm.mediaPickerDetailsOverlay" position="right">
<div class="umb-control-group" ng-if="vm.shouldShowUrl()">
<h5>
<localize key="@general_url"></localize>
</h5>
<input type="text" localize="placeholder" placeholder="@general_url" class="umb-property-editor umb-textstring" ng-model="target.url" />
</div>
<div class="umb-control-group">
<h5>
<localize key="@content_altTextOptional"></localize>
</h5>
<input type="text" class="umb-property-editor umb-textstring" ng-model="target.altText" />
</div>
<div class="umb-control-group">
<div ng-if="vm.mediaPickerDetailsOverlay.disableFocalPoint && target.thumbnail">
<h5>
<localize key="general_preview">Preview</localize>
</h5>
<img ng-src="{{target.thumbnail}}" alt="{{target.name}}" />
</div>
<div ng-if="!vm.mediaPickerDetailsOverlay.disableFocalPoint">
<h5>
<localize key="@general_focalPoint">Focal point</localize>
</h5>
<div ng-if="target.url">
<umb-image-gravity src="target.url"
center="target.focalPoint"
on-value-changed="vm.focalPointChanged(left, top)">
</umb-image-gravity>
</div>
<div ng-if="cropSize">
<h5>
<localize key="general_preview">Preview</localize>
</h5>
<umb-image-thumbnail center="target.focalPoint"
src="target.url"
height="{{cropSize.height}}"
width="{{cropSize.width}}"
max-size="400">
</umb-image-thumbnail>
</div>
</div>
</div>
</umb-overlay>
</form>
</umb-editor-container>
@@ -218,4 +160,4 @@
</umb-editor-view>
</div>
</div>

View File

@@ -0,0 +1,59 @@
angular.module("umbraco")
.controller("Umbraco.Editors.MediaCropDetailsController",
function ($scope) {
var vm = this;
vm.submit = submit;
vm.close = close;
if (!$scope.model.target.coordinates && !$scope.model.target.focalPoint) {
$scope.model.target.focalPoint = { left: .5, top: .5 };
}
vm.shouldShowUrl = shouldShowUrl;
vm.focalPointChanged = focalPointChanged;
if (!$scope.model.target.image) {
$scope.model.target.image = $scope.model.target.url;
}
function shouldShowUrl() {
if (!$scope.model.target) {
return false;
}
if ($scope.model.target.id) {
return false;
}
if ($scope.model.target.url && $scope.model.target.url.toLower().indexOf("blob:") === 0) {
return false;
}
return true;
}
/**
* Called when the umbImageGravity component updates the focal point value
* @param {any} left
* @param {any} top
*/
function focalPointChanged(left, top) {
// update the model focalpoint value
$scope.model.target.focalPoint = {
left: left,
top: top
};
}
function submit() {
if ($scope.model && $scope.model.submit) {
$scope.model.submit($scope.model);
}
}
function close() {
if ($scope.model && $scope.model.close) {
$scope.model.close($scope.model);
}
}
});

View File

@@ -0,0 +1,78 @@
<div ng-controller="Umbraco.Editors.MediaCropDetailsController as vm">
<umb-editor-view>
<umb-editor-header name="model.title"
name-locked="true"
hide-alias="true"
hide-icon="true"
hide-description="true">
</umb-editor-header>
<umb-editor-container>
<div class="umb-control-group" ng-if="vm.shouldShowUrl()">
<h5>
<localize key="@general_url"></localize>
</h5>
<input type="text" localize="placeholder" placeholder="@general_url" class="umb-property-editor umb-textstring" ng-model="model.target.url" />
</div>
<div class="umb-control-group" ng-if="model.target">
<h5>
<localize key="@content_altTextOptional"></localize>
</h5>
<input type="text" class="umb-property-editor umb-textstring" ng-model="model.target.altText" umb-auto-focus />
</div>
<div class="umb-control-group" ng-if="model.target">
<div ng-if="model.disableFocalPoint && model.target.thumbnail">
<h5>
<localize key="general_preview">Preview</localize>
</h5>
<img ng-src="{{model.target.thumbnail}}" alt="{{model.target.name}}" />
</div>
<div ng-if="!model.disableFocalPoint">
<h5>
<localize key="@general_cropSection">Crop section</localize>
</h5>
<div>
<umb-image-crop height="{{model.cropSize.height}}"
width="{{model.cropSize.width}}"
crop="model.target.coordinates"
center="model.target.focalPoint"
max-size="400"
src="model.target.image">
</umb-image-crop>
</div>
</div>
</div>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button action="vm.close()"
button-style="link"
shortcut="esc"
label-key="general_close"
type="button">
</umb-button>
<umb-button button-style="success"
label-key="buttons_select"
type="button"
disabled="model.selection.length === 0"
action="vm.submit(model)">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</div>

View File

@@ -22,7 +22,16 @@ angular.module("umbraco")
$scope.setImage = function(){
var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined;
var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined;
var value = $scope.control.value;
var target = value
? {
udi: value.udi,
url: value.image,
image: value.image,
focalPoint: value.focalPoint,
coordinates: value.coordinates
}
: null;
var mediaPicker = {
startNodeId: startNodeId,
startNodeIsVirtual: startNodeIsVirtual,
@@ -31,11 +40,13 @@ angular.module("umbraco")
disableFolderSelect: true,
onlyImages: true,
dataTypeKey: $scope.model.dataTypeKey,
currentTarget: target,
submit: function(model) {
var selectedImage = model.selection[0];
$scope.control.value = {
focalPoint: selectedImage.focalPoint,
coordinates: selectedImage.coordinates,
id: selectedImage.id,
udi: selectedImage.udi,
image: selectedImage.image,
@@ -66,14 +77,25 @@ angular.module("umbraco")
var url = $scope.control.value.image;
if($scope.control.editor.config && $scope.control.editor.config.size){
url += "?width=" + $scope.control.editor.config.size.width;
if ($scope.control.value.coordinates) {
// New way, crop by percent must come before width/height.
var coords = $scope.control.value.coordinates;
url += "?crop=" + coords.x1 + "," + coords.y1 + "," + coords.x2 + "," + coords.y2 + "&cropmode=percentage";
} else {
// Here in order not to break existing content where focalPoint were used.
// For some reason width/height have to come first when mode=crop.
if ($scope.control.value.focalPoint) {
url += "?center=" + $scope.control.value.focalPoint.top + "," + $scope.control.value.focalPoint.left;
url += "&mode=crop";
} else {
// Prevent black padding and no crop when focal point not set / changed from default
url += "?center=0.5,0.5&mode=crop";
}
}
url += "&width=" + $scope.control.editor.config.size.width;
url += "&height=" + $scope.control.editor.config.size.height;
url += "&animationprocessmode=first";
if($scope.control.value.focalPoint){
url += "&center=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left;
url += "&mode=crop";
}
}
// set default size if no crop present (moved from the view)

View File

@@ -1,16 +1,16 @@
<div ng-controller="Umbraco.PropertyEditors.Grid.MediaController">
<div class="umb-editor-placeholder" ng-click="setImage()" ng-if="control.value === null">
<i class="icon icon-picture"></i>
<div class="umb-editor-placeholder" ng-click="setImage()" ng-if="control.value === null">
<i class="icon icon-picture"></i>
<div ng-id="!control.$inserted" class="help-text"><localize key="grid_clickToInsertImage">Click to insert image</localize></div>
</div>
</div>
<div ng-if="thumbnailUrl !== null">
<img
ng-click="setImage()"
ng-src="{{thumbnailUrl}}"
class="fullSizeImage" />
<div ng-if="thumbnailUrl !== null">
<img
ng-click="setImage()"
ng-src="{{thumbnailUrl}}"
class="fullSizeImage" />
<input type="text" class="caption" ng-model="control.value.caption" localize="placeholder" placeholder="@grid_placeholderImageCaption" />
</div>
</div>
</div>