forked from enviPath/enviPy
[Feature] Implement Search modal in Modern UI (#185)
Implementing a search modal (stretching the level of dynamic that is possible without going to frameworks). ## Major Change - Search needs packages and is available everywhere now; so had to add reviewed and user packages to global context. Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#185 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
@ -286,7 +286,6 @@
|
||||
</script>
|
||||
{% block modals %}
|
||||
{% include "modals/cite_modal.html" %}
|
||||
{% include "modals/signup_modal.html" %}
|
||||
{% include "modals/predict_modal.html" %}
|
||||
{% include "modals/batch_predict_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,38 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html data-theme="envipath" lang="en">
|
||||
{% load static %}
|
||||
<head>
|
||||
{% load static %}
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<meta name="csrf-token" content="{{ csrf_token }}" />
|
||||
|
||||
{# Favicon #}
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="{% static 'images/favicon.ico' %}"
|
||||
/>
|
||||
|
||||
{# Tailwind CSS + DaisyUI Output #}
|
||||
<link href="{% static 'css/output.css' %}" rel="stylesheet" type="text/css"/>
|
||||
<link
|
||||
href="{% static 'css/output.css' %}"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
||||
{# jQuery - Keep for compatibility with existing JS #}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
|
||||
{# Font Awesome #}
|
||||
<link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
||||
<link
|
||||
href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
{# Discourse embed for community #}
|
||||
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
|
||||
|
||||
<script>
|
||||
const csrftoken = document.querySelector('[name=csrf-token]').content;
|
||||
const csrftoken = document.querySelector("[name=csrf-token]").content;
|
||||
|
||||
// Setup CSRF header for all jQuery AJAX requests
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
|
||||
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Setup CSRF header for all jQuery AJAX requests
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr, settings) {
|
||||
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
|
||||
xhr.setRequestHeader("X-CSRFToken", csrftoken);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
{# General EP JS #}
|
||||
@ -41,136 +52,155 @@
|
||||
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
|
||||
|
||||
{% if not debug %}
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function () {
|
||||
var u = "//matomo.envipath.com/";
|
||||
_paq.push(['setTrackerUrl', u + 'matomo.php']);
|
||||
_paq.push(['setSiteId', '{{ meta.site_id }}']);
|
||||
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
|
||||
g.async = true;
|
||||
g.src = u + 'matomo.js';
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = (window._paq = window._paq || []);
|
||||
_paq.push(["trackPageView"]);
|
||||
_paq.push(["enableLinkTracking"]);
|
||||
(function () {
|
||||
var u = "//matomo.envipath.com/";
|
||||
_paq.push(["setTrackerUrl", u + "matomo.php"]);
|
||||
_paq.push(["setSiteId", "{{ meta.site_id }}"]);
|
||||
var d = document,
|
||||
g = d.createElement("script"),
|
||||
s = d.getElementsByTagName("script")[0];
|
||||
g.async = true;
|
||||
g.src = u + "matomo.js";
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
{% endif %}
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-300">
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-300">
|
||||
{% include "includes/navbar.html" %}
|
||||
|
||||
{# Main Content Area #}
|
||||
<main class="w-full">
|
||||
{% block main_content %}
|
||||
{# Breadcrumbs - outside main content, optional #}
|
||||
{% if breadcrumbs %}
|
||||
<div id="bread" class="max-w-7xl mx-auto px-4 py-4">
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
{% for elem in breadcrumbs %}
|
||||
{% for name, url in elem.items %}
|
||||
{% if forloop.parentloop.last %}
|
||||
<li>{{ name }}</li>
|
||||
{% else %}
|
||||
<li><a href="{{ url }}">{{ name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Main content container - paper effect on medium+ screens #}
|
||||
<div id="docContent" class="w-full xl:w-xl md:mx-auto md:my-8 bg-base-100 md:shadow-2xl md:rounded-lg border-2">
|
||||
{# Messages - inside paper #}
|
||||
{% if message %}
|
||||
<div id="message" class="alert alert-info m-4">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Page content - no enforced styles #}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
|
||||
{# License - inside paper if present #}
|
||||
{% if meta.url_contains_package and meta.current_package.license %}
|
||||
<div class="collapse collapse-arrow bg-base-200 m-8">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
License
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<a target="_blank" href="{{ meta.current_package.license.link }}">
|
||||
<img src="{{ meta.current_package.license.image_link }}" alt="License">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block main_content %}
|
||||
{# Breadcrumbs - outside main content, optional #}
|
||||
{% if breadcrumbs %}
|
||||
<div id="bread" class="max-w-7xl mx-auto px-4 py-4">
|
||||
<div class="text-sm breadcrumbs">
|
||||
<ul>
|
||||
{% for elem in breadcrumbs %}
|
||||
{% for name, url in elem.items %}
|
||||
{% if forloop.parentloop.last %}
|
||||
<li>{{ name }}</li>
|
||||
{% else %}
|
||||
<li><a href="{{ url }}">{{ name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock main_content %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Main content container - paper effect on medium+ screens #}
|
||||
<div
|
||||
id="docContent"
|
||||
class="w-full xl:w-xl md:mx-auto md:my-8 bg-base-100 md:shadow-2xl md:rounded-lg border-2"
|
||||
>
|
||||
{# Messages - inside paper #}
|
||||
{% if message %}
|
||||
<div id="message" class="alert alert-info m-4">{{ message }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# Page content - no enforced styles #}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
|
||||
{# License - inside paper if present #}
|
||||
{% if meta.url_contains_package and meta.current_package.license %}
|
||||
<div class="collapse collapse-arrow bg-base-200 m-8">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">License</div>
|
||||
<div class="collapse-content">
|
||||
<a
|
||||
target="_blank"
|
||||
href="{{ meta.current_package.license.link }}"
|
||||
>
|
||||
<img
|
||||
src="{{ meta.current_package.license.image_link }}"
|
||||
alt="License"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock main_content %}
|
||||
</main>
|
||||
|
||||
{% include "includes/footer.html" %}
|
||||
|
||||
{# Floating Help Tab #}
|
||||
{% if not public_mode %}
|
||||
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
|
||||
<a href="https://community.envipath.org/" target="_blank"
|
||||
class="flex items-center justify-center btn btn-secondary hover:btn-secondary-focus text-secondary-content text-sm shadow-lg transition-all duration-300 hover:scale-105 hover:-translate-x-1"
|
||||
title="Get Help from the Community">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<path d="M12 17h.01"/>
|
||||
</svg>
|
||||
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
|
||||
<a
|
||||
href="https://community.envipath.org/"
|
||||
target="_blank"
|
||||
class="flex items-center justify-center btn btn-secondary hover:btn-secondary-focus text-secondary-content text-sm shadow-lg transition-all duration-300 hover:scale-105 hover:-translate-x-1"
|
||||
title="Get Help from the Community"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"
|
||||
>
|
||||
<path
|
||||
d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"
|
||||
/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Modals - TODO: Convert these to DaisyUI modals #}
|
||||
{% block modals %}
|
||||
{# Note: These modals still use Bootstrap markup and will need conversion #}
|
||||
{% include "modals/cite_modal.html" %}
|
||||
{% include "modals/signup_modal.html" %}
|
||||
{% include "modals/predict_modal.html" %}
|
||||
{% include "modals/batch_predict_modal.html" %}
|
||||
{% include "modals/search_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
// Hide actionsbutton if there's no action defined
|
||||
if ($('#actionsButton ul').children().length > 0) {
|
||||
$('#actionsButton').show();
|
||||
}
|
||||
$(function () {
|
||||
// Hide actionsbutton if there's no action defined
|
||||
if ($("#actionsButton ul").children().length > 0) {
|
||||
$("#actionsButton").show();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
|
||||
document.addEventListener("keydown", function (event) {
|
||||
// Check if user is typing in an input field
|
||||
const activeElement = document.activeElement;
|
||||
const isInputField =
|
||||
activeElement &&
|
||||
(activeElement.tagName === "INPUT" ||
|
||||
activeElement.tagName === "TEXTAREA" ||
|
||||
activeElement.contentEditable === "true");
|
||||
|
||||
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
|
||||
document.addEventListener('keydown', function(event) {
|
||||
// Check if user is typing in an input field
|
||||
const activeElement = document.activeElement;
|
||||
const isInputField = activeElement && (
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.contentEditable === 'true'
|
||||
);
|
||||
if (isInputField) {
|
||||
return; // Don't trigger shortcut when typing in input fields
|
||||
}
|
||||
|
||||
if (isInputField) {
|
||||
return; // Don't trigger shortcut when typing in input fields
|
||||
}
|
||||
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
|
||||
const isMac = /Mac/.test(navigator.platform);
|
||||
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
|
||||
|
||||
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
|
||||
const isMac = /Mac/.test(navigator.platform);
|
||||
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
|
||||
|
||||
if (isCorrectModifier && event.key === 'k') {
|
||||
event.preventDefault();
|
||||
window.location.href = '/search';
|
||||
}
|
||||
});
|
||||
if (isCorrectModifier && event.key === "k") {
|
||||
event.preventDefault();
|
||||
search_modal.showModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,71 +1,147 @@
|
||||
{% load static %}
|
||||
{# Modern DaisyUI Navbar #}
|
||||
<div class="navbar bg-neutral-50 text-neutral-950 shadow-lg x-50">
|
||||
<div class="navbar-start">
|
||||
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl">
|
||||
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img">
|
||||
<use href='{% static "/images/logo-name.svg" %}#ep-logo-name' />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-start">
|
||||
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl">
|
||||
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img">
|
||||
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if not public_mode %}
|
||||
{% if not public_mode %}
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<a href="#" role="button" class="btn btn-ghost" id="predictLink">Predict</a>
|
||||
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
|
||||
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
|
||||
<div class="dropdown dropdown-center">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
|
||||
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
|
||||
<li><a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a></li>
|
||||
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
|
||||
<li><a href="{{ meta.server_url }}/compound" id="compoundLink">Compound</a></li>
|
||||
<li><a href="{{ meta.server_url }}/reaction" id="reactionLink">Reaction</a></li>
|
||||
<li><a href="{{ meta.server_url }}/model" id="relative-reasoningLink">Model</a></li>
|
||||
<li><a href="{{ meta.server_url }}/scenario" id="scenarioLink">Scenario</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="#" role="button" class="btn btn-ghost" id="predictLink"
|
||||
>Predict</a
|
||||
>
|
||||
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
|
||||
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
|
||||
<div class="dropdown dropdown-center">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
|
||||
>
|
||||
<li>
|
||||
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a>
|
||||
</li>
|
||||
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
|
||||
<li>
|
||||
<a href="{{ meta.server_url }}/compound" id="compoundLink"
|
||||
>Compound</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ meta.server_url }}/reaction" id="reactionLink"
|
||||
>Reaction</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ meta.server_url }}/model" id="relative-reasoningLink"
|
||||
>Model</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ meta.server_url }}/scenario" id="scenarioLink"
|
||||
>Scenario</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="navbar-end">
|
||||
{% if not public_mode %}
|
||||
<a href="/search" role="button" >
|
||||
<div class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
<span id="search-shortcut">⌘K</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if meta.user.username == 'anonymous' or public_mode %}
|
||||
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
|
||||
{% else %}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost m-1 btn-circle" id="loggedInButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>
|
||||
</div>
|
||||
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm">
|
||||
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
|
||||
<li>
|
||||
<form id="logoutForm" action="{% url 'logout' %}" method="post" style="display: none;">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="logout" value="true">
|
||||
</form>
|
||||
<a href="#" id="logoutButton" onclick="event.preventDefault(); document.getElementById('logoutForm').submit();">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
{% if not public_mode %}
|
||||
<button
|
||||
type="button"
|
||||
onclick="search_modal.showModal()"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
<div
|
||||
class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-search-icon lucide-search"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
<span id="search-shortcut">⌘K</span>
|
||||
</div>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if meta.user.username == 'anonymous' or public_mode %}
|
||||
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
|
||||
{% else %}
|
||||
<div class="dropdown dropdown-end">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-ghost m-1 btn-circle"
|
||||
id="loggedInButton"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-circle-user-icon lucide-circle-user"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
<path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="-1"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm"
|
||||
>
|
||||
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
|
||||
<li>
|
||||
<form
|
||||
id="logoutForm"
|
||||
action="{% url 'logout' %}"
|
||||
method="post"
|
||||
style="display: none;"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="logout" value="true" />
|
||||
</form>
|
||||
<a
|
||||
href="#"
|
||||
id="logoutButton"
|
||||
onclick="event.preventDefault(); document.getElementById('logoutForm').submit();"
|
||||
>Logout</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// OS-aware search shortcut display
|
||||
(function() {
|
||||
const isMac = /Mac/.test(navigator.platform);
|
||||
const shortcutElement = document.getElementById('search-shortcut');
|
||||
if (shortcutElement) {
|
||||
shortcutElement.textContent = isMac ? '⌘K' : 'Ctrl+K';
|
||||
}
|
||||
})();
|
||||
// OS-aware search shortcut display
|
||||
(function () {
|
||||
const isMac = /Mac/.test(navigator.platform);
|
||||
const shortcutElement = document.getElementById("search-shortcut");
|
||||
if (shortcutElement) {
|
||||
shortcutElement.textContent = isMac ? "⌘K" : "Ctrl+K";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
535
templates/modals/search_modal.html
Normal file
535
templates/modals/search_modal.html
Normal file
@ -0,0 +1,535 @@
|
||||
{% load static %}
|
||||
<dialog id="search_modal" class="modal @max-sm:modal-top justify-center">
|
||||
<div class="modal-box w-lvw sm:w-[85vw] sm:max-w-5xl h-full sm:h-8/12 p-1" >
|
||||
|
||||
<!-- Search Input and Mode Selector -->
|
||||
<div class="form-control mb-4 flex-shrink-0 w-full">
|
||||
<div class="join w-full m-0 p-3 items-center">
|
||||
<label class="input join-item input-ghost grow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
|
||||
<input type="text" autofocus
|
||||
id="modal_searchbar"
|
||||
placeholder="Benfuracarb"
|
||||
class="grow" aria-label="Search" />
|
||||
</label>
|
||||
|
||||
<!-- Mode Dropdown -->
|
||||
<div>
|
||||
<button type="button"
|
||||
tabindex="0"
|
||||
id="modal_mode_button"
|
||||
popovertarget="search_dropdown_menu" style="anchor-name:--anchor-1"
|
||||
class="btn join-item btn-ghost">
|
||||
Text
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul tabindex="0"" class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-64 p-2 shadow-lg" popover id="search_dropdown_menu" style="position-anchor:--anchor-1">
|
||||
<li class="menu-title">Text</li>
|
||||
<li>
|
||||
<a id="modal_dropdown_text"
|
||||
class="tooltip tooltip-left"
|
||||
data-tip="Search on object names and descriptions">
|
||||
Text
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-title">SMILES</li>
|
||||
<li>
|
||||
<a id="modal_dropdown_smiles_default"
|
||||
class="tooltip tooltip-left"
|
||||
data-tip="Ignores stereochemistry and charge">
|
||||
Default
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="modal_dropdown_smiles_canonical"
|
||||
class="tooltip tooltip-left"
|
||||
data-tip="Ignores stereochemistry, preserves charge">
|
||||
Canonical
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a id="modal_dropdown_smiles_exact"
|
||||
class="tooltip tooltip-left"
|
||||
data-tip="Exact match for stereochemistry and charge">
|
||||
Exact
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-title">InChI</li>
|
||||
<li>
|
||||
<a id="modal_dropdown_inchikey"
|
||||
class="tooltip tooltip-left"
|
||||
data-tip="Search by InChIKey">
|
||||
InChIKey
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button type="button" id="modal_search_button" class="btn btn-xs btn-ghost join-item">
|
||||
<kbd class="kbd kbd-sm p-1 text-base-content/50">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>
|
||||
</kbd>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Package Selector with Pills -->
|
||||
<div class="form-control mb-4 flex-shrink-0">
|
||||
<!-- Pills Container -->
|
||||
<div id="modal_package_pills_container"
|
||||
class="flex flex-wrap gap-2 p-3 border-2 border-dashed border-base-300 rounded-lg m-3 min-h-[3rem] items-center">
|
||||
<!-- Pills will be added here dynamically -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Package Dropdown Menu -->
|
||||
<ul class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-80 max-h-96 overflow-y-auto p-2 shadow-lg"
|
||||
popover
|
||||
id="package_dropdown_menu"
|
||||
style="position-anchor:--anchor-packages">
|
||||
{% if unreviewed_packages %}
|
||||
<li class="menu-title">Reviewed Packages</li>
|
||||
{% for obj in reviewed_packages %}
|
||||
<li>
|
||||
<a class="package-option flex justify-between items-center"
|
||||
data-package-url="{{ obj.url }}"
|
||||
data-package-name="{{ obj.name }}">
|
||||
<span>{{ obj.name }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="menu-title">Unreviewed Packages</li>
|
||||
{% for obj in unreviewed_packages %}
|
||||
<li>
|
||||
<a class="package-option flex justify-between items-center"
|
||||
data-package-url="{{ obj.url }}"
|
||||
data-package-name="{{ obj.name }}">
|
||||
<span>{{ obj.name }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="menu-title">Reviewed Packages</li>
|
||||
{% for obj in reviewed_packages %}
|
||||
<li>
|
||||
<a class="package-option flex justify-between items-center"
|
||||
data-package-url="{{ obj.url }}"
|
||||
data-package-name="{{ obj.name }}">
|
||||
<span>{{ obj.name }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div id="search_loading" class="hidden justify-center py-8 flex-shrink-0">
|
||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||
</div>
|
||||
|
||||
<!-- Results Container - scrollable -->
|
||||
<div id="search_results" class="flex-1 overflow-y-auto min-h-0 p-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop to close -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Package Selector Module - Data-driven multiselect package selection
|
||||
const PackageSelector = {
|
||||
// Single source of truth: array of selected packages
|
||||
selectedPackages: [],
|
||||
|
||||
elements: {
|
||||
pillsContainer: null,
|
||||
packageDropdown: null,
|
||||
packageOptions: null
|
||||
},
|
||||
|
||||
init() {
|
||||
this.cacheElements();
|
||||
this.loadInitialSelection();
|
||||
this.attachEventListeners();
|
||||
this.render();
|
||||
},
|
||||
|
||||
cacheElements() {
|
||||
this.elements.pillsContainer = document.getElementById('modal_package_pills_container');
|
||||
this.elements.packageDropdown = document.getElementById('package_dropdown_menu');
|
||||
this.elements.packageOptions = document.querySelectorAll('.package-option');
|
||||
},
|
||||
|
||||
loadInitialSelection() {
|
||||
// Load pre-selected packages from server-rendered pills
|
||||
const existingPills = this.elements.pillsContainer.querySelectorAll('.badge');
|
||||
existingPills.forEach(pill => {
|
||||
this.selectedPackages.push({
|
||||
url: pill.dataset.packageUrl,
|
||||
name: pill.dataset.packageName
|
||||
});
|
||||
});
|
||||
|
||||
// If no pills found, select all reviewed packages by default
|
||||
if (this.selectedPackages.length === 0) {
|
||||
// Iterate through all menu items and collect reviewed packages
|
||||
const menuItems = this.elements.packageDropdown.querySelectorAll('li');
|
||||
|
||||
for (const item of menuItems) {
|
||||
// Check if this is the "Unreviewed Packages" menu title
|
||||
if (item.classList.contains('menu-title') &&
|
||||
item.textContent.trim() === 'Unreviewed Packages') {
|
||||
break; // Stop processing after this point
|
||||
}
|
||||
|
||||
// Check for package options (only reviewed packages reach here)
|
||||
const packageOption = item.querySelector('.package-option');
|
||||
if (packageOption) {
|
||||
this.selectedPackages.push({
|
||||
url: packageOption.dataset.packageUrl,
|
||||
name: packageOption.dataset.packageName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
attachEventListeners() {
|
||||
// Toggle package selection on dropdown item click
|
||||
this.elements.packageOptions.forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent dropdown from closing
|
||||
const packageUrl = option.dataset.packageUrl;
|
||||
const packageName = option.dataset.packageName;
|
||||
this.togglePackageSelection(packageUrl, packageName);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove package when X is clicked (using event delegation)
|
||||
this.elements.pillsContainer.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('package-remove-btn') || e.target.closest('.package-remove-btn')) {
|
||||
const pill = e.target.closest('.badge');
|
||||
if (pill) {
|
||||
const packageUrl = pill.dataset.packageUrl;
|
||||
this.removePackage(packageUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
togglePackageSelection(packageUrl, packageName) {
|
||||
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
|
||||
|
||||
if (index !== -1) {
|
||||
// Remove from selection
|
||||
this.selectedPackages.splice(index, 1);
|
||||
} else {
|
||||
// Add to selection
|
||||
this.selectedPackages.push({ url: packageUrl, name: packageName });
|
||||
}
|
||||
|
||||
this.render();
|
||||
},
|
||||
|
||||
removePackage(packageUrl) {
|
||||
const index = this.selectedPackages.findIndex(pkg => pkg.url === packageUrl);
|
||||
if (index !== -1) {
|
||||
this.selectedPackages.splice(index, 1);
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
this.renderPills();
|
||||
this.renderAddButton();
|
||||
this.renderCheckmarks();
|
||||
},
|
||||
|
||||
renderPills() {
|
||||
// Clear existing pills and button (except placeholder)
|
||||
const pills = this.elements.pillsContainer.querySelectorAll('.badge');
|
||||
pills.forEach(pill => pill.remove());
|
||||
|
||||
const existingButton = this.elements.pillsContainer.querySelector('#modal_package_add_button');
|
||||
if (existingButton) {
|
||||
existingButton.remove();
|
||||
}
|
||||
|
||||
// Create pills from data
|
||||
this.selectedPackages.forEach(pkg => {
|
||||
const pill = this.createPillElement(pkg.url, pkg.name);
|
||||
this.elements.pillsContainer.appendChild(pill);
|
||||
});
|
||||
},
|
||||
|
||||
renderAddButton() {
|
||||
// Only render button if there are packages available
|
||||
if (this.elements.packageOptions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.id = 'modal_package_add_button';
|
||||
button.setAttribute('popovertarget', 'package_dropdown_menu');
|
||||
button.style.cssText = 'anchor-name:--anchor-packages';
|
||||
button.className = 'btn btn-sm btn-ghost gap-2 text-base-content/50';
|
||||
|
||||
button.innerHTML = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
Add Package
|
||||
`;
|
||||
|
||||
this.elements.pillsContainer.appendChild(button);
|
||||
},
|
||||
|
||||
createPillElement(packageUrl, packageName) {
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'badge badge-outline gap-2 max-w-xs';
|
||||
pill.dataset.packageUrl = packageUrl;
|
||||
pill.dataset.packageName = packageName;
|
||||
|
||||
pill.innerHTML = `
|
||||
<span class="truncate" title="${packageName}">${packageName}</span>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 cursor-pointer hover:text-error package-remove-btn flex-shrink-0 rotate-45"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14"/><path d="M12 5v14"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return pill;
|
||||
},
|
||||
|
||||
renderCheckmarks() {
|
||||
// Update all checkmarks based on selected packages
|
||||
this.elements.packageOptions.forEach(option => {
|
||||
const packageUrl = option.dataset.packageUrl;
|
||||
const isSelected = this.selectedPackages.some(pkg => pkg.url === packageUrl);
|
||||
const checkmark = option.querySelector('.package-checkmark');
|
||||
|
||||
if (checkmark) {
|
||||
checkmark.classList.toggle('hidden', !isSelected);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getSelectedPackages() {
|
||||
return this.selectedPackages.map(pkg => pkg.url);
|
||||
}
|
||||
};
|
||||
|
||||
// Modal and Search Management
|
||||
const modal = document.getElementById('search_modal');
|
||||
const searchbar = document.getElementById('modal_searchbar');
|
||||
const searchButton = document.getElementById('modal_search_button');
|
||||
const modeButton = document.getElementById('modal_mode_button');
|
||||
const resultsDiv = document.getElementById('search_results');
|
||||
const loadingDiv = document.getElementById('search_loading');
|
||||
|
||||
// MutationObserver to detect when modal opens
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'open' && modal.open) {
|
||||
PackageSelector.render();
|
||||
// Delay focus to allow CSS transitions to complete (modal has 0.3s transition)
|
||||
setTimeout(() => {
|
||||
searchbar.focus();
|
||||
}, 320);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(modal, { attributes: true });
|
||||
|
||||
// Clear results when modal closes
|
||||
modal.addEventListener('close', function() {
|
||||
resultsDiv.innerHTML = '';
|
||||
loadingDiv.classList.add('hidden');
|
||||
searchbar.value = '';
|
||||
});
|
||||
|
||||
|
||||
// Mode dropdown handlers
|
||||
const dropdownMenu = document.getElementById('search_dropdown_menu');
|
||||
|
||||
const modeButtons = [
|
||||
{ id: 'modal_dropdown_text', text: 'Text' },
|
||||
{ id: 'modal_dropdown_smiles_default', text: 'Default' },
|
||||
{ id: 'modal_dropdown_smiles_canonical', text: 'Canonical' },
|
||||
{ id: 'modal_dropdown_smiles_exact', text: 'Exact' },
|
||||
{ id: 'modal_dropdown_inchikey', text: 'InChIKey' }
|
||||
];
|
||||
|
||||
modeButtons.forEach(({ id, text }) => {
|
||||
document.getElementById(id).addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
modeButton.innerHTML = text + ` <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>`;
|
||||
// Close dropdown using popover API
|
||||
if (dropdownMenu && typeof dropdownMenu.hidePopover === 'function') {
|
||||
dropdownMenu.hidePopover();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize Package Selector
|
||||
PackageSelector.init();
|
||||
|
||||
// Search Response Handler
|
||||
function handleSearchResponse(data) {
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
function makeContent(objs) {
|
||||
let links = '';
|
||||
objs.forEach(obj => {
|
||||
links += `<a href="${obj.url}" class="block px-4 py-2 hover:bg-base-300 rounded-lg transition-colors">${obj.name}</a>`;
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
let allEmpty = true;
|
||||
let content = '';
|
||||
|
||||
// Category order for better UX
|
||||
const categoryOrder = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways'];
|
||||
|
||||
categoryOrder.forEach(key => {
|
||||
if (!data[key] || data[key].length < 1) {
|
||||
return;
|
||||
}
|
||||
allEmpty = false;
|
||||
|
||||
content += `
|
||||
<div class="collapse collapse-arrow bg-base-200 mb-2">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title font-medium">
|
||||
${key} <span class="badge badge-neutral badge-sm ml-2">${data[key].length}</span>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
${makeContent(data[key])}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (allEmpty) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="mb-2">
|
||||
<div class="text-sm font-semibold text-base-content/70 mb-2">Results</div>
|
||||
${content}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Search Execution
|
||||
function performSearch(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const query = searchbar.value.trim();
|
||||
|
||||
if (!query) {
|
||||
console.log('Search phrase empty, won\'t do search');
|
||||
return;
|
||||
}
|
||||
|
||||
const selPacks = PackageSelector.getSelectedPackages();
|
||||
|
||||
if (selPacks.length < 1) {
|
||||
console.log('No package selected, won\'t do search');
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Please select at least one package</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = modeButton.textContent.trim().toLowerCase();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
selPacks.forEach(pack => params.append('packages', pack));
|
||||
params.append('search', query);
|
||||
params.append('mode', mode);
|
||||
|
||||
// Show loading
|
||||
loadingDiv.classList.remove('hidden');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
fetch(`{{ SERVER_BASE }}/search?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Search request failed');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
loadingDiv.classList.add('hidden');
|
||||
handleSearchResponse(result);
|
||||
})
|
||||
.catch(error => {
|
||||
loadingDiv.classList.add('hidden');
|
||||
console.error('Search error:', error);
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span>Search failed. Please try again.</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners for search
|
||||
searchButton.addEventListener('click', performSearch);
|
||||
searchbar.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch(e);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -1,110 +0,0 @@
|
||||
<div class="modal fade bs-modal-sm" id="signupmodal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<br>
|
||||
<div class="bs-example bs-example-tabs">
|
||||
<ul id="myTab" class="nav nav-tabs">
|
||||
<li class="active">
|
||||
<a href="#signin" data-toggle="tab">Sign In</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a href="#signup" data-toggle="tab">Register</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a href="#why" data-toggle="tab">Why?</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="myTabContent" class="tab-content">
|
||||
<div class="tab-pane fade active in" id="signin">
|
||||
<form class="form-horizontal" method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<input type="hidden" name="login" id="login" value="true"/>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="username">Username:</label>
|
||||
<div class="controls">
|
||||
<input required id="username" name="username" type="text" class="form-control"
|
||||
placeholder="username" autocomplete="username">
|
||||
</div>
|
||||
<label class="control-label" for="passwordinput">Password:</label>
|
||||
<div class="controls">
|
||||
<input required id="passwordinput" name="password" class="form-control"
|
||||
type="password" placeholder="********" autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button -->
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="signin"></label>
|
||||
<div class="controls">
|
||||
<button id="signin" name="signin" class="btn btn-success">Sign In</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Why tab -->
|
||||
<div class="tab-pane fade in" id="why">
|
||||
<p>After you register, you have more permissions on
|
||||
this site, e.g., can create your own
|
||||
packages, submit data for review, and set access
|
||||
permissions to your data.</p>
|
||||
<p></p>
|
||||
<p>
|
||||
<br> Please
|
||||
contact <a href="mailto:admin@envipath.org">admin@envipath.org</a>
|
||||
if you have any questions.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Register -->
|
||||
<div class="tab-pane fade"
|
||||
id="signup">
|
||||
<div id="passwordGuideline" class="alert alert-info">
|
||||
The password must contain 8 to 30 characters<br>
|
||||
The following characters are allowed:
|
||||
- Upper and lower case characters<br>
|
||||
- Digits<br>
|
||||
- Special characters _, -, +<br>
|
||||
|
||||
</div>
|
||||
|
||||
<form id="signup-action" class="form-horizontal" action="{% url 'login' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="register" id="register" value="true"/>
|
||||
|
||||
<label class="control-label" for="userid">Username:</label>
|
||||
<input id="userid" name="username" class="form-control" type="text" placeholder="user" required autocomplete="username">
|
||||
|
||||
<label class="control-label" for="email">Email:</label>
|
||||
<input id="email" name="email" class="form-control" type="email" placeholder="user@envipath.org" required>
|
||||
|
||||
<label class="control-label" for="password">Password:</label>
|
||||
<input id="password" name="password" class="form-control" type="password" placeholder="********" required autocomplete="new-password">
|
||||
|
||||
<label class="control-label" for="rpassword">Repeat Password:</label>
|
||||
<input id="rpassword" name="rpassword" class="form-control" type="password" placeholder="********" required autocomplete="new-password">
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="confirmsignup"></label>
|
||||
<div class="controls">
|
||||
<button id="confirmsignup" name="confirmsignup" class="btn btn-success">Sign Up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<center>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user