mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Citation dialog: show placeholder after a new bubble is added (#5932)
After the first bubble is added, the focused input gets a placeholder indicating that typing a number will add it as a page to the just-added bubble. The placeholder is truncated if it's too close to the edge in multi-item citations. Also add a tip to the item details popup explaining that locators can be typed into the main input field, with a link to the documentation. The tip stops appearing once a typed locator has been used. --------- Co-authored-by: Dan Stillman <dstillman@zotero.org>
This commit is contained in:
parent
40f949ba94
commit
0f2d3e103d
8 changed files with 143 additions and 10 deletions
|
|
@ -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
|
||||
|
|
@ -343,6 +343,13 @@
|
|||
if (!Utils.isInputEmpty(input) || !this.contains(event.relatedTarget)) {
|
||||
this._lastFocusedInput = input;
|
||||
}
|
||||
// Collapse a placeholder input back to regular size once focus leaves.
|
||||
// The placeholder attributes are reset on the next refresh()
|
||||
if (input.classList.contains("just-added-placeholder")) {
|
||||
input.classList.remove("just-added-placeholder");
|
||||
input.removeAttribute("title");
|
||||
input.style.minWidth = "";
|
||||
}
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
|
@ -604,6 +611,87 @@
|
|||
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 <input> 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) {
|
||||
// If the just-added placeholder was dismissed (e.g. by right-arrow at the
|
||||
// end of the input) while the input is still focused and empty, keep the
|
||||
// visible placeholder but fall back to the default search prompt, since
|
||||
// searching still works. It collapses when the input loses focus (see the
|
||||
// blur handler in _createInputElem())
|
||||
if (input.classList.contains("just-added-placeholder")
|
||||
&& document.activeElement == input && !input.value) {
|
||||
let placeholder = this.truncateToWidth(
|
||||
" " + Zotero.getString("integration-citationDialog-search-for-items"),
|
||||
parseFloat(input.style.minWidth) || Infinity);
|
||||
document.l10n.setAttributes(input, "integration-citationDialog-just-added-input-citation", { placeholder, title: "" });
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1206,6 +1206,7 @@ class ListLayout extends Layout {
|
|||
const IOManager = {
|
||||
sectionExpandedStatus: {},
|
||||
_skipInputAcceptOnEnterUntil: 0,
|
||||
_timesItemsAdded: 0,
|
||||
|
||||
// most essential IO functionality that is added immediately on load
|
||||
preInit() {
|
||||
|
|
@ -1406,7 +1407,12 @@ 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;
|
||||
// Only show the placeholder guidance on the first add -- after
|
||||
// that, the user presumably knows about the shortcut
|
||||
_id("bubble-input").showJustAddedPlaceholder = DIALOG_STATE.isCitingItems()
|
||||
&& this._timesItemsAdded < 1;
|
||||
}
|
||||
this._timesItemsAdded++;
|
||||
await CitationDataManager.addItems({ bubbleItems, index });
|
||||
// Refresh the itemTree if in library mode
|
||||
if (currentLayout.type == "library") {
|
||||
|
|
@ -1695,6 +1701,9 @@ const IOManager = {
|
|||
input.value = "";
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
this.updateBubbleInput();
|
||||
// The typed-locator shortcut has been used, so stop showing the tip
|
||||
// about it in the item details popup
|
||||
Zotero.Prefs.set("integration.citationDialogShowLocatorTip", false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1735,9 +1744,14 @@ const IOManager = {
|
|||
|
||||
_deleteItem(dialogReferenceID) {
|
||||
CitationDataManager.deleteItem({ dialogReferenceID });
|
||||
// If the citation is emptied, show the placeholder guidance again on the next add
|
||||
if (!CitationDataManager.items.length) {
|
||||
this._timesItemsAdded = 0;
|
||||
}
|
||||
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();
|
||||
|
|
@ -1835,9 +1849,14 @@ const IOManager = {
|
|||
bubbleItem.label = "page";
|
||||
}
|
||||
IOManager._hideLoadingSpinner();
|
||||
// Clear the input and update bubbles
|
||||
// Clear the input and update bubbles. The placeholder stays, since both of its
|
||||
// suggestions still apply -- typed digits keep appending to the locator, and
|
||||
// any other input starts a search
|
||||
input.value = "";
|
||||
IOManager.updateBubbleInput();
|
||||
// The typed-locator shortcut has been used, so stop showing the tip
|
||||
// about it in the item details popup
|
||||
Zotero.Prefs.set("integration.citationDialogShowLocatorTip", false);
|
||||
// Disable Enter on input from accepting the dialog for the next 500ms;
|
||||
// If one intends to confirmed the numeric locator by pressing Enter (via _handleInputEnter),
|
||||
// we ensure that the Enter keypress won't happen right after when the locator is added to
|
||||
|
|
@ -1851,11 +1870,14 @@ const IOManager = {
|
|||
// and Enter is presses, just-added bubbles get that locator.
|
||||
_clearJustAddedBubbles(event) {
|
||||
if (!this._justAddedBubbles) return;
|
||||
// on keydown, only proceed if it's an arrow key
|
||||
let navigationKeys = ["ArrowUp", "ArrowDown", "ArrowRight", "ArrowLeft"];
|
||||
if (event && event.type == "keydown" && !navigationKeys.includes(event.key)) return;
|
||||
// On keydown, only proceed for left/right arrows, which move to another
|
||||
// reference (e.g. to explicitly search for a year). Up/down arrows just move
|
||||
// the list selection while focus remains in the input, so locator entry
|
||||
// stays active.
|
||||
if (event && event.type == "keydown" && !["ArrowLeft", "ArrowRight"].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();
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<head>
|
||||
<title data-l10n-id="integration-citationDialog"></title>
|
||||
<link rel="localization" href="zotero.ftl"/>
|
||||
|
|
@ -154,6 +156,9 @@
|
|||
<!--fx128: size="0" forces select have default native style -->
|
||||
<select name="locator" id="label" class="details-label" size="0"></select>
|
||||
<input id="locator" class="details-data" aria-labelledby="label" aria-describedby="itemDetails-combinedInfo"/>
|
||||
<div class="details-locator-info" data-l10n-id="integration-citationDialog-details-locator-info">
|
||||
<a data-l10n-name="docs-link" class="link" tabindex="0" href="https://www.zotero.org/support/word_processor_plugin_usage#page_and_other_locators" onclick="Zotero.launchURL(this.href)"/>
|
||||
</div>
|
||||
|
||||
<label class="details-label" for="prefix" data-l10n-id="integration-citationDialog-details-prefix"></label>
|
||||
<input id="prefix" class="details-data"/>
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ export class CitationDialogPopupsHandler {
|
|||
popup.style.top = `${bubbleRect.bottom + 10}px`;
|
||||
|
||||
this._getNode("#itemDetails .show").hidden = !this.bubbleItem.item.id;
|
||||
// Hide the typed-locator tip once the shortcut has been used
|
||||
this._getNode("#itemDetails .details-locator-info").hidden
|
||||
= !Zotero.Prefs.get("integration.citationDialogShowLocatorTip");
|
||||
let topLevelItem = this.bubbleItem.item.topLevelItem;
|
||||
|
||||
// Add header and fill inputs with their values
|
||||
|
|
|
|||
|
|
@ -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 = Tip: You can also type page numbers and other locators directly into the main field. <a data-l10n-name="docs-link">Learn more</a>
|
||||
integration-citationDialog-details-includeComments = Include Comments
|
||||
integration-citationDialog-details-remove = { general-remove }
|
||||
integration-citationDialog-details-done =
|
||||
|
|
@ -77,6 +78,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” to cite pages, or search for items
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ pref("extensions.zotero.integration.keepAddCitationDialogRaised", false);
|
|||
pref("extensions.zotero.integration.upgradeTemplateDelayedOn", 0);
|
||||
pref("extensions.zotero.integration.dontPromptMendeleyImport", false);
|
||||
pref("extensions.zotero.integration.citationDialogMode", "last-used");
|
||||
pref("extensions.zotero.integration.citationDialogShowLocatorTip", true);
|
||||
pref("extensions.zotero.integration.annotationDialogIncludeComments", true);
|
||||
pref("extensions.zotero.integration.citationPreviewShown", true);
|
||||
|
||||
|
|
|
|||
|
|
@ -786,6 +786,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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue