mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 15:26:06 +08:00
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:
parent
0c2c1c3465
commit
9305bc8ac1
4
app/package-lock.json
generated
4
app/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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')),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue