diff --git a/chrome/content/zotero/elements/bubbleInput.js b/chrome/content/zotero/elements/bubbleInput.js index 6f93259659..f5460f0893 100644 --- a/chrome/content/zotero/elements/bubbleInput.js +++ b/chrome/content/zotero/elements/bubbleInput.js @@ -36,6 +36,7 @@ this._body = this.querySelector('.bubble-input.body'); this._body.addEventListener('click', this._onBodyClick.bind(this)); this._lastFocusedInput = null; + this.showJustAddedPlaceholder = false; Utils.init(this); DragDropHandler.init(this); @@ -123,11 +124,10 @@ if (isOnlyInput) { document.l10n.setAttributes(this._body.firstChild, `integration-citationDialog-single-input-${dialogType}`); } - // otherwise, add a regular aria descriptions and placeholders to all inputs + // otherwise, set default placeholders for all inputs, with special handling + // of the last input after a bubble is added with visible placeholder else { - for (let input of [...this.querySelectorAll(".input")]) { - document.l10n.setAttributes(input, `integration-citationDialog-input-${dialogType}`); - } + Utils.setupInputPlaceholders(this.showJustAddedPlaceholder, dialogType); } // If any two inputs end up next to each other (e.g. after bubble is deleted), // have them merged @@ -604,6 +604,74 @@ return spanWidth; }, + getTextWidth(text) { + let span = document.createElement("span"); + span.classList = "input"; + span.innerText = text; + this.bubbleInput._body.appendChild(span); + let spanWidth = span.getBoundingClientRect().width; + span.remove(); + return spanWidth; + }, + + // Return the longest prefix of `text` such that prefix + "…" fits within maxWidth, + // or the original text if it already fits. Used for placeholder truncation, since + // text-overflow:ellipsis doesn't work on in Firefox chrome. + truncateToWidth(text, maxWidth) { + if (this.getTextWidth(text) <= maxWidth) return text; + let ellipsis = "…"; + for (let i = text.length - 1; i > 0; i--) { + let candidate = text.slice(0, i) + ellipsis; + if (this.getTextWidth(candidate) <= maxWidth) { + return candidate; + } + } + return ellipsis; + }, + + // Set aria-descriptions and placeholders on every input in the bubble-input. + // For the just-added input (the last one, when showJustAddedPlaceholder is set), the + // placeholder is truncated to the space remaining on its line and passed to Fluent as + // a variable. + setupInputPlaceholders(showJustAddedPlaceholder, dialogType) { + let allInputs = [...this.bubbleInput.querySelectorAll(".input")]; + let lastInput = allInputs[allInputs.length - 1]; + let bodyRight = this.bubbleInput._body.getBoundingClientRect().right; + for (let input of allInputs) { + let isJustAdded = showJustAddedPlaceholder && input === lastInput; + if (!isJustAdded) { + input.classList.remove("just-added-placeholder"); + document.l10n.setAttributes(input, `integration-citationDialog-input-${dialogType}`); + // Clear any stale title and min-width left over from a previous just-added state + input.removeAttribute("title"); + input.style.minWidth = ""; + } + if (isJustAdded && !input.classList.contains("just-added-placeholder")) { + input.classList.add("just-added-placeholder"); + // Leading NBSP gives a small visual gap between the cursor and the placeholder + // text (CSS padding/text-indent on input or ::placeholder both move the cursor too) + let fullPlaceholder = " " + Zotero.getString("integration-citationDialog-just-added-input-placeholder"); + let availableWidth = bodyRight - input.getBoundingClientRect().left - 20; + let placeholderWidth = this.getTextWidth(fullPlaceholder); + let placeholder, title, minWidth; + if (availableWidth >= placeholderWidth) { + placeholder = fullPlaceholder; + title = ""; + minWidth = placeholderWidth; + } + else { + placeholder = this.truncateToWidth(fullPlaceholder, availableWidth); + title = fullPlaceholder; + minWidth = availableWidth; + } + // min-width keeps the placeholder visible when the input is empty, but + // lets the input grow to fit content the user types beyond the placeholder. + input.style.minWidth = minWidth + 'px'; + document.l10n.setAttributes(input, "integration-citationDialog-just-added-input-citation", { placeholder, title }); + } + } + }, + // If a bubble is removed between two inputs we need to combine them combineNeighboringInputs(startingNode) { let node = startingNode; diff --git a/chrome/content/zotero/integration/citationDialog.js b/chrome/content/zotero/integration/citationDialog.js index c54d5d6b87..f5d54e90ce 100644 --- a/chrome/content/zotero/integration/citationDialog.js +++ b/chrome/content/zotero/integration/citationDialog.js @@ -1375,6 +1375,7 @@ const IOManager = { // If no locator is provided, record which bubbles were just added. // If a locator is typed next, these bubbles will receive it. this._justAddedBubbles = bubbleItems; + _id("bubble-input").showJustAddedPlaceholder = DIALOG_STATE.isCitingItems(); } await CitationDataManager.addItems({ bubbleItems, index }); // Refresh the itemTree if in library mode @@ -1693,6 +1694,7 @@ const IOManager = { if (currentLayout.type == "library") { libraryLayout.refreshItemsView(); } + this._clearJustAddedBubbles(); this.updateBubbleInput(); // Always refresh items list to make sure the opened and selected items are up to date currentLayout.refreshItemsList(); @@ -1811,6 +1813,7 @@ const IOManager = { if (event && event.type == "keydown" && !navigationKeys.includes(event.key)) return; // clear just added bubbles and update bubble input to reflect that this._justAddedBubbles = null; + _id("bubble-input").showJustAddedPlaceholder = false; this.updateBubbleInput(); }, diff --git a/chrome/content/zotero/integration/citationDialog.xhtml b/chrome/content/zotero/integration/citationDialog.xhtml index 38784ef7df..121dd06cc1 100644 --- a/chrome/content/zotero/integration/citationDialog.xhtml +++ b/chrome/content/zotero/integration/citationDialog.xhtml @@ -31,7 +31,9 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" drawintitlebar-platforms="mac,win" - resizable="false"> + resizable="false" + windowtype="zotero:citation-dialog" + > @@ -145,6 +147,9 @@ +
+ +
diff --git a/chrome/locale/en-US/zotero/integration.ftl b/chrome/locale/en-US/zotero/integration.ftl index 36b8d0b9a0..9d411cb028 100644 --- a/chrome/locale/en-US/zotero/integration.ftl +++ b/chrome/locale/en-US/zotero/integration.ftl @@ -30,6 +30,7 @@ integration-citationDialog-section-cited = { $count -> integration-citationDialog-details-suffix = Suffix integration-citationDialog-details-prefix = Prefix integration-citationDialog-details-suppressAuthor = Omit Author +integration-citationDialog-details-locator-info = After adding an item, you can type page numbers directly into the main input field. You can type other locators (e.g. chapter 5) and add them via Enter. Learn more. integration-citationDialog-details-includeComments = Include Comments integration-citationDialog-details-remove = { general-remove } integration-citationDialog-details-done = @@ -74,6 +75,11 @@ integration-citationDialog-aria-bubble = integration-citationDialog-single-input-citation = .placeholder = { integration-citationDialog-search-for-items } .aria-description = Press Tab to select items to add to this citation. Press Escape to discard the changes and close the dialog. +integration-citationDialog-just-added-input-placeholder = type "10-15" for page +integration-citationDialog-just-added-input-citation = + .placeholder = { $placeholder } + .title = { $title } + .aria-description = { integration-citationDialog-general-instructions } integration-citationDialog-input-citation = .placeholder = { integration-citationDialog-search-for-items } .aria-description = { integration-citationDialog-general-instructions } diff --git a/scss/components/_citationDialog.scss b/scss/components/_citationDialog.scss index df915ef9f3..fda0ee4296 100644 --- a/scss/components/_citationDialog.scss +++ b/scss/components/_citationDialog.scss @@ -741,6 +741,14 @@ height: 24px; // same height as input } } + .details-locator-info { + padding-inline-start: 5px; + grid-column: 2; + // pull closer to the locator input above + margin-top: -4px; + color: var(--fill-secondary); + font-size: .93rem; + } } .buttons { display: flex; diff --git a/scss/elements/_bubbleInput.scss b/scss/elements/_bubbleInput.scss index 2a72c9c675..905dc08706 100644 --- a/scss/elements/_bubbleInput.scss +++ b/scss/elements/_bubbleInput.scss @@ -52,7 +52,7 @@ bubble-input { // inputs initially occupy no width so they don't misalign bubbles at the // start of each line. On focus, set their width for the cursor to appear // and offset it by negative margin to avoid bubbles shifting - &.empty:not(.full-width):focus { + &.empty:not(.full-width):not(.just-added-placeholder):focus { min-width: 1px; margin-inline-start: -1px; }