UNPKG

image-focus

Version:

A dependency free utility for cropping images based on a focal point ~2.13kB gzipped

203 lines (175 loc) 6.22 kB
import { debounce } from './helpers/debounce'; import { assign } from './helpers/assign'; import { Focus, FocusedImageOptions } from './interfaces'; import { CONTAINER_STYLES, ABSOLUTE_STYLES } from './sharedStyles'; const IMG_STYLES = { // Set these styles in case the image dimensions // are smaller than the container's minHeight: '100%', minWidth: '100%', }; const RESIZE_LISTENER_OBJECT_STYLES = { height: '100%', width: '100%', border: 'none', // set these styles to emulate "visibility: hidden" // can't use visibility because it breaks the object // events in Firefox opacity: 0, zIndex: -1, pointerEvents: 'none', }; const DEFAULT_OPTIONS: FocusedImageOptions = { debounceTime: 17, updateOnWindowResize: true, updateOnContainerResize: false, containerPosition: 'relative', }; export class FocusedImage { focus: Focus; options: FocusedImageOptions; container: HTMLElement; img: HTMLImageElement; resizeListenerObject: HTMLObjectElement; listening: boolean = false; debounceApplyShift: () => void; constructor( private imageNode: HTMLImageElement, options: FocusedImageOptions = {} ) { // Merge in options this.options = assign(DEFAULT_OPTIONS, options); // Set up element references this.img = imageNode; this.container = imageNode.parentElement; // Set up instance if (this.img['__focused_image_instance__']) { this.img['__focused_image_instance__'].stopListening(); this.img.removeEventListener('load', this.applyShift); } this.img['__focused_image_instance__'] = this; // Add image load event listener this.img.addEventListener('load', this.applyShift); // Set up styles assign(this.container.style, CONTAINER_STYLES); this.container.style.position = this.options.containerPosition; assign(this.img.style, IMG_STYLES, ABSOLUTE_STYLES); // Create debouncedShift function this.debounceApplyShift = debounce( this.applyShift, this.options.debounceTime ); // Initialize focus this.focus = this.options.focus ? this.options.focus : { x: parseFloat(this.img.getAttribute('data-focus-x')) || 0, y: parseFloat(this.img.getAttribute('data-focus-y')) || 0, }; // Start listening for resize events this.startListening(); // Set focus this.setFocus(this.focus); } public setFocus = (focus: Focus) => { this.focus = focus; this.img.setAttribute('data-focus-x', focus.x.toString()); this.img.setAttribute('data-focus-y', focus.y.toString()); this.applyShift(); }; public applyShift = () => { const { naturalWidth: imageW, naturalHeight: imageH } = this.img; const { width: containerW, height: containerH, } = this.container.getBoundingClientRect(); // Amount position will be shifted let hShift = '0'; let vShift = '0'; if (!(containerW > 0 && containerH > 0 && imageW > 0 && imageH > 0)) { return false; // Need dimensions to proceed } // Which is over by more? const wR = imageW / containerW; const hR = imageH / containerH; // Reset max-width and -height this.img.style.maxHeight = null; this.img.style.maxWidth = null; // Minimize image while still filling space if (imageW > containerW && imageH > containerH) { this.img.style[wR > hR ? 'maxHeight' : 'maxWidth'] = '100%'; } if (wR > hR) { hShift = `${this.calcShift(hR, containerW, imageW, this.focus.x)}%`; } else if (wR < hR) { vShift = `${this.calcShift(wR, containerH, imageH, this.focus.y, true)}%`; } this.img.style.top = vShift; this.img.style.left = hShift; }; public startListening() { if (this.listening) { return; } this.listening = true; if (this.options.updateOnWindowResize) { window.addEventListener('resize', this.debounceApplyShift); } if (this.options.updateOnContainerResize) { const object = document.createElement('object'); assign(object.style, RESIZE_LISTENER_OBJECT_STYLES, ABSOLUTE_STYLES); // Use load event callback because contentDocument doesn't exist // until this fires in Firefox object.addEventListener('load', (e: Event) => object.contentDocument.defaultView.addEventListener('resize', () => this.debounceApplyShift() ) ); object.type = 'text/html'; object.setAttribute('aria-hidden', 'true'); object.tabIndex = -1; this.container.appendChild(object); object.data = 'about:blank'; this.resizeListenerObject = object; } } public stopListening() { if (!this.listening) { return; } this.listening = false; window.removeEventListener('resize', this.debounceApplyShift); if ( this.resizeListenerObject && this.resizeListenerObject.contentDocument ) { this.resizeListenerObject.contentDocument.defaultView.removeEventListener( 'resize', this.debounceApplyShift ); this.container.removeChild(this.resizeListenerObject); this.resizeListenerObject = null; } } // Calculate the new left/top percentage shift of an image private calcShift( conToImageRatio: number, containerSize: number, imageSize: number, focusSize: number, toMinus?: boolean ) { const containerCenter = Math.floor(containerSize / 2); // Container center in px const focusFactor = (focusSize + 1) / 2; // Focus point of resize image in px const scaledImage = Math.floor(imageSize / conToImageRatio); // Can't use width() as images may be display:none let focus = Math.floor(focusFactor * scaledImage); if (toMinus) focus = scaledImage - focus; let focusOffset = focus - containerCenter; // Calculate difference between focus point and center const remainder = scaledImage - focus; // Reduce offset if necessary so image remains filled const containerRemainder = containerSize - containerCenter; if (remainder < containerRemainder) focusOffset -= containerRemainder - remainder; if (focusOffset < 0) focusOffset = 0; return (focusOffset * -100) / containerSize; } }