editable-text updates

- Added nowrap attribute to not add the stretching aspect
of the editable-text and to not have any text wrapping. To be used in cases when
the input is a single line and it's width is stretched with flexbox, for
example tags or itemBox value fields.
- Added 'tight' attribute to set lower padding values on editable-text.
To be applied on fields that are not as prominent as abstract or header
as it makes the actual editable-text component smaller.
- Minor style changes to not shift layout on focus on windows (using
transparent border instead of 0 margin).
- No overflow of textarea to avoid longer-than-needed textareas on
windows.
- Keep track if the component was focused on via mouse click or not.
If the focus happened without prior mouse click, select all text.
- Reset cursor and selection on blur.
- Make sure the .input is always re-added if it disappears after drag-drop.
- Added sizeToContent function to set max-width based on the width
of the input.
- Allow the browser to break up words more freely with overflow-wrap: anywhere
to avoid stretching the itemBox with extra long header without spaces.

-  Apply 'nowrap' and 'tight' to tags so that a long tag does not stretch
the itemBox or make the editable-text extra tall.
This commit is contained in:
Bogdan Abaev 2023-12-21 09:18:57 -05:00 committed by Dan Stillman
parent 55b97cd397
commit 553d1f6b3c
3 changed files with 119 additions and 15 deletions

View file

@ -29,8 +29,21 @@
class EditableText extends XULElementBase {
_input;
static observedAttributes = ['multiline', 'readonly', 'placeholder', 'label', 'aria-label', 'value'];
static observedAttributes = [
'multiline',
'readonly',
'placeholder',
'label',
'aria-label',
'aria-labelledby',
'value',
'nowrap'
];
get noWrap() {
return this.hasAttribute('nowrap');
}
get multiline() {
return this.hasAttribute('multiline');
}
@ -68,6 +81,10 @@
get ariaLabel() {
return this.getAttribute('aria-label') || '';
}
get ariaLabelledBy() {
return this.getAttribute('aria-labelledby') || '';
}
set ariaLabel(ariaLabel) {
this.setAttribute('aria-label', ariaLabel);
@ -115,6 +132,18 @@
get ref() {
return this._input;
}
sizeToContent = () => {
// Add a temp span, fetch it's width with current paddings and set max-width based on that
let span = document.createElement("span");
span.innerText = this.value;
this.append(span);
let size = span.getBoundingClientRect();
let inlinePadding = getComputedStyle(this).getPropertyValue('--editable-text-padding-inline');
let blockPadding = getComputedStyle(this).getPropertyValue('--editable-text-padding-block');
this.style['max-width'] = `calc(${size.width}px + 2*${inlinePadding} + 2*${blockPadding})`;
this.querySelector("span").remove();
};
attributeChangedCallback() {
this.render();
@ -134,7 +163,7 @@
input.type = 'autocomplete';
}
else {
input = document.createElement('textarea');
input = this.noWrap ? document.createElement('input') : document.createElement('textarea');
input.rows = 1;
}
input.classList.add('input');
@ -147,24 +176,38 @@
let handleChange = () => {
this.value = this._input.value;
};
input.addEventListener('mousedown', () => {
this.setAttribute("mousedown", true);
});
input.addEventListener('input', handleInput);
input.addEventListener('change', handleChange);
input.addEventListener('focus', () => {
this.dispatchEvent(new CustomEvent('focus'));
this.classList.add("focused");
// Select all text if focused via keyboard
if (!this.getAttribute("mousedown")) {
this._input.select();
}
this._input.dataset.initialValue = this._input.value;
});
input.addEventListener('blur', () => {
this.dispatchEvent(new CustomEvent('blur'));
this.classList.remove("focused");
this._input.scrollLeft = 0;
this._input.setSelectionRange(0, 0);
this.removeAttribute("mousedown");
delete this._input.dataset.initialValue;
});
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (this.multiline === event.shiftKey) {
event.preventDefault();
this.dispatchEvent(new CustomEvent('escape_enter'));
this._input.blur();
}
}
else if (event.key === 'Escape') {
this.dispatchEvent(new CustomEvent('escape_enter'));
this._input.value = this.value = this._input.dataset.initialValue;
this._input.blur();
}
@ -195,9 +238,19 @@
}
this._input.readOnly = this.readOnly;
this._input.placeholder = this.label;
this._input.setAttribute('aria-label', this.ariaLabel);
if (this.ariaLabel.length) {
this._input.setAttribute('aria-label', this.ariaLabel);
}
if (this.ariaLabelledBy.length) {
this._input.setAttribute('aria-labelledby', this.ariaLabelledBy);
}
this._input.value = this.value;
// The actual input node can disappear if the component is moved
if (this.childElementCount == 0) {
this.replaceChildren(this._input);
}
if (autocompleteEnabled) {
this._input.setAttribute('autocomplete', 'on');
this._input.setAttribute('autocompletepopup', autocompleteParams.popup || '');

View file

@ -340,6 +340,8 @@
var valueElement = document.createXULElement("editable-text");
valueElement.setAttribute('fieldname', 'tag');
valueElement.setAttribute('flex', 1);
valueElement.setAttribute('nowrap', true);
valueElement.setAttribute('tight', true);
valueElement.className = 'zotero-box-label';
valueElement.readOnly = !this.editable;
valueElement.value = valueText;

View file

@ -17,52 +17,101 @@ editable-text {
--max-visible-lines: 20;
}
&[nowrap] {
--min-visible-lines: 1;
}
&[tight] {
@include comfortable {
--editable-text-padding-inline: 4px;
--editable-text-padding-block: 2px;
}
@include compact {
--editable-text-padding-inline: 3px;
--editable-text-padding-block: 1px;
}
}
// Fun auto-sizing approach from CSSTricks:
// https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
display: grid;
&::after {
span {
visibility: hidden;
margin: 1px;
width: fit-content;
white-space: nowrap;
}
&:not([nowrap])::after {
content: attr(value) ' ';
visibility: hidden;
margin: 1px;
}
&::after, .input {
grid-area: 1 / 1 / 2 / 2;
padding: var(--editable-text-padding-block) var(--editable-text-padding-inline);
font: inherit;
line-height: inherit;
color: inherit;
overflow-wrap: break-word;
}
&:not([nowrap])::after, &:not([nowrap]) .input {
grid-area: 1 / 1 / 2 / 2;
overflow-wrap: anywhere;
white-space: pre-wrap;
max-height: calc(2ex * var(--max-visible-lines));
}
.input {
@include focus-ring;
// Necessary for consistent padding, even if it's actually an <input>
-moz-default-appearance: textarea;
min-height: calc(2ex * var(--min-visible-lines));
margin: 0;
border: 1px solid transparent;
font: inherit;
line-height: inherit;
color: inherit;
padding: var(--editable-text-padding-block) var(--editable-text-padding-inline);
&:read-only, &:not(:focus) {
appearance: none;
border: none;
outline: none;
background: transparent;
margin: 1px;
}
&:hover:not(:read-only, :focus) {
border-radius: 5px;
border: 1px solid var(--fill-tertiary);
box-shadow: 0 0 0 1px var(--fill-quinary);
margin: 0;
box-shadow: 0 0 0 1px var(--fill-quinary);
}
::placeholder {
color: var(--fill-tertiary);
}
}
&[multiline] {
&::after, .input {
overflow-y: auto;
}
.input {
min-height: 5em;
}
}
&[nowrap] {
.input:not(:focus, :hover) {
text-overflow: ellipsis;
}
}
&[hidden]{
display: none;
}
// somehow it fixes extra tall textareas on windows that was rendered to have at least
// 3 rows even when there's no text
textarea {
overflow: hidden;
}
}