webmail: autoresize address input field in compose window

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.
This commit is contained in:
Mechiel Lukkien 2023-10-15 10:42:20 +02:00
parent 101c2703d2
commit 4ab3e6bc9b
No known key found for this signature in database
3 changed files with 68 additions and 31 deletions

View file

@ -68,6 +68,9 @@ table.search td { padding: .25em; }
.quoted1 { color: #03828f; } .quoted1 { color: #03828f; }
.quoted2 { color: #c7445c; } .quoted2 { color: #c7445c; }
.quoted3 { color: #417c10; } .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; } .scrollparent { position: relative; }
.yscroll { overflow-y: scroll; position: absolute; top: 0; bottom: 0; left: 0; right: 0; } .yscroll { overflow-y: scroll; position: absolute; top: 0; bottom: 0; left: 0; right: 0; }

View file

@ -1959,6 +1959,7 @@ const compose = (opts) => {
let fieldset; let fieldset;
let from; let from;
let customFrom = null; let customFrom = null;
let subjectAutosize;
let subject; let subject;
let body; let body;
let attachments; let attachments;
@ -2042,8 +2043,8 @@ const compose = (opts) => {
if (single && views.length !== 0) { if (single && views.length !== 0) {
return; return;
} }
let input; let autosizeElem, inputElem;
const root = dom.span(input = dom.input(focusPlaceholder('Jane <jane@example.org>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) { const root = dom.span(autosizeElem = dom.span(dom._class('autosize'), inputElem = dom.input(focusPlaceholder('Jane <jane@example.org>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) {
if (e.key === '-' && e.ctrlKey) { if (e.key === '-' && e.ctrlKey) {
remove(); remove();
} }
@ -2055,12 +2056,16 @@ const compose = (opts) => {
} }
e.preventDefault(); e.preventDefault();
e.stopPropagation(); 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(); remove();
if (single && views.length === 0) { if (single && views.length === 0) {
btn.style.display = ''; btn.style.display = '';
} }
}), ' '); }), ' ');
autosizeElem.dataset.value = inputElem.value;
const remove = () => { const remove = () => {
const i = views.indexOf(v); const i = views.indexOf(v);
views.splice(i, 1); views.splice(i, 1);
@ -2087,14 +2092,14 @@ const compose = (opts) => {
next.focus(); next.focus();
} }
}; };
const v = { root: root, input: input }; const v = { root: root, input: inputElem };
views.push(v); views.push(v);
cell.appendChild(v.root); cell.appendChild(v.root);
row.style.display = ''; row.style.display = '';
if (single) { if (single) {
btn.style.display = 'none'; btn.style.display = 'none';
} }
input.focus(); inputElem.focus();
return v; return v;
}; };
let noAttachmentsWarning; let noAttachmentsWarning;
@ -2147,8 +2152,12 @@ const compose = (opts) => {
border: '1px solid #ccc', border: '1px solid #ccc',
padding: '1em', padding: '1em',
minWidth: '40em', minWidth: '40em',
maxWidth: '95vw',
borderRadius: '.25em', 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 // Explicit string object so it doesn't get the highlight-unicode-block-changes
// treatment, which would cause characters to disappear. // 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) { 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) {
@ -2172,6 +2181,7 @@ const compose = (opts) => {
e.preventDefault(); e.preventDefault();
shortcutCmd(cmdSend, shortcuts); 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.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.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow));
(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow)); (opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow));

View file

@ -1159,6 +1159,7 @@ const compose = (opts: ComposeOptions) => {
let fieldset: HTMLFieldSetElement let fieldset: HTMLFieldSetElement
let from: HTMLSelectElement let from: HTMLSelectElement
let customFrom: HTMLInputElement | null = null let customFrom: HTMLInputElement | null = null
let subjectAutosize: HTMLElement
let subject: HTMLInputElement let subject: HTMLInputElement
let body: HTMLTextAreaElement let body: HTMLTextAreaElement
let attachments: HTMLInputElement let attachments: HTMLInputElement
@ -1253,24 +1254,31 @@ const compose = (opts: ComposeOptions) => {
return return
} }
let input: HTMLInputElement let autosizeElem: HTMLElement, inputElem: HTMLInputElement
const root = dom.span( const root = dom.span(
input=dom.input( autosizeElem=dom.span(
focusPlaceholder('Jane <jane@example.org>'), dom._class('autosize'),
style({width: 'auto'}), inputElem=dom.input(
attr.value(addr), focusPlaceholder('Jane <jane@example.org>'),
newAddressComplete(), style({width: 'auto'}),
function keydown(e: KeyboardEvent) { attr.value(addr),
if (e.key === '-' && e.ctrlKey) { newAddressComplete(),
remove() function keydown(e: KeyboardEvent) {
} else if (e.key === '=' && e.ctrlKey) { if (e.key === '-' && e.ctrlKey) {
newAddrView('', views, btn, cell, row, single) remove()
} else { } else if (e.key === '=' && e.ctrlKey) {
return newAddrView('', views, btn, cell, row, single)
} } else {
e.preventDefault() return
e.stopPropagation() }
}, 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() { dom.clickbutton('-', style({padding: '0 .25em'}), attr.arialabel('Remove address.'), attr.title('Remove address.'), function click() {
@ -1281,6 +1289,7 @@ const compose = (opts: ComposeOptions) => {
}), }),
' ', ' ',
) )
autosizeElem.dataset.value = inputElem.value
const remove = () => { const remove = () => {
const i = views.indexOf(v) const i = views.indexOf(v)
@ -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) views.push(v)
cell.appendChild(v.root) cell.appendChild(v.root)
row.style.display = '' row.style.display = ''
if (single) { if (single) {
btn.style.display = 'none' btn.style.display = 'none'
} }
input.focus() inputElem.focus()
return v return v
} }
@ -1375,6 +1384,7 @@ const compose = (opts: ComposeOptions) => {
border: '1px solid #ccc', border: '1px solid #ccc',
padding: '1em', padding: '1em',
minWidth: '40em', minWidth: '40em',
maxWidth: '95vw',
borderRadius: '.25em', borderRadius: '.25em',
}), }),
dom.form( dom.form(
@ -1403,24 +1413,36 @@ const compose = (opts: ComposeOptions) => {
), ),
toRow=dom.tr( toRow=dom.tr(
dom.td('To:', style({textAlign: 'right', color: '#555'})), dom.td('To:', style({textAlign: 'right', color: '#555'})),
toCell=dom.td(style({width: '100%'})), toCell=dom.td(style({lineHeight: '1.5'})),
), ),
replyToRow=dom.tr( replyToRow=dom.tr(
dom.td('Reply-To:', style({textAlign: 'right', color: '#555'})), 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( ccRow=dom.tr(
dom.td('Cc:', style({textAlign: 'right', color: '#555'})), dom.td('Cc:', style({textAlign: 'right', color: '#555'})),
ccCell=dom.td(style({width: '100%'})), ccCell=dom.td(style({lineHeight: '1.5'})),
), ),
bccRow=dom.tr( bccRow=dom.tr(
dom.td('Bcc:', style({textAlign: 'right', color: '#555'})), dom.td('Bcc:', style({textAlign: 'right', color: '#555'})),
bccCell=dom.td(style({width: '100%'})), bccCell=dom.td(style({lineHeight: '1.5'})),
), ),
dom.tr( dom.tr(
dom.td('Subject:', style({textAlign: 'right', color: '#555'})), dom.td('Subject:', style({textAlign: 'right', color: '#555'})),
dom.td(style({width: '100%'}), dom.td(
subject=dom.input(focusPlaceholder('subject...'), attr.value(opts.subject || ''), attr.required(''), style({width: '100%'})), 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
},
),
),
), ),
), ),
), ),
@ -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.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.cc || []).forEach(s => newAddrView(s, ccViews, ccBtn, ccCell, ccRow))
;(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow)) ;(opts.bcc || []).forEach(s => newAddrView(s, bccViews, bccBtn, bccCell, bccRow))