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:
parent
55b97cd397
commit
553d1f6b3c
3 changed files with 119 additions and 15 deletions
|
@ -29,8 +29,21 @@
|
||||||
class EditableText extends XULElementBase {
|
class EditableText extends XULElementBase {
|
||||||
_input;
|
_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() {
|
get multiline() {
|
||||||
return this.hasAttribute('multiline');
|
return this.hasAttribute('multiline');
|
||||||
}
|
}
|
||||||
|
@ -68,6 +81,10 @@
|
||||||
get ariaLabel() {
|
get ariaLabel() {
|
||||||
return this.getAttribute('aria-label') || '';
|
return this.getAttribute('aria-label') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ariaLabelledBy() {
|
||||||
|
return this.getAttribute('aria-labelledby') || '';
|
||||||
|
}
|
||||||
|
|
||||||
set ariaLabel(ariaLabel) {
|
set ariaLabel(ariaLabel) {
|
||||||
this.setAttribute('aria-label', ariaLabel);
|
this.setAttribute('aria-label', ariaLabel);
|
||||||
|
@ -115,6 +132,18 @@
|
||||||
get ref() {
|
get ref() {
|
||||||
return this._input;
|
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() {
|
attributeChangedCallback() {
|
||||||
this.render();
|
this.render();
|
||||||
|
@ -134,7 +163,7 @@
|
||||||
input.type = 'autocomplete';
|
input.type = 'autocomplete';
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
input = document.createElement('textarea');
|
input = this.noWrap ? document.createElement('input') : document.createElement('textarea');
|
||||||
input.rows = 1;
|
input.rows = 1;
|
||||||
}
|
}
|
||||||
input.classList.add('input');
|
input.classList.add('input');
|
||||||
|
@ -147,24 +176,38 @@
|
||||||
let handleChange = () => {
|
let handleChange = () => {
|
||||||
this.value = this._input.value;
|
this.value = this._input.value;
|
||||||
};
|
};
|
||||||
|
input.addEventListener('mousedown', () => {
|
||||||
|
this.setAttribute("mousedown", true);
|
||||||
|
});
|
||||||
input.addEventListener('input', handleInput);
|
input.addEventListener('input', handleInput);
|
||||||
input.addEventListener('change', handleChange);
|
input.addEventListener('change', handleChange);
|
||||||
input.addEventListener('focus', () => {
|
input.addEventListener('focus', () => {
|
||||||
this.dispatchEvent(new CustomEvent('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;
|
this._input.dataset.initialValue = this._input.value;
|
||||||
});
|
});
|
||||||
input.addEventListener('blur', () => {
|
input.addEventListener('blur', () => {
|
||||||
this.dispatchEvent(new CustomEvent('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;
|
delete this._input.dataset.initialValue;
|
||||||
});
|
});
|
||||||
input.addEventListener('keydown', (event) => {
|
input.addEventListener('keydown', (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
if (this.multiline === event.shiftKey) {
|
if (this.multiline === event.shiftKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
this.dispatchEvent(new CustomEvent('escape_enter'));
|
||||||
this._input.blur();
|
this._input.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (event.key === 'Escape') {
|
else if (event.key === 'Escape') {
|
||||||
|
this.dispatchEvent(new CustomEvent('escape_enter'));
|
||||||
this._input.value = this.value = this._input.dataset.initialValue;
|
this._input.value = this.value = this._input.dataset.initialValue;
|
||||||
this._input.blur();
|
this._input.blur();
|
||||||
}
|
}
|
||||||
|
@ -195,9 +238,19 @@
|
||||||
}
|
}
|
||||||
this._input.readOnly = this.readOnly;
|
this._input.readOnly = this.readOnly;
|
||||||
this._input.placeholder = this.label;
|
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;
|
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) {
|
if (autocompleteEnabled) {
|
||||||
this._input.setAttribute('autocomplete', 'on');
|
this._input.setAttribute('autocomplete', 'on');
|
||||||
this._input.setAttribute('autocompletepopup', autocompleteParams.popup || '');
|
this._input.setAttribute('autocompletepopup', autocompleteParams.popup || '');
|
||||||
|
|
|
@ -340,6 +340,8 @@
|
||||||
var valueElement = document.createXULElement("editable-text");
|
var valueElement = document.createXULElement("editable-text");
|
||||||
valueElement.setAttribute('fieldname', 'tag');
|
valueElement.setAttribute('fieldname', 'tag');
|
||||||
valueElement.setAttribute('flex', 1);
|
valueElement.setAttribute('flex', 1);
|
||||||
|
valueElement.setAttribute('nowrap', true);
|
||||||
|
valueElement.setAttribute('tight', true);
|
||||||
valueElement.className = 'zotero-box-label';
|
valueElement.className = 'zotero-box-label';
|
||||||
valueElement.readOnly = !this.editable;
|
valueElement.readOnly = !this.editable;
|
||||||
valueElement.value = valueText;
|
valueElement.value = valueText;
|
||||||
|
|
|
@ -17,52 +17,101 @@ editable-text {
|
||||||
--max-visible-lines: 20;
|
--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:
|
// Fun auto-sizing approach from CSSTricks:
|
||||||
// https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
|
// https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
&::after {
|
span {
|
||||||
|
visibility: hidden;
|
||||||
|
margin: 1px;
|
||||||
|
width: fit-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([nowrap])::after {
|
||||||
content: attr(value) ' ';
|
content: attr(value) ' ';
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
}
|
|
||||||
|
|
||||||
&::after, .input {
|
|
||||||
grid-area: 1 / 1 / 2 / 2;
|
|
||||||
padding: var(--editable-text-padding-block) var(--editable-text-padding-inline);
|
padding: var(--editable-text-padding-block) var(--editable-text-padding-inline);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
color: 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;
|
white-space: pre-wrap;
|
||||||
max-height: calc(2ex * var(--max-visible-lines));
|
max-height: calc(2ex * var(--max-visible-lines));
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
|
@include focus-ring;
|
||||||
// Necessary for consistent padding, even if it's actually an <input>
|
// Necessary for consistent padding, even if it's actually an <input>
|
||||||
-moz-default-appearance: textarea;
|
-moz-default-appearance: textarea;
|
||||||
|
|
||||||
min-height: calc(2ex * var(--min-visible-lines));
|
min-height: calc(2ex * var(--min-visible-lines));
|
||||||
margin: 0;
|
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) {
|
&:read-only, &:not(:focus) {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(:read-only, :focus) {
|
&:hover:not(:read-only, :focus) {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 1px solid var(--fill-tertiary);
|
border: 1px solid var(--fill-tertiary);
|
||||||
box-shadow: 0 0 0 1px var(--fill-quinary);
|
box-shadow: 0 0 0 1px var(--fill-quinary);
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
color: var(--fill-tertiary);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue