diff --git a/src/index.css b/src/index.css index 55342ef..ad2f87b 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,7 @@ overflow: hidden; margin-bottom: 10px; padding-bottom: 0; + position: relative; &-picture { max-width: 100%; @@ -42,6 +43,30 @@ box-sizing: border-box; } } + + &-resize-handle { + position: absolute; + top: 50%; + right: 4px; + width: 14px; + height: 120px; + max-height: calc(100% - 8px); + background: rgba(0, 0, 0, 0.35); + border: 2px solid rgba(255, 255, 255, 0.95); + border-radius: 12px; + cursor: ew-resize; + transform: translateY(-50%); + opacity: 0; + transition: opacity 0.15s ease, background 0.15s ease; + } + + &:hover &-resize-handle { + opacity: 1; + } + + &-resize-handle:hover { + background: rgba(0, 0, 0, 0.5); + } } &__caption { @@ -175,4 +200,4 @@ 100% { transform: rotate(360deg); } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 7700606..ed297c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,6 +140,7 @@ export default class ImageTool implements BlockTool { */ this._data = { caption: '', + width: undefined, withBorder: false, withBackground: false, stretched: false, @@ -223,6 +224,7 @@ export default class ImageTool implements BlockTool { const caption = this.ui.nodes.caption; this._data.caption = caption.innerHTML; + this._data.width = this.ui.getWidth(); return this.data; } @@ -394,7 +396,9 @@ export default class ImageTool implements BlockTool { this.image = data.file; this._data.caption = data.caption || ''; + this._data.width = typeof data.width === 'number' ? data.width : undefined; this.ui.fillCaption(this._data.caption); + this.ui.applyWidth(this._data.width); ImageTool.tunes.forEach(({ name: tune }) => { const value = typeof data[tune as keyof ImageToolData] !== 'undefined' ? data[tune as keyof ImageToolData] === true || data[tune as keyof ImageToolData] === 'true' : false; diff --git a/src/types/types.ts b/src/types/types.ts index 3de5505..98ffd4d 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -72,6 +72,10 @@ export type ImageToolData = { * Caption for the image. */ caption: string; + /** + * Optional width (in pixels) applied to the image container. + */ + width?: number; /** * Flag indicating whether the image has a border. diff --git a/src/ui.ts b/src/ui.ts index f66afa0..3e29538 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -56,6 +56,11 @@ interface Nodes { * Caption element for the image. */ caption: HTMLElement; + + /** + * Resize handle element. + */ + resizeHandle?: HTMLElement; } /** @@ -112,6 +117,11 @@ export default class Ui { */ private readOnly: boolean; + /** + * Tracked width in pixels. + */ + private currentWidth?: number; + /** * @param ui - image tool Ui module * @param ui.api - Editor.js API @@ -244,11 +254,73 @@ export default class Ui { if (this.nodes.imagePreloader !== undefined) { this.nodes.imagePreloader.style.backgroundImage = ''; } + this.applyWidth(this.currentWidth); + this.ensureResizeHandle(); }); this.nodes.imageContainer.appendChild(this.nodes.imageEl); } + /** + * Apply a specific width (px) to the image container. + */ + public applyWidth(width?: number): void { + const MIN = 40; + const parentWidth = this.nodes.wrapper.parentElement?.getBoundingClientRect()?.width; + const max = parentWidth && Number.isFinite(parentWidth) ? Math.max(MIN, parentWidth) : undefined; + let next = width; + if (typeof next === 'number' && next > 0) { + if (max) next = Math.min(next, max); + next = Math.max(MIN, next); + this.currentWidth = next; + this.nodes.imageContainer.style.width = `${next}px`; + } else { + this.currentWidth = undefined; + this.nodes.imageContainer.style.width = ''; + } + } + + /** + * Returns current width if set. + */ + public getWidth(): number | undefined { + if (typeof this.currentWidth === 'number') return this.currentWidth; + const inline = parseFloat(this.nodes.imageContainer.style.width); + return Number.isFinite(inline) ? inline : undefined; + } + + /** + * Ensure resize handle exists and is wired. + */ + private ensureResizeHandle(): void { + if (this.readOnly || this.nodes.resizeHandle) return; + const handle = make('div', this.CSS.resizeHandle); + let startX = 0; + let startWidth = 0; + + const onMove = (event: MouseEvent) => { + const delta = event.clientX - startX; + const next = startWidth + delta; + this.applyWidth(next); + }; + + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + }; + + handle.addEventListener('mousedown', (event: MouseEvent) => { + event.preventDefault(); + startX = event.clientX; + startWidth = this.nodes.imageContainer.getBoundingClientRect().width; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + + this.nodes.imageContainer.appendChild(handle); + this.nodes.resizeHandle = handle; + } + /** * Shows caption input * @param text - caption content text @@ -291,6 +363,7 @@ export default class Ui { imagePreloader: 'image-tool__image-preloader', imageEl: 'image-tool__image-picture', caption: 'image-tool__caption', + resizeHandle: 'image-tool__image-resize-handle', }; };