Skip to content

Commit aa1db7d

Browse files
committed
fix: group snippets under 'more' button when screensize is too small (fixes 62)
1 parent 884ad3b commit aa1db7d

File tree

2 files changed

+268
-83
lines changed

2 files changed

+268
-83
lines changed

packages/yasqe/src/index.ts

Lines changed: 225 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class Yasqe extends CodeMirror {
5858
private resizeWrapper?: HTMLDivElement;
5959
private snippetsBar?: HTMLDivElement;
6060
private snippetsClickHandler?: (e: MouseEvent) => void;
61+
private snippetsResizeHandler?: () => void;
6162
public rootEl: HTMLDivElement;
6263
public storage: YStorage;
6364
public config: Config;
@@ -468,98 +469,241 @@ export class Yasqe extends CodeMirror {
468469

469470
const snippets = this.config.snippets;
470471

471-
// If 10 or fewer snippets, show all as buttons
472-
if (snippets.length <= 10) {
473-
snippets.forEach((snippet) => {
474-
const btn = document.createElement("button");
475-
addClass(btn, "yasqe_snippetButton");
476-
btn.textContent = snippet.label;
477-
btn.title = snippet.code;
478-
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
479-
btn.onclick = () => this.insertSnippet(snippet.code);
480-
this.snippetsBar!.appendChild(btn);
481-
});
482-
} else {
483-
// Group snippets by group property
484-
const grouped: { [group: string]: Snippet[] } = {};
485-
const ungrouped: Snippet[] = [];
486-
487-
snippets.forEach((snippet) => {
488-
if (snippet.group) {
489-
if (!grouped[snippet.group]) grouped[snippet.group] = [];
490-
grouped[snippet.group].push(snippet);
491-
} else {
492-
ungrouped.push(snippet);
493-
}
472+
// Create a container for visible items
473+
const visibleContainer = document.createElement("div");
474+
addClass(visibleContainer, "yasqe_snippetsVisible");
475+
476+
// Create all buttons/dropdowns
477+
const allItems: HTMLElement[] = [];
478+
479+
// Group snippets by group property for all snippets (not just >10)
480+
const grouped: { [group: string]: Snippet[] } = {};
481+
const ungrouped: Snippet[] = [];
482+
483+
snippets.forEach((snippet) => {
484+
if (snippet.group) {
485+
if (!grouped[snippet.group]) grouped[snippet.group] = [];
486+
grouped[snippet.group].push(snippet);
487+
} else {
488+
ungrouped.push(snippet);
489+
}
490+
});
491+
492+
// Create dropdown for each group
493+
Object.keys(grouped).forEach((groupName) => {
494+
const dropdown = document.createElement("div");
495+
addClass(dropdown, "yasqe_snippetDropdown");
496+
497+
const dropdownBtn = document.createElement("button");
498+
addClass(dropdownBtn, "yasqe_snippetDropdownButton");
499+
dropdownBtn.textContent = groupName + " ";
500+
const chevron = drawSvgStringAsElement(imgs.chevronDown);
501+
addClass(chevron, "chevronIcon");
502+
dropdownBtn.appendChild(chevron);
503+
dropdownBtn.setAttribute("aria-label", `${groupName} snippets`);
504+
dropdownBtn.setAttribute("aria-expanded", "false");
505+
506+
const dropdownContent = document.createElement("div");
507+
addClass(dropdownContent, "yasqe_snippetDropdownContent");
508+
dropdownContent.setAttribute("role", "menu");
509+
510+
grouped[groupName].forEach((snippet) => {
511+
const item = document.createElement("button");
512+
addClass(item, "yasqe_snippetDropdownItem");
513+
item.textContent = snippet.label;
514+
item.title = snippet.code;
515+
item.setAttribute("role", "menuitem");
516+
item.onclick = () => {
517+
this.insertSnippet(snippet.code);
518+
dropdownContent.style.display = "none";
519+
dropdownBtn.setAttribute("aria-expanded", "false");
520+
};
521+
dropdownContent.appendChild(item);
494522
});
495523

496-
// Create dropdown for each group
497-
Object.keys(grouped).forEach((groupName) => {
498-
const dropdown = document.createElement("div");
499-
addClass(dropdown, "yasqe_snippetDropdown");
500-
501-
const dropdownBtn = document.createElement("button");
502-
addClass(dropdownBtn, "yasqe_snippetDropdownButton");
503-
dropdownBtn.textContent = groupName + " ";
504-
const chevron = drawSvgStringAsElement(imgs.chevronDown);
505-
addClass(chevron, "chevronIcon");
506-
dropdownBtn.appendChild(chevron);
507-
dropdownBtn.setAttribute("aria-label", `${groupName} snippets`);
508-
dropdownBtn.setAttribute("aria-expanded", "false");
509-
510-
const dropdownContent = document.createElement("div");
511-
addClass(dropdownContent, "yasqe_snippetDropdownContent");
512-
dropdownContent.setAttribute("role", "menu");
513-
514-
grouped[groupName].forEach((snippet) => {
515-
const item = document.createElement("button");
516-
addClass(item, "yasqe_snippetDropdownItem");
517-
item.textContent = snippet.label;
518-
item.title = snippet.code;
519-
item.setAttribute("role", "menuitem");
520-
item.onclick = () => {
521-
this.insertSnippet(snippet.code);
522-
dropdownContent.style.display = "none";
523-
dropdownBtn.setAttribute("aria-expanded", "false");
524-
};
525-
dropdownContent.appendChild(item);
524+
dropdownBtn.onclick = (e) => {
525+
e.stopPropagation();
526+
const isOpen = dropdownContent.style.display === "block";
527+
// Close all other dropdowns
528+
const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
529+
allDropdowns.forEach((dd) => {
530+
(dd as HTMLElement).style.display = "none";
526531
});
532+
const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
533+
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
527534

528-
dropdownBtn.onclick = (e) => {
529-
e.stopPropagation();
530-
const isOpen = dropdownContent.style.display === "block";
531-
// Close all other dropdowns
532-
const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
533-
allDropdowns.forEach((dd) => {
534-
(dd as HTMLElement).style.display = "none";
535-
});
536-
const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
537-
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
535+
// Toggle this dropdown
536+
if (!isOpen) {
537+
dropdownContent.style.display = "block";
538+
dropdownBtn.setAttribute("aria-expanded", "true");
539+
}
540+
};
538541

539-
// Toggle this dropdown
540-
if (!isOpen) {
541-
dropdownContent.style.display = "block";
542-
dropdownBtn.setAttribute("aria-expanded", "true");
543-
}
544-
};
542+
dropdown.appendChild(dropdownBtn);
543+
dropdown.appendChild(dropdownContent);
544+
allItems.push(dropdown);
545+
});
546+
547+
// Add ungrouped snippets as individual buttons
548+
ungrouped.forEach((snippet) => {
549+
const btn = document.createElement("button");
550+
addClass(btn, "yasqe_snippetButton");
551+
btn.textContent = snippet.label;
552+
btn.title = snippet.code;
553+
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
554+
btn.onclick = () => this.insertSnippet(snippet.code);
555+
allItems.push(btn);
556+
});
545557

546-
dropdown.appendChild(dropdownBtn);
547-
dropdown.appendChild(dropdownContent);
548-
this.snippetsBar!.appendChild(dropdown);
558+
// Add all items to visible container initially
559+
allItems.forEach((item) => visibleContainer.appendChild(item));
560+
this.snippetsBar.appendChild(visibleContainer);
561+
562+
// Create "Show More" dropdown (initially hidden)
563+
const showMoreDropdown = document.createElement("div");
564+
addClass(showMoreDropdown, "yasqe_snippetDropdown", "yasqe_showMore");
565+
showMoreDropdown.style.display = "none";
566+
567+
const showMoreBtn = document.createElement("button");
568+
addClass(showMoreBtn, "yasqe_snippetDropdownButton", "yasqe_showMoreButton");
569+
showMoreBtn.textContent = "More ";
570+
const chevron = drawSvgStringAsElement(imgs.chevronDown);
571+
addClass(chevron, "chevronIcon");
572+
showMoreBtn.appendChild(chevron);
573+
showMoreBtn.setAttribute("aria-label", "Show more snippets");
574+
showMoreBtn.setAttribute("aria-expanded", "false");
575+
576+
const showMoreContent = document.createElement("div");
577+
addClass(showMoreContent, "yasqe_snippetDropdownContent", "yasqe_showMoreContent");
578+
showMoreContent.setAttribute("role", "menu");
579+
580+
showMoreBtn.onclick = (e) => {
581+
e.stopPropagation();
582+
const isOpen = showMoreContent.style.display === "block";
583+
// Close all other dropdowns
584+
const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
585+
allDropdowns.forEach((dd) => {
586+
if (dd !== showMoreContent) {
587+
(dd as HTMLElement).style.display = "none";
588+
}
589+
});
590+
const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
591+
allButtons.forEach((btn) => {
592+
if (btn !== showMoreBtn) {
593+
btn.setAttribute("aria-expanded", "false");
594+
}
549595
});
550596

551-
// Add ungrouped snippets as individual buttons
552-
ungrouped.forEach((snippet) => {
553-
const btn = document.createElement("button");
554-
addClass(btn, "yasqe_snippetButton");
555-
btn.textContent = snippet.label;
556-
btn.title = snippet.code;
557-
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
558-
btn.onclick = () => this.insertSnippet(snippet.code);
559-
this.snippetsBar!.appendChild(btn);
597+
// Toggle this dropdown
598+
if (!isOpen) {
599+
showMoreContent.style.display = "block";
600+
showMoreBtn.setAttribute("aria-expanded", "true");
601+
} else {
602+
showMoreContent.style.display = "none";
603+
showMoreBtn.setAttribute("aria-expanded", "false");
604+
}
605+
};
606+
607+
showMoreDropdown.appendChild(showMoreBtn);
608+
showMoreDropdown.appendChild(showMoreContent);
609+
this.snippetsBar.appendChild(showMoreDropdown);
610+
611+
// Create function to handle overflow detection
612+
const handleOverflow = () => {
613+
if (!this.snippetsBar) return;
614+
615+
const containerWidth = this.snippetsBar.offsetWidth;
616+
const showMoreWidth = 80; // Approximate width for "Show More" button
617+
let availableWidth = containerWidth - showMoreWidth - 20; // 20px for padding/margin
618+
let currentWidth = 0;
619+
const overflowItems: { element: HTMLElement; snippet?: Snippet; groupName?: string }[] = [];
620+
621+
// Check each item to see if it fits
622+
allItems.forEach((item, index) => {
623+
const itemWidth = item.offsetWidth + 5; // 5px gap
624+
625+
if (currentWidth + itemWidth > availableWidth && index > 0) {
626+
// This item overflows, hide it and add to show more
627+
item.style.display = "none";
628+
629+
// Find the corresponding snippet or group
630+
if (item.classList.contains("yasqe_snippetDropdown")) {
631+
// It's a group dropdown
632+
const groupBtn = item.querySelector(".yasqe_snippetDropdownButton");
633+
const groupName = groupBtn?.textContent?.trim().replace(" ", "") || "Group";
634+
overflowItems.push({ element: item, groupName });
635+
} else {
636+
// It's an individual button
637+
const snippet = ungrouped.find((s) => s.label === item.textContent);
638+
overflowItems.push({ element: item, snippet });
639+
}
640+
} else {
641+
currentWidth += itemWidth;
642+
}
560643
});
644+
645+
// If there are overflow items, show the "Show More" button
646+
if (overflowItems.length > 0) {
647+
showMoreDropdown.style.display = "inline-block";
648+
649+
// Populate show more content with proper grouping
650+
overflowItems.forEach(({ element, snippet, groupName }) => {
651+
if (groupName) {
652+
// It's a group - add a group header and its items
653+
const groupHeader = document.createElement("div");
654+
addClass(groupHeader, "yasqe_snippetGroupHeader");
655+
groupHeader.textContent = groupName;
656+
showMoreContent.appendChild(groupHeader);
657+
658+
const groupItems = element.querySelectorAll(".yasqe_snippetDropdownItem");
659+
groupItems.forEach((item) => {
660+
const clonedItem = item.cloneNode(true) as HTMLElement;
661+
clonedItem.onclick = (item as HTMLElement).onclick;
662+
showMoreContent.appendChild(clonedItem);
663+
});
664+
} else if (snippet) {
665+
// It's an individual snippet
666+
const item = document.createElement("button");
667+
addClass(item, "yasqe_snippetDropdownItem");
668+
item.textContent = snippet.label;
669+
item.title = snippet.code;
670+
item.setAttribute("role", "menuitem");
671+
item.onclick = () => {
672+
this.insertSnippet(snippet.code);
673+
showMoreContent.style.display = "none";
674+
showMoreBtn.setAttribute("aria-expanded", "false");
675+
};
676+
showMoreContent.appendChild(item);
677+
}
678+
});
679+
}
680+
};
681+
682+
// Run overflow detection on next frame
683+
requestAnimationFrame(handleOverflow);
684+
685+
// Set up resize handler for overflow detection
686+
// Remove any existing handler first
687+
if (this.snippetsResizeHandler) {
688+
window.removeEventListener("resize", this.snippetsResizeHandler);
561689
}
562690

691+
// Create and store the resize handler
692+
this.snippetsResizeHandler = () => {
693+
if (!this.snippetsBar) return;
694+
695+
// Reset all items to visible first
696+
allItems.forEach((item) => (item.style.display = ""));
697+
showMoreDropdown.style.display = "none";
698+
showMoreContent.innerHTML = "";
699+
700+
// Re-run overflow detection
701+
requestAnimationFrame(handleOverflow);
702+
};
703+
704+
// Add the resize handler
705+
window.addEventListener("resize", this.snippetsResizeHandler);
706+
563707
// Set up click handler for closing dropdowns when clicking outside
564708
// Remove any existing handler first
565709
if (this.snippetsClickHandler) {

0 commit comments

Comments
 (0)