Skip to content

Instantly share code, notes, and snippets.

@tiagorangel2011
Last active June 9, 2024 09:40
Show Gist options
  • Save tiagorangel2011/023337b26cf589c97ccd0b8fc15456f3 to your computer and use it in GitHub Desktop.
Save tiagorangel2011/023337b26cf589c97ccd0b8fc15456f3 to your computer and use it in GitHub Desktop.
Simple JS+CSS omnibar.
/* tiagorangel.com * If you use this, please credit me somewhere
* Make sure to set the following variables:
--foreground: r, g, b
--background: r, g, b
Example:
--foreground: 13, 13, 14;
--background: 255, 255, 255;
*/
.omnibar__wrapper {
background-color: rgba(0, 0, 0, 0.5);
display: flex;
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 1000;
justify-content: center;
padding-top: 20% !important;
padding: 10px;
overscroll-behavior: contain;
transition: opacity .25s;
}
.omnibar {
padding: 0;
background: rgb(var(--background));
max-width: 600px;
max-height: min(400px, calc(100vh - 20% - 2rem));
width: 95%;
top: 20%;
position: absolute;
z-index: 2;
display: flex;
flex-direction: column;
border-radius: 8px;
transition: transform .25s, filter .25s;
overflow: hidden;
animation: omnibar-in .25s;
}
.omnibar__search {
appearance: none;
background: transparent;
font-family: var(--font-sans);
border: none;
outline: none;
padding: 1rem;
color: rgba(var(--foreground), 0.9);
font-size: 1rem;
border-bottom: 1px solid rgba(var(--foreground), 0.1);
}
.omnibar__search[readonly] {
cursor: default;
}
.omnibar__search[readonly]::placeholder {
opacity: 1;
color: rgb(var(--foreground));
}
.omnibar__results {
overflow: auto;
display: flex;
flex-direction: column;
gap: 2px;
user-select: none;
}
.omnibar__section {
display: flex;
flex-direction: column;
}
.omnibar__section h4 {
font-size: 0.8rem;
padding: 0.2rem 1rem;
color: rgba(var(--foreground), .85);
margin-top: 0.5rem;
font-family: var(--font-sans);
margin: 5px 0px;
}
.omnibar__result {
display: flex;
align-items: center;
width: 100%;
padding: 0.6rem 1rem;
color: rgba(var(--foreground), 0.95);
text-decoration: none;
border-left: 2px solid transparent;
gap: 7px;
cursor: pointer;
transition: border-left-color .15s, background-color .15s
}
.omnibar__result svg {
width: 20px;
height: 20px;
color: rgb(var(--foreground));
}
.omnibar__result p {
margin: 0px;
}
.omnibar__result .omnibar__shortcut {
margin-left: auto;
}
.omnibar__result.active {
background-color: rgba(var(--foreground), 0.07);
border-left-color: rgba(var(--foreground), 0.8);
}
@keyframes omnibar-in {
from {
opacity: 0;
transform: scale(0.95);
filter: blur(4px)
}
to {
opacity: 1;
transform: none;
filter: none
}
}
// tiagorangel.com * If you use this, please credit me somewhere
// Usage: call omnibar.open with the options below.
const omnibar = {
// [{name (html allowed), shortcut(ex. ["⌘","⇧","H"]), icon_svg, action, id}]
// |
// [{name (optional), children}]
// | { disableTyping, hideInputBorder, placeholder, noFilter }
// | |
open: function (sections, options) {
let isOpen = true;
let onClose, onSelection;
let dataToFilter = [];
const create = function (tag, classes) {
const el = document.createElement(tag);
if (classes) {
classes.split(" ").forEach(function (name) {
el.classList.add(name);
});
}
return el;
};
const closeModal = function () {
(onClose || function () {})();
isOpen = false;
wrapper.style.opacity = "0";
modal.style.transform = "scale(0.95)";
modal.style.filter = "blur(4px)";
setTimeout(function () {
wrapper.remove();
}, 250);
};
const wrapper = create("div", "omnibar__wrapper");
wrapper.style.opacity = "0";
const modal = create("div", "omnibar");
modal.style.transform = "scale(0.95)";
modal.style.filter = "blur(4px)";
wrapper.appendChild(modal);
document.body.appendChild(wrapper);
setTimeout(function () {
wrapper.style.opacity = "1";
modal.style.transform = "none";
modal.style.filter = "none";
}, 1);
setTimeout(function () {
input.focus();
}, 100);
const input = create("input", "omnibar__search");
input.setAttribute("type", "search");
input.setAttribute("name", "search");
input.setAttribute("autocomplete", "off");
input.setAttribute("spellcheck", "off");
input.setAttribute(
"placeholder",
options?.placeholder || "Type a command or search"
);
if (options?.disableTyping) {
input.setAttribute("readonly", "true");
}
if (options?.hideInputBorder) {
input.style.borderBottom = "0px solid transparent";
}
modal.appendChild(input);
const resultsWrap = create("div", "omnibar__results");
modal.appendChild(resultsWrap);
const focusIntoEl = function (childEl) {
resultsWrap.querySelectorAll(".omnibar__result").forEach((e) => {
e.classList.remove("active");
});
childEl.style.height = childEl.offsetHeight + "px";
const childRect = childEl.getBoundingClientRect();
const parentRect = resultsWrap.getBoundingClientRect();
const isFullyVisible =
childRect.top >= parentRect.top &&
childRect.bottom <= parentRect.bottom;
if (!isFullyVisible) {
if (childRect.top < parentRect.top) {
resultsWrap.scrollTo({
top: resultsWrap.scrollTop - (parentRect.top - childRect.top),
behavior: "smooth",
});
} else if (childRect.bottom > parentRect.bottom) {
resultsWrap.scrollTo({
top: resultsWrap.scrollTop + (childRect.bottom - parentRect.bottom),
behavior: "smooth",
});
}
}
childEl.style.height = "unset";
childEl.classList.add("active");
};
sections.forEach((section) => {
const sectionEl = create("div", "omnibar__section");
if (section.name) {
const name = create("h4");
name.innerText = section.name;
sectionEl.appendChild(name);
}
section.children.forEach((child) => {
const childEl = create("div", "omnibar__result");
childEl.setAttribute("title", child.name);
childEl.addEventListener("mouseover", function () {
focusIntoEl(childEl);
});
childEl.addEventListener("mouseout", function () {
resultsWrap.querySelectorAll(".omnibar__result").forEach((e) => {
e.classList.remove("active");
});
});
const childIcon = create("div");
childIcon.outerHTML = child.icon_svg;
childEl.appendChild(childIcon);
const childName = create("p");
childName.innerHTML = child.name;
childEl.appendChild(childName);
if (child.shortcut && child.shortcut.length > 0) {
const childShortcut = create("div", "omnibar__shortcut");
childShortcut.innerHTML = `<code>${child.shortcut.join(
"</code><code>"
)}</code>`;
childEl.appendChild(childShortcut);
}
childEl.addEventListener("click", function () {
closeModal();
(onSelection || function () {})(child.id);
(child.action || function () {})(child.id);
});
sectionEl.appendChild(childEl);
dataToFilter.push({
id: child.id,
name: child.name,
});
});
resultsWrap.appendChild(sectionEl);
});
document.addEventListener("keyup", function (e) {
if (!isOpen) {
return;
}
if (e.key === "Escape") {
closeModal();
}
});
document.addEventListener("click", function (e) {
if (!isOpen) {
return;
}
if (e.target === wrapper) {
closeModal();
}
});
if (!options?.noFilter) {
input.addEventListener("input", function () {
resultsWrap
.querySelectorAll(".omnibar__section, .omnibar__result")
.forEach((e) => {
e.style.display = "flex";
});
resultsWrap.querySelectorAll(".omnibar__result").forEach((e) => {
if (!e.querySelector("p").innerText.includes(input.value.trim())) {
e.style.display = "none";
}
});
document.querySelectorAll(".omnibar__section").forEach((section) => {
let allHidden = Array.from(
section.querySelectorAll(".omnibar__result")
).every(function (result) {
return result.offsetParent === null;
});
if (allHidden) {
section.style.display = "none";
}
});
});
}
input.addEventListener("keydown", function (e) {
if (e.key == "ArrowDown" || e.key == "ArrowLeft" || e.key == "Tab") {
e.preventDefault();
let lastIndex = -1;
document.querySelectorAll(".omnibar__result").forEach((e, i) => {
if (e.classList.contains("active")) {
lastIndex = i;
e.classList.remove("active");
}
});
focusIntoEl(
document.querySelectorAll(".omnibar__result")[lastIndex + 1] ||
document.querySelectorAll(".omnibar__result")[0]
);
}
if (
e.key == "ArrowUp" ||
e.key == "ArrowRight" ||
(e.shiftKey && e.key == "Tab")
) {
e.preventDefault();
let lastIndex = -1;
document.querySelectorAll(".omnibar__result").forEach((e, i) => {
if (e.classList.contains("active")) {
lastIndex = i;
e.classList.remove("active");
}
});
focusIntoEl(
document.querySelectorAll(".omnibar__result")[lastIndex - 1] ||
document.querySelectorAll(".omnibar__result")[
document.querySelectorAll(".omnibar__result").length - 1
]
);
}
if (e.key == "Enter") {
e.preventDefault();
if (resultsWrap.querySelector(".omnibar__results")) {
(
resultsWrap.querySelectorAll(".omnibar__result")[0] ||
resultsWrap.querySelectorAll(".omnibar__result.active")[0]
).click();
} else {
closeModal();
(onSelection || function () {})(input.value);
}
}
});
return {
close: closeModal,
onClose: function (listener) {
onClose = listener;
},
onSelection: function (listener) {
onSelection = listener;
},
onQuery: function (listener) {
input.addEventListener("keypress", function () {
listener(input.value);
});
},
onSubmitText: function (listener) {
input.addEventListener("keypress", function (e) {
if (e.key == "Enter") {
listener(input.value);
}
});
},
asyncEvents: {
awaitClose: function () {
return new Promise((resolve) => {
onClose = resolve;
});
},
awaitSelection: function () {
return new Promise((resolve) => {
onSelection = resolve;
});
},
awaitSubmitText: function () {
return new Promise((resolve) => {
input.addEventListener("keypress", function (e) {
if (e.key == "Enter") {
resolve(input.value);
}
});
});
},
},
};
},
prompt: function (prompt, options) {
return new Promise((resolve) => {
let query = "";
const bar = omnibar.open([], {
placeholder: prompt,
noFilter: true,
hideInputBorder: true,
});
bar.onQuery(function (q) {
query = q;
});
bar.onSubmitText(function (q) {
query = q;
resolve(query.trim());
})
bar.onClose(function () {
resolve(false);
});
});
},
};
@tiagorangel2011
Copy link
Author

⬇️ post issues here ⬇️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment