Skip to content

Commit

Permalink
#17 image editor refactored
Browse files Browse the repository at this point in the history
  • Loading branch information
Roel van Hintum committed May 17, 2024
1 parent b6e2ccb commit 1bbb54c
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 68 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## 3.0.0-beta.2 - 2024-05-17
- #17 image editor refactored to support zoom and panning (using scroll and drag)
- points can now be dragged around

## 3.0.0-beta.1 - 2024-03-22
Hotspots are not yet prefilled with data from nested entries as used in the new Matrix fields.

Expand Down
57 changes: 31 additions & 26 deletions src/assetbundles/imagehotspotsfield/dist/css/Input.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,43 @@
}

.image-hotspot-editor {
padding-bottom: 58px;
background-color: var(--gray-900);
color: var(--white);
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
--focus-ring: 0 0 0 1px hsl(var(--light-focus-hsl)), 0 0 0 3px hsla(var(--light-focus-hsl),0.7);
opacity: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border-radius: 0;
}

.image-hotspot-editor .body {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}

.image-hotspot-editor .image-container {
position: relative;
display: inline-block;
user-select: none;
z-index: 0;
.image-hotspot-editor .footer {
background-color: transparent;
border-top: 1px solid #000;
bottom: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
}
.image-hotspot-editor .image-container.align-height {
width: auto;
}
.image-hotspot-editor .image-container.align-width {
height: auto;
}

.image-hotspot-editor img {
max-height: 100%;
max-width: 100%;
.image-hotspot-container {
position: absolute;
display: block;
}

.image-hotspot-editor .align-height img {
z-index: 0;
width: 100%;
height: 100%;
opacity: 0;
}

.image-hotspot-editor .align-width img {
width: 100%;
.image-hotspot-editor img {
display: block;
}

.image-hotspot-editor .point,
Expand All @@ -68,3 +67,9 @@
width: 100%;
margin: 0;
}

.image-hotspot-buttons {
position: absolute;
right: 20px;
bottom: 20px;
}
216 changes: 174 additions & 42 deletions src/assetbundles/imagehotspotsfield/dist/js/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@
if (typeof Born05 === 'undefined') var Born05 = {};

Born05.ImageHotspotEditor = Garnish.Modal.extend({
$body: null,
$footer: null,
$buttons: null,

$img: null,
$cancelBtn: null,
$saveBtn: null,
$imgContainer: null,

asset: null,
pos: null,
listData: null,
imageBounds: null,

isDraggingImage: false,
isDraggingPoint: false,
previousMouseX: 0,
previousMouseY: 0,

prevOffsetX: 0,
prevOffsetY: 0,

init: function(asset, pos, listData, settings) {
this.setSettings(settings, Born05.ImageHotspotEditor.defaults);
Expand All @@ -21,46 +34,88 @@ Born05.ImageHotspotEditor = Garnish.Modal.extend({
this.updateData(listData);

// Build the modal
var $container = $('<div class="modal image-hotspot-editor"></div>').appendTo(Garnish.$bod);
var $body = $('<div class="body"></div>').appendTo($container);
var $footer = $('<div class="footer"/>').appendTo($container);

this.$primaryButtons = $('<div class="buttons right"/>').appendTo($footer);
this.$cancelBtn = $('<div class="btn submit">' + Craft.t('app', 'Done') + '</div>').appendTo(this.$primaryButtons);

this.$imgContainer = $('<div class="image-container"></div>').appendTo($body);
this.$img = $('<img src="'+this.asset.url+'" alt="" />').appendTo(this.$imgContainer);
this.$container = $('<div class="modal fitted image-hotspot-editor"></div>').appendTo(Garnish.$bod);
this.$body = $('<div class="body"></div>').appendTo(this.$container);
this.$footer = $('<div class="footer"/>').appendTo(this.$container);
this.$buttons = $('<div class="buttons right"/>').appendTo(this.$footer);

this.$imgContainer = $('<div class="image-hotspot-container"></div>').appendTo(this.$body);
this.$primaryButtons = $('<div class="image-hotspot-buttons"/>').appendTo(this.$body);
this.$saveBtn = $('<div class="btn submit">' + Craft.t('app', 'Done') + '</div>').appendTo(this.$buttons);

this.base(this.$container, this.settings);
this.updateSizeAndPosition();

this.$img = $('<img src="'+this.asset.url+'" width="'+this.asset.width+'" height="'+this.asset.height+'" alt="" />').appendTo(this.$imgContainer);
this.$point = $('<div class="current"></div>').appendTo(this.$imgContainer);
this.setPoint(this.pos.x, this.pos.y);

this.base($container, this.settings);

this.addListener(this.$imgContainer, 'click', 'select');
this.addListener(this.$cancelBtn, 'activate', 'hide');
this.addListener(this.$saveBtn, 'activate', 'hide');

// Add mouse event listeners
this.addListener(this.$container, 'wheel', 'zoomImage');

this.addListener(
this.$container,
'mousedown,touchstart',
'handleDragStart'
);
this.addListener(
this.$container,
'mousemove,touchmove',
'handleDragMove'
);
this.addListener(
this.$container,
'mouseup,touchend,touchcancel',
'handleDragEnd'
);
},

createImage: function(asset) {
this.$imgContainer.removeClass('align-height align-width');
/**
* Update the modal size and position on browser resize
*/
updateSizeAndPosition: function () {
if (!this.$container) {
return;
}

var containerBounds = this.$imgContainer[0].getBoundingClientRect();
var containerRatio = containerBounds.width / containerBounds.height;
var imageRatio = asset.width / asset.height;
// Fullscreen modal
var innerWidth = window.innerWidth;
var innerHeight = window.innerHeight;

if (containerRatio > imageRatio) {
this.$imgContainer.addClass('align-height');
} else {
this.$imgContainer.addClass('align-width');
this.$container.css({
width: innerWidth,
'min-width': innerWidth,
left: 0,

height: innerHeight,
'min-height': innerHeight,
top: 0,
});

this.$body.css({
height: innerHeight - (this.$footer.outerHeight() - 1),
});

if (innerWidth < innerHeight) {
this.$container.addClass('vertical');
} else {
this.$container.removeClass('vertical');
}
},

updateData: function(listData) {
this.listData = listData;
// If image is already loaded, make sure it looks pretty.
if (this.$imgContainer) {
this.resizeImageContainer();
}
},

show: function() {
Garnish.Modal.prototype.show.call(this);

this.$imgContainer.find('.point').remove();
/**
* Make sure underlying content is not scrolled by accident.
*/
onShow: function() {
Garnish.$bod.addClass('no-scroll');

// Loop through points
Object.keys(this.listData).forEach(function(key) {
Expand All @@ -74,31 +129,108 @@ Born05.ImageHotspotEditor = Garnish.Modal.extend({
}, this);
},

onFadeIn: function() {
this.createImage(this.asset);
/**
* Allow the content to scroll.
*/
onHide: function () {
this.$imgContainer.find('.point').remove();

Garnish.$bod.removeClass('no-scroll');
},

setPoint: function(x, y) {
this.$point.css({
top: (y * 100) + '%',
left: (x * 100) + '%',
resizeImageContainer: function() {
this.containerBounds = this.$body[0].getBoundingClientRect();
this.containerRatio = this.containerBounds.width / this.containerBounds.height;
this.imageRatio = this.asset.width / this.asset.height;
this.zoom = 1;
this.offsetX = 0;
this.offsetY = 0;

if (this.containerRatio > this.imageRatio) {
this.$imgContainer.css({
width: this.asset.width * (this.containerBounds.height / this.asset.height),
height: this.containerBounds.height,
top: 0,
left: (this.containerBounds.width - (this.asset.width * (this.containerBounds.height / this.asset.height))) / 2,
transform: 'translate(0, 0)',
opacity: 1,
});
} else {
this.$imgContainer.css({
width: this.containerBounds.width,
height: this.asset.height * (this.containerBounds.width / this.asset.width),
top: (this.containerBounds.height - (this.asset.height * (this.containerBounds.width / this.asset.width))) / 2,
left: 0,
transform: 'translate(0, 0)',
opacity: 1,
});
}
},

zoomImage: function(e) {
this.zoom = Math.max(1, Math.min(20, this.zoom + e.originalEvent.deltaY/-100));
this.updateTransform();
},

updateTransform: function() {
this.$imgContainer.css({
transform: 'translate('+this.offsetX+'px, '+this.offsetY+'px) scale('+this.zoom+')',
});
},

select: function(e) {
handleDragStart: function(e) {
if (this.isDraggingPoint) return;
e.preventDefault();

var imageBounds = this.$img[0].getBoundingClientRect();
if (e.target === this.$point[0]) {
this.imageBounds = this.$img[0].getBoundingClientRect();
this.isDraggingPoint = true;
} else {
this.previousMouseX = e.pageX;
this.previousMouseY = e.pageY;
this.prevOffsetX = this.offsetX;
this.prevOffsetY = this.offsetY;
this.isDraggingImage = true;
}
},
handleDragMove: function(e) {
if (!this.isDraggingImage && !this.isDraggingPoint) return;
e.preventDefault();

if (this.isDraggingImage) {
this.offsetX = this.prevOffsetX + (e.pageX - this.previousMouseX);
this.offsetY = this.prevOffsetY + (e.pageY - this.previousMouseY);
this.updateTransform();
}

var x = (e.clientX - imageBounds.left) / imageBounds.width;
var y = (e.clientY - imageBounds.top) / imageBounds.height;
this.setPoint(x, y);
if (this.isDraggingPoint) {
var x = (e.clientX - this.imageBounds.left) / this.imageBounds.width;
var y = (e.clientY - this.imageBounds.top) / this.imageBounds.height;
this.setPoint(x, y);
}
},
handleDragEnd: function(e) {
if (!this.isDraggingImage && !this.isDraggingPoint) return;
e.preventDefault();

this.isDraggingImage = false;
this.isDraggingPoint = false;
},

updateData: function(listData) {
this.listData = listData;
},

setPoint: function(x, y) {
this.$point.css({
top: (y * 100) + '%',
left: (x * 100) + '%',
});
this.settings.onSelect(x, y);
},
},
{
defaults: {
id: null,
onSelect: $.noop,
animationDuration: 100,
}
});

0 comments on commit 1bbb54c

Please sign in to comment.