From 4ab3e6bc9b41252f8d22900ec05861cc25b6a5e9 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 15 Oct 2023 10:42:20 +0200 Subject: [PATCH] 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. --- webmail/webmail.html | 3 ++ webmail/webmail.js | 22 +++++++++---- webmail/webmail.ts | 74 +++++++++++++++++++++++++++++--------------- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/webmail/webmail.html b/webmail/webmail.html index 8af5d3f02c..c5e151cf22 100644 --- a/webmail/webmail.html +++ b/webmail/webmail.html @@ -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; } diff --git a/webmail/webmail.js b/webmail/webmail.js index 2b6d6e37ad..a2d4d65cda 100644 --- a/webmail/webmail.js +++ b/webmail/webmail.js @@ -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 '), 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 '), 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)); diff --git a/webmail/webmail.ts b/webmail/webmail.ts index e0871dce58..65ac444885 100644 --- a/webmail/webmail.ts +++ b/webmail/webmail.ts @@ -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 '), - 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 '), + 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))