Skip to content

Commit

Permalink
webmail: autoresize address input field in compose window
Browse files Browse the repository at this point in the history
so full name/email address is visible.

using a hidden grid element that gets the same content as the input element.
from https://css-tricks.com/auto-growing-inputs-textareas/

a recent commit probably also make the compose window full-screen-width on
chrome, this restores to the intended behaviour of a less wide default size.

if you add multiple address fields, the compose window will still grow. not
great, in the future, we should make the compose window resizable by dragging.
  • Loading branch information
mjl- committed Oct 15, 2023
1 parent 101c270 commit 4ab3e6b
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 31 deletions.
3 changes: 3 additions & 0 deletions webmail/webmail.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@
.quoted1 { color: #03828f; }
.quoted2 { color: #c7445c; }
.quoted3 { color: #417c10; }
.autosize { display: inline-grid; max-width: 90vw; }
.autosize.input { grid-area: 1 / 2; }
.autosize::after { content: attr(data-value); margin-right: 1em; line-height: 0; visibility: hidden; white-space: pre-wrap; overflow-x: hidden; }

.scrollparent { position: relative; }
.yscroll { overflow-y: scroll; position: absolute; top: 0; bottom: 0; left: 0; right: 0; }
Expand Down
22 changes: 16 additions & 6 deletions webmail/webmail.js
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,7 @@ const compose = (opts) => {
let fieldset;
let from;
let customFrom = null;
let subjectAutosize;
let subject;
let body;
let attachments;
Expand Down Expand Up @@ -2042,8 +2043,8 @@ const compose = (opts) => {
if (single && views.length !== 0) {
return;
}
let input;
const root = dom.span(input = dom.input(focusPlaceholder('Jane <[email protected]>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) {
let autosizeElem, inputElem;
const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane <[email protected]>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) {
if (e.key === '-' && e.ctrlKey) {
remove();
}
Expand All @@ -2055,12 +2056,16 @@ const compose = (opts) => {
}
e.preventDefault();
e.stopPropagation();
}), ' ', dom.clickbutton('-', style({ padding: '0 .25em' }), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() {
}, function input() {
// data-value is used for size of ::after css pseudo-element to stretch input field.
autosizeElem.dataset.value = inputElem.value;
})), ' ', dom.clickbutton('-', style({ padding: '0 .25em' }), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() {
remove();
if (single && views.length === 0) {
btn.style.display = '';
}
}), ' ');
autosizeElem.dataset.value = inputElem.value;
const remove = () => {
const i = views.indexOf(v);
views.splice(i, 1);
Expand All @@ -2087,14 +2092,14 @@ const compose = (opts) => {
next.focus();
}
};
const v = { root: root, input: input };
const v = { root: root, input: inputElem };
views.push(v);
cell.appendChild(v.root);
row.style.display = '';
if (single) {
btn.style.display = 'none';
}
input.focus();
inputElem.focus();
return v;
};
let noAttachmentsWarning;
Expand Down Expand Up @@ -2147,8 +2152,12 @@ const compose = (opts) => {
border: '1px solid #ccc',
padding: '1em',
minWidth: '40em',
maxWidth: '95vw',
borderRadius: '.25em',
}), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ width: '100%' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ width: '100%' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ width: '100%' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ width: '100%' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(style({ width: '100%' }), subject = dom.input(focusPlaceholder('subject...'), attr.value(opts.subject || ''), attr.required(''), style({ width: '100%' }))))), body = dom.textarea(dom._class('mono'), attr.rows('15'), style({ width: '100%' }),
}), dom.form(fieldset = dom.fieldset(dom.table(style({ width: '100%' }), dom.tr(dom.td(style({ textAlign: 'right', color: '#555' }), dom.span('From:')), dom.td(dom.clickbutton('Cancel', style({ float: 'right' }), attr.title('Close window, discarding message.'), clickCmd(cmdCancel, shortcuts)), from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts)))), toRow = dom.tr(dom.td('To:', style({ textAlign: 'right', color: '#555' })), toCell = dom.td(style({ lineHeight: '1.5' }))), replyToRow = dom.tr(dom.td('Reply-To:', style({ textAlign: 'right', color: '#555' })), replyToCell = dom.td(style({ lineHeight: '1.5' }))), ccRow = dom.tr(dom.td('Cc:', style({ textAlign: 'right', color: '#555' })), ccCell = dom.td(style({ lineHeight: '1.5' }))), bccRow = dom.tr(dom.td('Bcc:', style({ textAlign: 'right', color: '#555' })), bccCell = dom.td(style({ lineHeight: '1.5' }))), dom.tr(dom.td('Subject:', style({ textAlign: 'right', color: '#555' })), dom.td(subjectAutosize = dom.span(dom._class('autosize'), style({ width: '100%' }), // Without 100% width, the span takes minimal width for input, we want the full table cell.
subject = dom.input(style({ width: '100%' }), attr.value(opts.subject || ''), attr.required(''), focusPlaceholder('subject...'), function input() {
subjectAutosize.dataset.value = subject.value;
}))))), body = dom.textarea(dom._class('mono'), attr.rows('15'), style({ width: '100%' }),
// Explicit string object so it doesn't get the highlight-unicode-block-changes
// treatment, which would cause characters to disappear.
new String(opts.body || ''), opts.body && !opts.isForward && !opts.body.startsWith('\n\n') ? prop({ selectionStart: opts.body.length, selectionEnd: opts.body.length }) : [], function keyup(e) {
Expand All @@ -2172,6 +2181,7 @@ const compose = (opts) => {
e.preventDefault();
shortcutCmd(cmdSend, shortcuts);
}));
subjectAutosize.dataset.value = subject.value;
(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, toViews, toBtn, toCell, toRow));
(opts.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow));
(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow));
Expand Down
74 changes: 49 additions & 25 deletions webmail/webmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ const compose = (opts: ComposeOptions) => {
let fieldset: HTMLFieldSetElement
let from: HTMLSelectElement
let customFrom: HTMLInputElement | null = null
let subjectAutosize: HTMLElement
let subject: HTMLInputElement
let body: HTMLTextAreaElement
let attachments: HTMLInputElement
Expand Down Expand Up @@ -1253,24 +1254,31 @@ const compose = (opts: ComposeOptions) => {
return
}

let input: HTMLInputElement
let autosizeElem: HTMLElement, inputElem: HTMLInputElement
const root = dom.span(
input=dom.input(
focusPlaceholder('Jane <[email protected]>'),
style({width: 'auto'}),
attr.value(addr),
newAddressComplete(),
function keydown(e: KeyboardEvent) {
if (e.key === '-' && e.ctrlKey) {
remove()
} else if (e.key === '=' && e.ctrlKey) {
newAddrView('', views, btn, cell, row, single)
} else {
return
}
e.preventDefault()
e.stopPropagation()
},
autosizeElem=dom.span(
dom._class('autosize'),
inputElem=dom.input(
focusPlaceholder('Jane <[email protected]>'),
style({width: 'auto'}),
attr.value(addr),
newAddressComplete(),
function keydown(e: KeyboardEvent) {
if (e.key === '-' && e.ctrlKey) {
remove()
} else if (e.key === '=' && e.ctrlKey) {
newAddrView('', views, btn, cell, row, single)
} else {
return
}
e.preventDefault()
e.stopPropagation()
},
function input() {
// data-value is used for size of ::after css pseudo-element to stretch input field.
autosizeElem.dataset.value = inputElem.value
},
),
),
' ',
dom.clickbutton('-', style({padding: '0 .25em'}), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() {
Expand All @@ -1281,6 +1289,7 @@ const compose = (opts: ComposeOptions) => {
}),
' ',
)
autosizeElem.dataset.value = inputElem.value

const remove = () => {
const i = views.indexOf(v)
Expand Down Expand Up @@ -1310,14 +1319,14 @@ const compose = (opts: ComposeOptions) => {
}
}

const v: AddrView = {root: root, input: input}
const v: AddrView = {root: root, input: inputElem}
views.push(v)
cell.appendChild(v.root)
row.style.display = ''
if (single) {
btn.style.display = 'none'
}
input.focus()
inputElem.focus()
return v
}

Expand Down Expand Up @@ -1375,6 +1384,7 @@ const compose = (opts: ComposeOptions) => {
border: '1px solid #ccc',
padding: '1em',
minWidth: '40em',
maxWidth: '95vw',
borderRadius: '.25em',
}),
dom.form(
Expand Down Expand Up @@ -1403,24 +1413,36 @@ const compose = (opts: ComposeOptions) => {
),
toRow=dom.tr(
dom.td('To:', style({textAlign: 'right', color: '#555'})),
toCell=dom.td(style({width: '100%'})),
toCell=dom.td(style({lineHeight: '1.5'})),
),
replyToRow=dom.tr(
dom.td('Reply-To:', style({textAlign: 'right', color: '#555'})),
replyToCell=dom.td(style({width: '100%'})),
replyToCell=dom.td(style({lineHeight: '1.5'})),
),
ccRow=dom.tr(
dom.td('Cc:', style({textAlign: 'right', color: '#555'})),
ccCell=dom.td(style({width: '100%'})),
ccCell=dom.td(style({lineHeight: '1.5'})),
),
bccRow=dom.tr(
dom.td('Bcc:', style({textAlign: 'right', color: '#555'})),
bccCell=dom.td(style({width: '100%'})),
bccCell=dom.td(style({lineHeight: '1.5'})),
),
dom.tr(
dom.td('Subject:', style({textAlign: 'right', color: '#555'})),
dom.td(style({width: '100%'}),
subject=dom.input(focusPlaceholder('subject...'), attr.value(opts.subject || ''), attr.required(''), style({width: '100%'})),
dom.td(
subjectAutosize=dom.span(
dom._class('autosize'),
style({width: '100%'}), // Without 100% width, the span takes minimal width for input, we want the full table cell.
subject=dom.input(
style({width: '100%'}),
attr.value(opts.subject || ''),
attr.required(''),
focusPlaceholder('subject...'),
function input() {
subjectAutosize.dataset.value = subject.value
},
),
),
),
),
),
Expand Down Expand Up @@ -1465,6 +1487,8 @@ const compose = (opts: ComposeOptions) => {
),
)

subjectAutosize.dataset.value = subject.value

;(opts.to && opts.to.length > 0 ? opts.to : ['']).forEach(s => newAddrView(s, toViews, toBtn, toCell, toRow))
;(opts.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow))
;(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow))
Expand Down

0 comments on commit 4ab3e6b

Please sign in to comment.