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; }
.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; }

View file

@ -1959,6 +1959,7 @@ const compose = (opts) => {
let fieldset;
let from;
let customFrom = null;
let subjectAutosize;
let subject;
let body;
let attachments;
@ -2042,8 +2043,8 @@ const compose = (opts) => {
if (single && views.length !== 0) {
return;
}
let input;
const root = dom.span(input = dom.input(focusPlaceholder('Jane <jane@example.org>'), 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 <jane@example.org>'), style({ width: 'auto' }), attr.value(addr), newAddressComplete(), function keydown(e) {
if (e.key === '-' && e.ctrlKey) {
remove();
}
@ -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);
@ -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;
@ -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) {
@ -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));

View file

@ -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
@ -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 <jane@example.org>'),
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 <jane@example.org>'),
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() {
@ -1281,6 +1289,7 @@ const compose = (opts: ComposeOptions) => {
}),
' ',
)
autosizeElem.dataset.value = inputElem.value
const remove = () => {
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)
cell.appendChild(v.root)
row.style.display = ''
if (single) {
btn.style.display = 'none'
}
input.focus()
inputElem.focus()
return v
}
@ -1375,6 +1384,7 @@ const compose = (opts: ComposeOptions) => {
border: '1px solid #ccc',
padding: '1em',
minWidth: '40em',
maxWidth: '95vw',
borderRadius: '.25em',
}),
dom.form(
@ -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
},
),
),
),
),
),
@ -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))