private aspectRatioTrigger: JQuery;
private cropperCanvas: JQuery;
private cropInfo: JQuery;
+ private cropImageContainerSelector: string = '#t3js-crop-image-container';
+ private cropImageSelector: string = '#t3js-crop-image';
private coverAreaSelector: string = '.t3js-cropper-cover-area';
private cropInfoSelector: string = '.t3js-cropper-info-crop';
private focusAreaSelector: string = '#t3js-cropper-focus-area';
viewMode: 1,
zoomable: false,
};
+ private resizeTimeout: number = 450;
constructor() {
// Silence is golden
}
/**
- * Initialize the cropper modal
+ * @method initializeCropperModal
+ * @desc Initialize the cropper modal and dispatch the cropper init
+ * @private
*/
private initializeCropperModal(): void {
- const image: JQuery = this.currentModal.find('#t3js-crop-image');
+ const image: JQuery = this.currentModal.find(this.cropImageSelector);
ImagesLoaded(image, (): void => {
const modal: JQuery = this.currentModal.find('.modal-dialog');
modal.css({marginLeft: 'auto', marginRight: 'auto'});
});
}
+ /**
+ * @method show
+ * @desc Load the image and setup the modal UI
+ * @private
+ */
private show(): void {
const modalTitle: string = this.trigger.data('modalTitle');
const imageUri: string = this.trigger.data('url');
'.modal-content'
);
this.currentModal.addClass('modal-dark');
+ this.currentModal.on('hide.bs.modal', (e: JQueryEventObject): void => {
+ this.destroy();
+ });
+ // Do not dismiss the modal when clicking beside it to avoid data loss
+ this.currentModal.data('bs.modal').options.backdrop = 'static';
}
+ /**
+ * @method init
+ * @desc Initializes the cropper UI and sets up all the event indings for the UI
+ * @private
+ */
private init(): void {
- const image: JQuery = this.currentModal.find('#t3js-crop-image');
+ const image: JQuery = this.currentModal.find(this.cropImageSelector);
const imageHeight: number = $(image).height();
const imageWidth: number = $(image).width();
const data: string = this.trigger.attr('data-crop-variants');
// If we have data already set we assume an internal reinit eg. after resizing
this.data = $.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
// Initialize our class members
- this.currentModal.find('.cropper-image-container').css({height: imageHeight, width: imageWidth});
+ this.currentModal.find(this.cropImageContainerSelector).css({height: imageHeight, width: imageWidth});
this.cropVariantTriggers = this.currentModal.find('.t3js-crop-variant-trigger');
this.activeCropVariantTrigger = this.currentModal.find('.t3js-crop-variant-trigger.is-active');
this.cropInfo = this.currentModal.find(this.cropInfoSelector);
/**
* Assign EventListener to cropVariantTriggers
*/
- this.cropVariantTriggers.on('click', (e: JQueryEventObject): void => {
+ this.cropVariantTriggers.off('click').on('click', (e: JQueryEventObject): void => {
/**
* Is the current cropVariantTrigger is active, bail out.
/**
* Assign EventListener to aspectRatioTrigger
*/
- this.aspectRatioTrigger.on('click', (e: JQueryEventObject): void => {
+ this.aspectRatioTrigger.off('click').on('click', (e: JQueryEventObject): void => {
const ratioId: string = $(e.currentTarget).attr('data-option');
const temp: CropVariant = $.extend(true, {}, this.currentCropVariant);
const ratio: Ratio = temp.allowedAspectRatios[ratioId];
- this.updateAspectRatio(ratio);
- // Set data explicitly or updateAspectRatio upscales the crop
+ this.setAspectRatio(ratio);
+ // Set data explicitly or setAspectRatio upscales the crop
this.setCropArea(temp.cropArea);
this.currentCropVariant = $.extend(true, {}, temp, {selectedRatio: ratioId});
this.update(this.currentCropVariant);
/**
* Assign EventListener to saveButton
*/
- this.saveButton.on('click', (): void => {
+ this.saveButton.off('click').on('click', (): void => {
this.save(this.data);
});
* Assign EventListener to previewButton if preview url exists
*/
if (this.trigger.attr('data-preview-url')) {
- this.previewButton.on('click', (): void => {
+ this.previewButton.off('click').on('click', (): void => {
this.openPreview(this.data);
});
} else {
/**
* Assign EventListener to dismissButton
*/
- this.dismissButton.on('click', (): void => {
- this.destroy();
+ this.dismissButton.off('click').on('click', (): void => {
+ this.currentModal.modal('hide');
});
/**
* Assign EventListener to resetButton
*/
- this.resetButton.on('click', (e: JQueryEventObject): void => {
+ this.resetButton.off('click').on('click', (e: JQueryEventObject): void => {
const imageData: CropperImageData = this.cropper.cropper('getImageData');
const resetCropVariantString: string = $(e.currentTarget).attr('data-crop-variant');
e.preventDefault();
}));
}
+ /**
+ * @method cropBuiltHandler
+ * @desc Internal cropper handler. Called when the cropper has been instantiated
+ * @private
+ */
private cropBuiltHandler = (): void => {
const imageData: CropperImageData = this.cropper.cropper('getImageData');
+
+ // Iterate over the crop variants and set up their respective preview
+ this.cropVariantTriggers.each((index: number, elem: Element): void => {
+ const cropVariantId: string = $(elem).attr('data-crop-variant-id');
+ const cropArea: Area = this.convertRelativeToAbsoluteCropArea(
+ this.data[cropVariantId].cropArea,
+ imageData
+ );
+ const variant: CropVariant = $.extend(true, {}, this.data[cropVariantId], {cropArea});
+ this.updatePreviewThumbnail(variant, $(elem));
+ });
+
this.currentCropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(
this.currentCropVariant.cropArea,
imageData
);
+ // Can't use .t3js-* as selector because it is an extraneous selector
this.cropBox = this.currentModal.find('.cropper-crop-box');
this.setCropArea(this.currentCropVariant.cropArea);
}
if (this.currentCropVariant.selectedRatio) {
- this.updateAspectRatio(this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio]);
- // Set data explicitly or updateAspectRatio up-scales the crop
+ this.setAspectRatio(this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio]);
+ // Set data explicitly or setAspectRatio up-scales the crop
this.setCropArea(this.currentCropVariant.cropArea);
this.currentModal.find(`[data-option='${this.currentCropVariant.selectedRatio}']`).addClass('active');
}
+
this.cropperCanvas.addClass('is-visible');
};
+ /**
+ * @method cropMoveHandler
+ * @desc Internal cropper handler. Called when the cropping area is moving
+ * @private
+ */
private cropMoveHandler = (e: CropperEvent): void => {
this.currentCropVariant.cropArea = $.extend(true, this.currentCropVariant.cropArea, {
height: Math.floor(e.height),
x: Math.floor(e.x),
y: Math.floor(e.y),
});
- this.updatePreviewThumbnail(this.currentCropVariant);
+ this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
this.updateCropVariantData(this.currentCropVariant);
this.cropInfo.text(`${this.currentCropVariant.cropArea.width}×${this.currentCropVariant.cropArea.height} px`);
};
+ /**
+ * @method cropStartHandler
+ * @desc Internal cropper handler. Called when the cropping starts moving
+ * @private
+ */
private cropStartHandler = (): void => {
if (this.currentCropVariant.focusArea) {
this.focusArea.draggable('option', 'disabled', true);
};
/**
- *
+ * @method cropEndHandler
+ * @desc Internal cropper handler. Called when the cropping ends moving
+ * @private
*/
private cropEndHandler = (): void => {
if (this.currentCropVariant.focusArea) {
/**
* Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
*/
- this.updateAspectRatio(selectedRatio);
+ this.setAspectRatio(selectedRatio);
this.setCropArea(temp.cropArea);
this.currentCropVariant = $.extend(true, {}, temp, cropVariant);
this.cropBox.find(this.coverAreaSelector).remove();
// Init or reinit focusArea
this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
}
- this.updatePreviewThumbnail(this.currentCropVariant);
+ this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
}
/**
* @method initFocusArea
* @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
- * @param container: JQuery
+ * @param {JQuery} container
+ * @private
*/
private initFocusArea(container: JQuery): void {
this.focusArea = $('<div id="t3js-cropper-focus-area" class="cropper-focus-area"></div>');
focusArea.x = (fLeft - left) / container.width();
focusArea.y = (fTop - top) / container.height();
- this.updatePreviewThumbnail(this.currentCropVariant);
+ this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
this.focusArea.addClass('has-nodrop');
} else {
focusArea.width = this.focusArea.width() / container.width();
focusArea.x = (fLeft - left) / container.width();
focusArea.y = (fTop - top) / container.height();
- this.updatePreviewThumbnail(this.currentCropVariant);
+ this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
this.focusArea.addClass('has-nodrop');
/**
* @method updatePreviewThumbnail
* @desc Sync the croping (and focus area) to the preview thumbnail
- * @param {CropVariant} cropVariant
+ * @param {CropVariant} cropVariant - The crop variant to preview in the thumbnail
+ * @param {JQuery} cropVariantTrigger - The crop variant element containing the thumbnail
+ * @private
*/
- private updatePreviewThumbnail(cropVariant: CropVariant): void {
+ private updatePreviewThumbnail(cropVariant: CropVariant, cropVariantTrigger: JQuery): void {
let styles: any;
const cropperPreviewThumbnailCrop: JQuery =
- this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
+ cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-area');
const cropperPreviewThumbnailImage: JQuery =
- this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
+ cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-crop-image');
const cropperPreviewThumbnailFocus: JQuery =
- this.activeCropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
+ cropVariantTrigger.find('.t3js-cropper-preview-thumbnail-focus-area');
const imageData: CropperImageData = this.cropper.cropper('getImageData');
// Update the position/dimension of the crop area in the preview
width: ImageManipulation.toCssPercent(focusArea.width),
});
this.currentCropVariant.focusArea = focusArea;
- this.updatePreviewThumbnail(this.currentCropVariant);
+ this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
this.updateCropVariantData(this.currentCropVariant);
}
* @method updateCropVariantData
* @desc Immutably updates the currently selected cropVariant data
* @param {CropVariant} currentCropVariant - The cropVariant to immutably save
+ * @private
*/
private updateCropVariantData(currentCropVariant: CropVariant): void {
const imageData: CropperImageData = this.cropper.cropper('getImageData');
}
/**
- * @method updateAspectRatio
- * @desc Updates the aspect ratio in the cropper
- * @param {ratio} ratio ratio set in the cropper
+ * @method setAspectRatio
+ * @desc Sets the cropper to a specific ratio
+ * @param {ratio} ratio - The ratio value to apply
+ * @private
*/
- private updateAspectRatio(ratio: Ratio): void {
+ private setAspectRatio(ratio: Ratio): void {
this.cropper.cropper('setAspectRatio', ratio.value);
}
/**
* @method setCropArea
- * @desc Updates the crop area in the cropper. The cropper will respect the selected ratio
- * @param {cropArea} cropArea ratio set in the cropper
+ * @desc Sets the cropper to a specific crop area
+ * @param {cropArea} cropArea - The crop area to apply
+ * @private
*/
private setCropArea(cropArea: Area): void {
- this.cropper.cropper('setData', {
- height: cropArea.height,
- width: cropArea.width,
- x: cropArea.x,
- y: cropArea.y,
- });
+ const currentRatio: Ratio = this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];
+ if (currentRatio.value === 0) {
+ this.cropper.cropper('setData', {
+ height: cropArea.height,
+ width: cropArea.width,
+ x: cropArea.x,
+ y: cropArea.y,
+ });
+ } else {
+ this.cropper.cropper('setData', {
+ height: cropArea.height,
+ x: cropArea.x,
+ y: cropArea.y,
+ });
+ }
}
/**
}
/**
- * @param cropArea
- * @param imageData
- * @return {{height: number, width: number, x: number, y: number}}
+ * @method convertAbsoluteToRelativeCropArea
+ * @desc Converts a crop area from absolute pixel-based into relative length values
+ * @param {Area} cropArea - The crop area to convert from
+ * @param {CropperImageData} imageData - The image data
+ * @return {Area}
*/
private convertAbsoluteToRelativeCropArea(cropArea: Area, imageData: CropperImageData): Area {
const {height, width, x, y}: Area = cropArea;
}
/**
- * @param cropArea
- * @param imageData
+ * @method convertRelativeToAbsoluteCropArea
+ * @desc Converts a crop area from relative into absolute pixel-based length values
+ * @param {Area} cropArea - The crop area to convert from
+ * @param {CropperImageData} imageData - The image data
* @return {{height: number, width: number, x: number, y: number}}
*/
private convertRelativeToAbsoluteCropArea(cropArea: Area, imageData: CropperImageData): Area {
};
}
- private setPreviewImage(data: Object): void {
+ /**
+ * @method setPreviewImages
+ * @desc Updates the preview images in the editing section with the respective crop variants
+ * @param {Object} data - The internal crop variants state
+ */
+ private setPreviewImages(data: Object): void {
let $image: any = this.cropper;
let imageData: CropperImageData = $image.cropper('getImageData');
+
+ // Iterate over the crop variants and set up their respective preview
Object.keys(data).forEach((cropVariantId: string) => {
const cropVariant: CropVariant = data[cropVariantId];
const cropData: Area = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
/**
* @method openPreview
- * @desc open a preview
+ * @desc Opens a preview view with the crop variants
* @param {object} data - The whole data object containing all the cropVariants
* @private
*/
const cropVariants: string = ImageManipulation.serializeCropVariants(data);
const hiddenField: JQuery = $(`#${this.trigger.attr('data-field')}`);
this.trigger.attr('data-crop-variants', JSON.stringify(data));
- this.setPreviewImage(data);
+ this.setPreviewImages(data);
hiddenField.val(cropVariants);
- this.destroy();
+ this.currentModal.modal('hide');
}
/**
*/
private destroy(): void {
if (this.currentModal) {
- this.currentModal.modal('hide');
this.cropper.cropper('destroy');
+ this.cropper = null;
this.currentModal = null;
+ this.data = null;
}
}
+ /**
+ * @method resizeEnd
+ * @desc Calls a function when the cropper has been resized
+ * @param {Function} fn - The function to call on resize completion
+ * @private
+ */
private resizeEnd(fn: Function): void {
let timer: number;
$(window).on('resize', (): void => {
clearTimeout(timer);
timer = setTimeout((): void => {
fn();
- }, 450);
+ }, this.resizeTimeout);
});
}
}