diff --git a/chrome/content/zotero/elements/editableText.js b/chrome/content/zotero/elements/editableText.js index 54e99dc324..7ff37d9a43 100644 --- a/chrome/content/zotero/elements/editableText.js +++ b/chrome/content/zotero/elements/editableText.js @@ -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 || ''); diff --git a/chrome/content/zotero/elements/tagsBox.js b/chrome/content/zotero/elements/tagsBox.js index 0b3ff4dfc9..1c82cc15c7 100644 --- a/chrome/content/zotero/elements/tagsBox.js +++ b/chrome/content/zotero/elements/tagsBox.js @@ -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; diff --git a/scss/elements/_editableText.scss b/scss/elements/_editableText.scss index 446f5782f7..1bb8dd16de 100644 --- a/scss/elements/_editableText.scss +++ b/scss/elements/_editableText.scss @@ -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 -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; + } }