feat: cool awesomebar

This commit is contained in:
Ejaaz Khan 2025-11-26 12:22:28 +05:30
parent 111427abe0
commit ac448227be
14 changed files with 286 additions and 103 deletions

View file

@ -29,7 +29,7 @@
position: relative;
}
#navbar-search{
#navbar-modal-search{
padding-left: 32px;
}
.desktop-search-icon{

View file

@ -9,13 +9,9 @@
>
</div>
<div class="desktop-search-wrapper input-group search-bar text-muted ">
<input
id="navbar-search"
type="text"
class="form-control"
aria-haspopup="true"
placeholder="Search for type a command"
>
<div id="navbar-modal-search">
Search for type a command
</div>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>

View file

@ -199,12 +199,12 @@ class DesktopPage {
setup_awesomebar() {
if (frappe.boot.desk_settings.search_bar) {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup(".desktop-search-wrapper #navbar-search");
awesome_bar.setup(".desktop-search-wrapper #navbar-modal-search");
}
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+g",
action: function (e) {
$(".desktop-search-wrapper #navbar-search").focus();
$(".desktop-search-wrapper #navbar-modal-search").click();
e.preventDefault();
return false;
},
@ -213,7 +213,7 @@ class DesktopPage {
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$(".desktop-search-wrapper #navbar-search").focus();
$(".desktop-search-wrapper #navbar-modal-search").click();
e.preventDefault();
return false;
},

View file

@ -200,7 +200,7 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
e.preventDefault();
return false;
},
@ -210,7 +210,7 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+k",
action: function (e) {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
e.preventDefault();
return false;
},
@ -220,7 +220,7 @@ frappe.ui.keys.add_shortcut({
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+g",
action: function (e) {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
e.preventDefault();
return false;
},

View file

@ -5,15 +5,81 @@ frappe.provide("frappe.tags");
frappe.search.AwesomeBar = class AwesomeBar {
setup(element) {
var me = this;
$(".search-bar").removeClass("hidden");
var $input = $(element);
var input = $input.get(0);
this.options = [];
this.global_results = [];
this.setup_search_modal(element);
frappe.search.utils.setup_recent();
}
setup_search_modal(element) {
let is_event_listeners_added = false;
let $search_element = $(element);
let search_modal = new frappe.get_modal("Search", "");
let search_modal_body = `<div class="align-baseline flex py-2 px-1 relative navbar-modal-wrapper">
<div class="modal-search-icon absolute pr-2 pl-3">${frappe.utils.icon("search")}</div>
<input
id="navbar-search"
type="text"
class="form-control bg-transparent shadow-none" aria-haspopup="true"
placeholder="${__("Search for type a command")}" autocomplete="off"
/>
<div class="modal-divider"></div>
</div>`;
let search_modal_footer = `<div class="awesomebar-modal-footer flex justify-between w-100">
<div class="help-navigation">
<span class="help-item-navigate">
<span class="help-item">${frappe.utils.icon("arrow-up")}</span>
<span class="help-item">${frappe.utils.icon("arrow-down")}</span>
<span>${__("to navigate")}</span>
</span>
<span class="help-item-navigate">
<span class="help-item">${frappe.utils.icon("corner-down-left")}</span>
<span>${__("to select")}</span>
</span>
<span class="help-item">${__("esc")}</span>
<span>${__("to close")}</span>
</div>
<div class="pointer">${frappe.utils.icon("circle-question-mark")}</div>
</div>`;
search_modal.find(".modal-body").css("padding", "0").html(search_modal_body);
search_modal.find(".modal-header").css("display", "none");
search_modal
.find(".modal-footer")
.removeClass("hide")
.addClass("cool-awesomebar-modal")
.html(search_modal_footer);
search_modal.find(".pointer").on("click", () => {
console.log("click called");
this.show_help();
});
$search_element.click(() => {
search_modal.modal("show");
setTimeout(() => {
search_modal.find("#navbar-search").get(0).focus();
}, 400);
if (is_event_listeners_added) return;
is_event_listeners_added = true;
this.setup_event_listeners(search_modal);
});
}
setup_event_listeners(search_modal) {
var me = this;
let $input = search_modal.find("#navbar-search");
let input = $input.get(0);
var awesomplete = new Awesomplete(input, {
minChars: 0,
maxItems: 99,
@ -85,10 +151,9 @@ frappe.search.AwesomeBar = class AwesomeBar {
);
me.options = me.options.concat(frappe.search.utils.get_frequent_links());
}
me.add_help();
awesomplete.list = me.deduplicate(me.options);
}, 100)
}, 50)
);
var open_recent = function () {
@ -96,6 +161,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
$(this).trigger("input");
}
};
$input.on("focus", open_recent);
$input.on("awesomplete-open", function (e) {
@ -130,6 +196,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
}
$input.val("");
$input.trigger("blur");
search_modal.modal("hide");
});
$input.on("awesomplete-selectcomplete", function (e) {
@ -141,51 +208,43 @@ frappe.search.AwesomeBar = class AwesomeBar {
$input.trigger("blur");
}
});
frappe.search.utils.setup_recent();
}
add_help() {
this.options.push({
value: __("Help on Search"),
index: -10,
default: "Help",
onclick: function () {
var txt =
'<table class="table table-bordered">\
<tr><td style="width: 50%">' +
__("Create a new record") +
"</td><td>" +
__("new type of document") +
"</td></tr>\
<tr><td>" +
__("List a document type") +
"</td><td>" +
__("document type..., e.g. customer") +
"</td></tr>\
<tr><td>" +
__("Search in a document type") +
"</td><td>" +
__("text in document type") +
"</td></tr>\
<tr><td>" +
__("Tags") +
"</td><td>" +
__("tag name..., e.g. #tag") +
"</td></tr>\
<tr><td>" +
__("Open a module or tool") +
"</td><td>" +
__("module name...") +
"</td></tr>\
<tr><td>" +
__("Calculate") +
"</td><td>" +
__("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") +
"</td></tr>\
</table>";
frappe.msgprint(txt, __("Search Help"));
},
});
show_help() {
const txt =
'<table class="table table-bordered">\
<tr><td style="width: 50%">' +
__("Create a new record") +
"</td><td>" +
__("new type of document") +
"</td></tr>\
<tr><td>" +
__("List a document type") +
"</td><td>" +
__("document type..., e.g. customer") +
"</td></tr>\
<tr><td>" +
__("Search in a document type") +
"</td><td>" +
__("text in document type") +
"</td></tr>\
<tr><td>" +
__("Tags") +
"</td><td>" +
__("tag name..., e.g. #tag") +
"</td></tr>\
<tr><td>" +
__("Open a module or tool") +
"</td><td>" +
__("module name...") +
"</td></tr>\
<tr><td>" +
__("Calculate") +
"</td><td>" +
__("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") +
"</td></tr>\
</table>";
frappe.msgprint(txt, __("Search Help"));
}
set_specifics(txt, end_txt) {
@ -299,7 +358,10 @@ frappe.search.AwesomeBar = class AwesomeBar {
this.options.push({
label: `
<span class="flex justify-between text-medium">
<span class="ellipsis">${__("Search for {0}", [frappe.utils.xss_sanitise(txt).bold()])}</span>
<span class="ellipsis">${__("{0} Search for {1}", [
frappe.search.utils.make_icon("search"),
frappe.utils.xss_sanitise(txt).bold(),
])}</span>
<kbd></kbd>
</span>
`,
@ -324,10 +386,12 @@ frappe.search.AwesomeBar = class AwesomeBar {
var options = {};
options[search_field] = ["like", "%" + txt + "%"];
this.options.push({
label: __("Find {0} in {1}", [
frappe.utils.xss_sanitise(txt).bold(),
__(route[1]).bold(),
]),
label:
frappe.search.utils.make_icon("search") +
__("Find {0} in {1}", [
frappe.utils.xss_sanitise(txt).bold(),
__(route[1]).bold(),
]),
value: __("Find {0} in {1}", [frappe.utils.xss_sanitise(txt), __(route[1])]),
route_options: options,
onclick: function () {
@ -394,7 +458,7 @@ frappe.search.AwesomeBar = class AwesomeBar {
make_random(txt) {
if (txt.toLowerCase().includes("random")) {
this.options.push({
label: __("Generate Random Password"),
label: __("{0} Generate Random Password", [frappe.search.utils.make_icon("key")]),
value: frappe.utils.get_random(16),
onclick: function () {
frappe.msgprint(frappe.utils.get_random(16), __("Result"));

View file

@ -22,13 +22,13 @@
</span>
{% } %}
<div class="input-group search-bar text-muted hidden">
<input
id="navbar-search"
type="text"
class="form-control"
placeholder="{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + K' : 'Ctrl + K']) %}"
aria-haspopup="true"
<div
id="navbar-modal-search"
class=""
placeholder="Search for type a command"
>
{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + K' : 'Ctrl + K']) %}
</div>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>

View file

@ -5,6 +5,11 @@ frappe.search.utils = {
setup_recent: function () {
this.recent = JSON.parse(frappe.boot.user.recent || "[]") || [];
},
make_icon(name) {
return `<span style="padding: 0 3px; margin-right: 5px;">${frappe.utils.icon(
name
)}</span>`;
},
results_to_hide: [],
get_recent_pages: function (keywords) {
if (keywords === null) keywords = "";
@ -66,10 +71,10 @@ frappe.search.utils = {
const doctype = route[1];
if (route.length > 2 && doctype !== route[2]) {
const docname = route[2];
out.label = __(doctype) + " " + docname.bold();
out.label = me.make_icon("sticky-note") + __(doctype) + " " + docname.bold();
out.value = __(doctype) + " " + docname;
} else {
out.label = __(doctype).bold();
out.label = me.make_icon("sticky-note") + __(doctype).bold();
out.value = __(doctype);
}
} else if (
@ -78,24 +83,30 @@ frappe.search.utils = {
) {
const view_type = route[0];
const view_name = route[1];
let icon, labelSuffix;
switch (view_type) {
case "List":
out.label = __("{0} List", [__(view_name).bold()]);
out.value = __("{0} List", [__(view_name)]);
icon = me.make_icon("list");
labelSuffix = "List";
break;
case "Tree":
out.label = __("{0} Tree", [__(view_name).bold()]);
out.value = __("{0} Tree", [__(view_name)]);
icon = me.make_icon("list-tree");
labelSuffix = "Tree";
break;
case "Workspaces":
out.label = __("{0} Workspace", [__(view_name).bold()]);
out.value = __("{0} Workspace", [__(view_name)]);
icon = me.make_icon("wallpaper");
labelSuffix = "Workspace";
break;
case "query-report":
out.label = __("{0} Report", [__(view_name).bold()]);
out.value = __("{0} Report", [__(view_name)]);
icon = me.make_icon("table");
labelSuffix = "Report";
break;
}
out.label = __("{0} {1} {2}", [icon, __(view_name).bold(), labelSuffix]);
out.value = __("{0} {1}", [__(view_name), labelSuffix]);
} else if (match[0]) {
out.label = frappe.utils.escape_html(match[0]).bold();
out.value = match[0];
@ -173,7 +184,10 @@ frappe.search.utils = {
if (level) {
out.push({
type: "New",
label: __("New {0}", [search_result.marked_string || __(item)]),
label: __("{0} New {1}", [
me.make_icon("circle-plus"),
search_result.marked_string || __(item),
]),
value: __("New {0}", [__(item)]),
index: 1 + level,
match: item,
@ -201,6 +215,13 @@ frappe.search.utils = {
} else {
label = __(`{0} ${skip_list ? "" : type}`, [marked_string || __(target)]);
}
if (type === "List") {
label = me.make_icon("list") + label;
} else if (type === "Report") {
label = me.make_icon("table") + label;
} else {
label = me.make_icon("search") + label;
}
return {
type: type,
label: label,
@ -223,7 +244,10 @@ frappe.search.utils = {
var match = item;
out.push({
type: "New",
label: __("New {0}", [search_result.marked_string || __(item)]),
label: __("{0} New {1}", [
me.make_icon("circle-plus"),
search_result.marked_string || __(item),
]),
value: __("New {0}", [__(item)]),
index: score + 0.015,
match: item,
@ -257,7 +281,10 @@ frappe.search.utils = {
else route = ["query-report", item];
out.push({
type: "Report",
label: __("Report {0}", [search_result.marked_string || __(item)]),
label: __("{0} Report {1}", [
me.make_icon("grid-3x3"),
search_result.marked_string || __(item),
]),
value: __("Report {0}", [__(item)]),
index: level,
route: route,
@ -283,7 +310,10 @@ frappe.search.utils = {
var page = me.pages[item];
out.push({
type: "Page",
label: __("Open {0}", [search_result.marked_string || __(item)]),
label: __("{0} Open {1}", [
me.make_icon("file-text"),
search_result.marked_string || __(item),
]),
value: __("Open {0}", [__(item)]),
match: item,
index: level,
@ -295,6 +325,7 @@ frappe.search.utils = {
if (__("calendar").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Calendar",
label: __("{0} Open {1}", [me.make_icon("calendar"), __(target)]),
value: __("Open {0}", [__(target)]),
index: me.fuzzy_search(keywords, "Calendar"),
match: target,
@ -305,6 +336,7 @@ frappe.search.utils = {
if (__("hub").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Hub",
label: __("{0} Open {1}", [me.make_icon("earth-lock"), __(target)]),
value: __("Open {0}", [__(target)]),
index: me.fuzzy_search(keywords, "Hub"),
match: target,
@ -314,6 +346,7 @@ frappe.search.utils = {
if (__("email inbox").indexOf(keywords.toLowerCase()) === 0) {
out.push({
type: "Inbox",
label: __("{0} Open {1}", [me.make_icon("inbox"), __(target)]),
value: __("Open {0}", [__("Email Inbox")]),
index: me.fuzzy_search(keywords, "email inbox"),
match: target,
@ -332,7 +365,10 @@ frappe.search.utils = {
if (level > 0) {
var ret = {
type: "Workspace",
label: __("Open {0}", [search_result.marked_string || __(item.name)]),
label: __("{0} Open {1}", [
me.make_icon("wallpaper"),
search_result.marked_string || __(item.name),
]),
value: __("Open {0}", [__(item.name)]),
index: level,
route: [frappe.router.slug(item.name)],
@ -353,7 +389,10 @@ frappe.search.utils = {
if (level > 0) {
var ret = {
type: "Dashboard",
label: __("{0} Dashboard", [search_result.marked_string || __(item.name)]),
label: __("{0} {1} Dashboard", [
me.make_icon("layout-dashboard"),
search_result.marked_string || __(item.name),
]),
value: __("{0} Dashboard", [__(item.name)]),
index: level,
route: ["dashboard-view", item.name],
@ -675,7 +714,10 @@ frappe.search.utils = {
const search_result = me.fuzzy_search(keywords, item.title, true);
if (search_result.score > 0) {
var ret = {
label: __("Install {0} from Marketplace", [search_result.marked_string]),
label: __("{0} Install {1} from Marketplace", [
me.make_icon("arrow-down-from-line"),
search_result.marked_string,
]),
value: __("Install {0} from Marketplace", [__(item.title)]),
index: search_result.score * 0.8,
route: [

View file

@ -184,7 +184,7 @@ frappe.ui.toolbar.Toolbar = class {
setup_awesomebar() {
if (frappe.boot.desk_settings.search_bar) {
let awesome_bar = new frappe.search.AwesomeBar();
awesome_bar.setup("#navbar-search");
awesome_bar.setup("#navbar-modal-search");
frappe.search.utils.make_function_searchable(
frappe.utils.generate_tracking_url,

View file

@ -922,21 +922,44 @@ Object.assign(frappe.utils, {
let route = route_str.split("/");
if (route[2] === "Report" || route[0] === "query-report") {
return (__(route[3]) || __(route[1])).bold() + " " + __("Report");
return (
frappe.search.utils.make_icon("table") +
(__(route[3]) || __(route[1])).bold() +
" " +
__("Report")
);
}
if (route[0] === "List") {
return __(route[1]).bold() + " " + __("List");
return frappe.search.utils.make_icon("list") + __(route[1]).bold() + " " + __("List");
}
if (route[0] === "modules") {
return __(route[1]).bold() + " " + __("Module");
return (
frappe.search.utils.make_icon("component") +
__(route[1]).bold() +
" " +
__("Module")
);
}
if (route[0] === "Workspaces") {
return __(route[1]).bold() + " " + __("Workspace");
return (
frappe.search.utils.make_icon("wallpaper") +
__(route[1]).bold() +
" " +
__("Workspace")
);
}
if (route[0] === "dashboard") {
return __(route[1]).bold() + " " + __("Dashboard");
return (
frappe.search.utils.make_icon("dashboard") +
__(route[1]).bold() +
" " +
__("Dashboard")
);
}
return __(frappe.utils.to_title_case(__(route[0]), true));
return (
frappe.search.utils.make_icon("file-text") +
__(frappe.utils.to_title_case(__(route[0]), true))
);
},
report_column_total: function (values, column, type) {
if (column.column.disable_total) {

View file

@ -799,7 +799,7 @@ frappe.views.Workspace = class Workspace {
"abcdefghijklmnopqrstuvwxyz".split("").forEach((letter) => {
const default_shortcut = {
action: (e) => {
$("#navbar-search").focus();
$("#navbar-modal-search").click();
return false; // don't prevent default = type the letter in awesomebar
},
page: this.page,

View file

@ -44,3 +44,11 @@
color: var(--ink-gray-9) !important;
}
}
#navbar-modal-search {
border-radius: var(--border-radius-sm);
padding: var(--input-padding);
background-color: var(--control-bg);
width: 100%;
padding-left: 40px;
}

View file

@ -90,6 +90,56 @@
}
}
.navbar-modal-wrapper {
.modal-search-icon {
position: absolute;
}
#navbar-search {
padding-left: 42px;
}
.awesomplete {
width: 100%;
ul {
position: relative;
background-color: transparent;
border: none;
border-radius: unset;
box-shadow: none;
margin-top: 12px;
}
}
.modal-divider {
width: 100%;
height: 1px;
background-color: var(--border-color);
position: absolute;
top: 40px;
border-radius: 50%;
}
}
.cool-awesomebar-modal {
padding: 10px 18px !important;
}
.awesomebar-modal-footer {
font-size: 12px;
.help-navigation {
.help-item-navigate {
margin-right: 1rem;
}
.help-item {
background-color: var(--border-color);
padding: 2px;
margin-right: 0.25rem;
border-radius: 4px;
}
}
}
.navbar.bg-dark {
.dropdown-menu {
font-size: 0.75rem;

View file

@ -23,7 +23,7 @@
.navbar {
padding: 0 1rem;
.navbar-search {
.navbar-modal-search {
width: 75vw;
}
}
@ -59,7 +59,7 @@
color: white;
}
.navbar-search {
.navbar-modal-search {
background-color: var(--blue-400);
width: 300px;
margin-right: 10px;

View file

@ -1,7 +1,7 @@
{% if navbar_search %}
<li>
<form action='/search'>
<input name='q' class='form-control navbar-search' type='text'
<input name='q' class='form-control navbar-modal-search' type='text'
value='{{ frappe.form_dict.q|e if frappe.form_dict.q else ''}}'
{% if not frappe.form_dict.q %}placeholder="{{ _("Search...") }}"{% endif %}>
</form>