mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-11-09 16:01:42 +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 changed files with 243 additions and 29 deletions
4
app/package-lock.json
generated
4
app/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mailspring",
|
"name": "mailspring",
|
||||||
"version": "1.10.5",
|
"version": "1.10.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mailspring",
|
"name": "mailspring",
|
||||||
"version": "1.10.5",
|
"version": "1.10.7",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108",
|
"@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, { Component } from 'react';
|
import React, { Component, CSSProperties } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import * as Actions from '../flux/actions';
|
import * as Actions from '../flux/actions';
|
||||||
|
|
@ -67,7 +67,9 @@ function buildContextMenu(fns: {
|
||||||
label: localized('Save Into...'),
|
label: localized('Save Into...'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
require('@electron/remote').Menu.buildFromTemplate(template).popup({});
|
require('@electron/remote')
|
||||||
|
.Menu.buildFromTemplate(template)
|
||||||
|
.popup({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProgressBar: React.FunctionComponent<{
|
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 displayName = 'ImageAttachmentItem';
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
imgProps: PropTypes.object,
|
imgProps: PropTypes.object,
|
||||||
|
onResized: PropTypes.func,
|
||||||
...propTypes,
|
...propTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -308,7 +316,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
||||||
};
|
};
|
||||||
|
|
||||||
renderImage() {
|
renderImage() {
|
||||||
const { download, filePath, draggable } = this.props;
|
const { download, filePath, draggable, imgProps } = this.props;
|
||||||
if (download && download.percent <= 5) {
|
if (download && download.percent <= 5) {
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', height: '100px' }}>
|
<div style={{ width: '100%', height: '100px' }}>
|
||||||
|
|
@ -316,9 +324,22 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const src =
|
const src =
|
||||||
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath;
|
download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath,
|
||||||
return <img draggable={draggable} src={src} alt="" onLoad={this._onImgLoaded} />;
|
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() {
|
componentDidMount() {
|
||||||
|
|
@ -340,6 +361,7 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
||||||
onSaveAttachment,
|
onSaveAttachment,
|
||||||
...extraProps
|
...extraProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`nylas-attachment-item image-attachment-item ${className || ''}`}
|
className={`nylas-attachment-item image-attachment-item ${className || ''}`}
|
||||||
|
|
@ -366,7 +388,132 @@ export class ImageAttachmentItem extends Component<AttachmentItemProps & { imgPr
|
||||||
{this.renderImage()}
|
{this.renderImage()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="resizer" onMouseDown={this._resizeStart}>
|
||||||
|
<i className="gg-arrows-expand-left"></i>
|
||||||
|
</div>
|
||||||
</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 { AttachmentStore } from 'mailspring-exports';
|
||||||
import { isQuoteNode } from './base-block-plugins';
|
import { isQuoteNode } from './base-block-plugins';
|
||||||
import { ComposerEditorPlugin } from './types';
|
import { ComposerEditorPlugin } from './types';
|
||||||
import { Editor, Node } from 'slate';
|
import { Editor, Inline, Node } from 'slate';
|
||||||
import { schema } from './conversion';
|
import { schema } from './conversion';
|
||||||
|
|
||||||
export const IMAGE_TYPE = 'image';
|
export const IMAGE_TYPE = 'image';
|
||||||
|
|
||||||
function ImageNode(props) {
|
function ImageNode(props) {
|
||||||
const { attributes, node, editor, targetIsHTML, isFocused } = 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) {
|
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;
|
const { draft } = editor.props.propsForPlugins;
|
||||||
|
|
@ -29,6 +30,22 @@ function ImageNode(props) {
|
||||||
filePath={AttachmentStore.pathForFile(file)}
|
filePath={AttachmentStore.pathForFile(file)}
|
||||||
displayName={file.filename}
|
displayName={file.filename}
|
||||||
onRemoveAttachment={() => editor.removeNodeByKey(node.key)}
|
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,20 +59,25 @@ function renderNode(props, editor: Editor = null, next = () => {}) {
|
||||||
|
|
||||||
const rules = [
|
const rules = [
|
||||||
{
|
{
|
||||||
deserialize(el, next) {
|
deserialize(el: HTMLElement, next) {
|
||||||
if (el.tagName.toLowerCase() === 'img' && (el.getAttribute('src') || '').startsWith('cid:')) {
|
if (el.tagName.toLowerCase() === 'img')
|
||||||
return {
|
if ((el.getAttribute('src') || '').startsWith('cid:')) {
|
||||||
object: 'inline',
|
return {
|
||||||
nodes: [],
|
object: 'inline',
|
||||||
type: IMAGE_TYPE,
|
nodes: [],
|
||||||
data: {
|
type: IMAGE_TYPE,
|
||||||
contentId: el
|
data: {
|
||||||
.getAttribute('src')
|
contentId: el
|
||||||
.split('cid:')
|
.getAttribute('src')
|
||||||
.pop(),
|
.split('cid:')
|
||||||
},
|
.pop(),
|
||||||
};
|
imgProps: {
|
||||||
}
|
width: Number.parseInt(el.getAttribute('width')),
|
||||||
|
height: Number.parseInt(el.getAttribute('height')),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
serialize(obj, children) {
|
serialize(obj, children) {
|
||||||
if (obj.object !== 'inline') return;
|
if (obj.object !== 'inline') return;
|
||||||
|
|
|
||||||
|
|
@ -185,10 +185,7 @@ body.platform-win32 {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
margin: 0;
|
||||||
margin-bottom: @spacing-standard;
|
|
||||||
margin-right: @spacing-standard;
|
|
||||||
margin-left: @spacing-standard;
|
|
||||||
width: initial;
|
width: initial;
|
||||||
max-width: calc(~'100% - 30px');
|
max-width: calc(~'100% - 30px');
|
||||||
|
|
||||||
|
|
@ -277,4 +274,52 @@ body.platform-win32 {
|
||||||
background-size: 8px;
|
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…
Add table
Reference in a new issue