Composer image resize - Community#729 (#2440)

* Apply resizer

TODO: check how to undo AND see if title/alt attribute implementation is possible (maybe a pop-up with fields for these AND width and height 🤷‍♂️)

* Stop composer ONLY styles

The composer is adding styles to inline images, this means a draft can look completely different to the received mail. Removed the `vertical-align` and reset the `margin` to match the email view

* image ratio retention & small fix

- Added image ratio retention (<kbd>shift</kbd> = freeform)
- Fixed resizing being stopped when reducing the image size reduces the height of the email
- Had to stop the container from reducing (whilst resizing) as reducing the image inside a reducing element was VERY janky 🤮
This commit is contained in:
Glenn 2022-12-29 16:37:37 +00:00 committed by GitHub
parent 0c2c1c3465
commit 9305bc8ac1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 243 additions and 29 deletions

4
app/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "mailspring",
"version": "1.10.5",
"version": "1.10.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "mailspring",
"version": "1.10.5",
"version": "1.10.7",
"license": "GPL-3.0",
"dependencies": {
"@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108",

View file

@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import classnames from 'classnames';
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import * as Actions from '../flux/actions';
@ -67,7 +67,9 @@ function buildContextMenu(fns: {
label: localized('Save Into...'),
});
}
require('@electron/remote').Menu.buildFromTemplate(template).popup({});
require('@electron/remote')
.Menu.buildFromTemplate(template)
.popup({});
}
const ProgressBar: React.FunctionComponent<{
@ -284,11 +286,17 @@ export class AttachmentItem extends Component<AttachmentItemProps> {
}
}
export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgProps?: any }> {
interface ImageAttachmentItemProps extends AttachmentItemProps {
onResized: (width: number, height: number) => void;
imgProps?: { width: number; height: number };
}
export class ImageAttachmentItem extends Component<ImageAttachmentItemProps> {
static displayName = 'ImageAttachmentItem';
static propTypes = {
imgProps: PropTypes.object,
onResized: PropTypes.func,
...propTypes,
};
@ -308,7 +316,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
};
renderImage() {
const { download, filePath, draggable } = this.props;
const { download, filePath, draggable, imgProps } = this.props;
if (download && download.percent <= 5) {
return (
<div style={{ width: '100%', height: '100px' }}>
@ -316,9 +324,22 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
</div>
);
}
const src =
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath;
return <img draggable={draggable} src={src} alt="" onLoad={this._onImgLoaded} />;
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath,
styles: CSSProperties = {};
if (imgProps) {
if (imgProps.height) {
styles.height = `${imgProps.height}px`;
}
if (imgProps.width) {
styles.width = `${imgProps.width}px`;
}
}
return <img draggable={draggable} src={src} alt="" onLoad={this._onImgLoaded} style={styles} />;
}
componentDidMount() {
@ -340,6 +361,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
onSaveAttachment,
...extraProps
} = this.props;
return (
<div
className={`nylas-attachment-item image-attachment-item ${className || ''}`}
@ -366,7 +388,132 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
{this.renderImage()}
</div>
</div>
<div className="resizer" onMouseDown={this._resizeStart}>
<i className="gg-arrows-expand-left"></i>
</div>
</div>
);
}
private _pData = { x: 0, y: 0, eH: 0 };
private _shiftData = {
held: false,
ratio: { wh: 0, hw: 0 },
};
private _editor = () => document.querySelector('.compose-body') as HTMLDivElement;
private _resizeImage = (
ev: (
| MouseEvent
| {
x: number;
y: number;
}
) & { useWH?: boolean }
) => {
const img = document.querySelector(
'.image-attachment-item[data-resizing] .file-preview img'
) as HTMLImageElement,
editor = this._editor();
if (img) {
let newWidth = ev.x - img.x,
newHeight = ev.y - img.y;
const width = ev.useWH ? newHeight * this._shiftData.ratio.wh : img.width;
if (!this._shiftData.held) {
if (
(newWidth - width) * this._shiftData.ratio.hw >
(newHeight - img.height) * this._shiftData.ratio.wh
) {
newHeight = newWidth * this._shiftData.ratio.hw;
} else {
newWidth = newHeight * this._shiftData.ratio.wh;
}
}
img.style.width = `${newWidth}px`;
img.style.height = `${newHeight}px`;
}
const firstChild = editor.children[0] as HTMLDivElement;
if (Number.parseInt(editor.style.flexBasis) < firstChild.offsetHeight) {
editor.style.flexBasis = `${firstChild.offsetHeight}px`;
}
this._pData = { x: ev.x, y: ev.y, eH: editor.clientHeight };
};
private _resizeImageKeyPress = (ev: KeyboardEvent) => {
const oldHeld = this._shiftData.held;
this._shiftData.held = ev.shiftKey;
if (oldHeld !== ev.shiftKey) {
this._resizeImage({
x: this._pData.x,
y: this._pData.y,
useWH: true,
});
}
};
private _resizeStart = (ev: React.MouseEvent<HTMLDivElement>) => {
ev.preventDefault();
const parent = ev.currentTarget.parentNode as HTMLDivElement,
imgEl = parent.querySelector('.file-preview img') as HTMLImageElement,
editor = this._editor();
this._pData = { x: ev.pageX, y: ev.pageY, eH: editor.clientHeight };
this._shiftData.held = ev.shiftKey;
this._shiftData.ratio = { wh: imgEl.width / imgEl.height, hw: imgEl.height / imgEl.width };
parent.dataset.resizing = '1';
imgEl.draggable = false;
editor.addEventListener('mousemove', this._resizeImage);
editor.addEventListener('mouseup', this._resizeEnd);
editor.parentElement.parentElement.parentElement.addEventListener(
'mouseleave',
this._resizeEnd
);
editor.addEventListener('keydown', this._resizeImageKeyPress);
editor.addEventListener('keyup', this._resizeImageKeyPress);
editor.style.flexBasis = `${(editor.children[0] as HTMLDivElement).offsetHeight}px`;
};
private _resizeEnd = (ev: MouseEvent) => {
ev.preventDefault();
const editor = this._editor(),
target = editor.querySelector('.image-attachment-item[data-resizing]') as HTMLDivElement;
if (editor.clientHeight == this._pData.eH && target) {
delete target.dataset.resizing;
(target.querySelector('.file-preview img') as HTMLImageElement).draggable = true;
editor.removeEventListener('mousemove', this._resizeImage);
editor.removeEventListener('mouseup', this._resizeEnd);
editor.parentElement.parentElement.parentElement.removeEventListener(
'mouseleave',
this._resizeEnd
);
editor.removeEventListener('keydown', this._resizeImageKeyPress);
editor.removeEventListener('keyup', this._resizeImageKeyPress);
editor.animate([{ flexBasis: `${(editor.children[0] as HTMLDivElement).offsetHeight}px` }], {
duration: 500,
iterations: 1,
}).onfinish = () => {
editor.style.flexBasis = '';
};
const img = target.querySelector('.file-preview img') as HTMLImageElement;
this.props.onResized(img.width, img.height);
} else {
this._pData.eH = editor.clientHeight;
}
};
}

View file

@ -3,17 +3,18 @@ import { ImageAttachmentItem } from 'mailspring-component-kit';
import { AttachmentStore } from 'mailspring-exports';
import { isQuoteNode } from './base-block-plugins';
import { ComposerEditorPlugin } from './types';
import { Editor, Node } from 'slate';
import { Editor, Inline, Node } from 'slate';
import { schema } from './conversion';
export const IMAGE_TYPE = 'image';
function ImageNode(props) {
const { attributes, node, editor, targetIsHTML, isFocused } = props;
const contentId = node.data.get ? node.data.get('contentId') : node.data.contentId;
const contentId = node.data.get ? node.data.get('contentId') : node.data.contentId,
imgProps = node.data.get ? node.data.get('imgProps') : node.data.imgProps;
if (targetIsHTML) {
return <img alt="" src={`cid:${contentId}`} />;
return <img alt="" src={`cid:${contentId}`} width={imgProps?.width} height={imgProps?.height} />;
}
const { draft } = editor.props.propsForPlugins;
@ -29,6 +30,22 @@ function ImageNode(props) {
filePath={AttachmentStore.pathForFile(file)}
displayName={file.filename}
onRemoveAttachment={() => editor.removeNodeByKey(node.key)}
imgProps={imgProps}
onResized={(width, height) => {
const e = editor as Editor,
n = node as Inline,
newN = {
key: n.key,
object: n.object,
data: n.data.asMutable(),
type: n.type,
nodes: n.nodes.asMutable(),
};
newN.data = newN.data.set('imgProps', { width: width, height: height });
e.setNodeByKey(n.key, newN);
}}
/>
);
}
@ -42,8 +59,9 @@ function renderNode(props, editor: Editor = null, next = () => {}) {
const rules = [
{
deserialize(el, next) {
if (el.tagName.toLowerCase() === 'img' && (el.getAttribute('src') || '').startsWith('cid:')) {
deserialize(el: HTMLElement, next) {
if (el.tagName.toLowerCase() === 'img')
if ((el.getAttribute('src') || '').startsWith('cid:')) {
return {
object: 'inline',
nodes: [],
@ -53,6 +71,10 @@ const rules = [
.getAttribute('src')
.split('cid:')
.pop(),
imgProps: {
width: Number.parseInt(el.getAttribute('width')),
height: Number.parseInt(el.getAttribute('height')),
},
},
};
}

View file

@ -185,10 +185,7 @@ body.platform-win32 {
position: relative;
text-align: center;
display: inline-block;
vertical-align: top;
margin-bottom: @spacing-standard;
margin-right: @spacing-standard;
margin-left: @spacing-standard;
margin: 0;
width: initial;
max-width: calc(~'100% - 30px');
@ -277,4 +274,52 @@ body.platform-win32 {
background-size: 8px;
}
}
.resizer {
align-items: center;
bottom: 0;
background: #000;
color: #fff;
cursor: nwse-resize;
display: flex;
height: 20px;
justify-content: center;
opacity: .3;
position: absolute;
right: 0;
width: 20px;
z-index: 9;
// Thanks: https://css.gg/arrows-expand-left
.gg-arrows-expand-left {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(0.9);
width: 14px;
height: 14px;
box-shadow:
6px 6px 0 -4px,
-6px -6px 0 -4px
}
.gg-arrows-expand-left::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 2px;
height: 22px;
top: -4px;
left: 6px;
transform: rotate(-45deg);
border-top: 9px solid;
border-bottom: 9px solid
}
}
&:hover, &[data-resizing] {
.resizer {
opacity: 1;
}
}
}