~netlandish/links-dev

links: Stop abusing tag autocomplete v1 APPLIED

Peter Sanchez: 1
 Stop abusing tag autocomplete

 3 files changed, 155 insertions(+), 74 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/183/mbox | git am -3
Learn more about email & git

[PATCH links] Stop abusing tag autocomplete Export this patch

---
 core/routes.go              |  11 ++-
 static/js/advancedsearch.js | 133 +++++++++++++++++++++++-------------
 static/js/autocomplete.js   |  85 ++++++++++++++++-------
 3 files changed, 155 insertions(+), 74 deletions(-)

diff --git a/core/routes.go b/core/routes.go
index 5e8fd53..39a13f6 100644
--- a/core/routes.go
+++ b/core/routes.go
@@ -1,6 +1,7 @@
package core

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
@@ -30,6 +31,7 @@ import (
	"netlandish.com/x/gobwebs/database"
	"netlandish.com/x/gobwebs/messages"
	"netlandish.com/x/gobwebs/server"
	"netlandish.com/x/gobwebs/timezone"
	"netlandish.com/x/gobwebs/validate"
)

@@ -3447,7 +3449,14 @@ func (s *Service) TagAutocomplete(c echo.Context) error {
		} else {
			return c.JSON(http.StatusOK, "{}")
		}
		tags, err = models.GetTags(c.Request().Context(), opts)
		
		// Create sql.DB context with timezone to avoid DBI connection hoarding
		dbi := database.DBIForContext(c.Request().Context())
		tz := timezone.ForContext(c.Request().Context())
		newCtx := database.Context(context.Background(), dbi.GetDB())
		newCtx = timezone.Context(newCtx, tz)
		
		tags, err = models.GetTags(newCtx, opts)
		if err != nil {
			return err
		}
diff --git a/static/js/advancedsearch.js b/static/js/advancedsearch.js
index f800ce2..21ff3b6 100644
--- a/static/js/advancedsearch.js
+++ b/static/js/advancedsearch.js
@@ -8,6 +8,10 @@ var tagInputDiv = document.getElementById("advanced-search-div")
// Track highlighted index for each autocomplete dropdown
var highlightedIndexes = new WeakMap();

// Track debounce timers and active requests for each tag selector
var debounceTimers = new WeakMap();
var activeRequests = new WeakMap();

// Track if we should allow form submission
var allowFormSubmit = true;

@@ -92,6 +96,19 @@ function autocomplete(event) {
        return;
    }
    
    // Cancel previous request for this tag selector if active
    var activeRequest = activeRequests.get(tagSelector);
    if (activeRequest) {
        activeRequest.abort();
        activeRequests.delete(tagSelector);
    }
    
    // Clear existing debounce timer for this tag selector
    var debounceTimer = debounceTimers.get(tagSelector);
    if (debounceTimer) {
        clearTimeout(debounceTimer);
    }
    
    tags = this.value.split(",")
    tag = tags[tags.length -1].trim()

@@ -100,56 +117,78 @@ function autocomplete(event) {
        autocompleteTags = this.nextElementSibling;
    }
    if (tag !== "") {
        // Store the query we're searching for
        var searchQuery = tag;
        
        fetch(url + "&q=" + tag)
            .then(function (response) {
                if (!response.ok) {
                    console.log("Network response was not ok");
                }
                return response.json();
            })
            .then(function (data) {
                // Check if the current input still matches what we searched for
                var currentTags = tagSelector.value.split(",");
                var currentTag = currentTags[currentTags.length - 1].trim();
                
                if (autocompleteTags !== undefined && currentTag === searchQuery) {
                    showSuggestions(autocompleteTags, data);
        // Debounce the request for 300ms
        var newTimer = setTimeout(function() {
            // Store the query we're searching for
            var searchQuery = tag;
            
            // Create AbortController for request cancellation
            var controller = new AbortController();
            activeRequests.set(tagSelector, controller);
            
            fetch(url + "&q=" + tag, { signal: controller.signal })
                .then(function (response) {
                    if (!response.ok) {
                        console.log("Network response was not ok");
                    }
                    return response.json();
                })
                .then(function (data) {
                    // Clear active request on completion
                    if (activeRequests.get(tagSelector) === controller) {
                        activeRequests.delete(tagSelector);
                    }
                    
                    // Set up click handler for this dropdown
                    autocompleteTags.onclick = function(event) {
                        if (event.target.classList.contains("tag-suggested")) {
                            var tags = tagSelector.value.split(",");
                            var newTag = event.target.textContent + ", ";
                            if (tags.length > 1) {
                                newTag = " " + newTag;
                    // Check if the current input still matches what we searched for
                    var currentTags = tagSelector.value.split(",");
                    var currentTag = currentTags[currentTags.length - 1].trim();
                    
                    if (autocompleteTags !== undefined && currentTag === searchQuery) {
                        showSuggestions(autocompleteTags, data);
                        
                        // Set up click handler for this dropdown
                        autocompleteTags.onclick = function(event) {
                            if (event.target.classList.contains("tag-suggested")) {
                                var tags = tagSelector.value.split(",");
                                var newTag = event.target.textContent + ", ";
                                if (tags.length > 1) {
                                    newTag = " " + newTag;
                                }
                                tags[tags.length - 1] = newTag;
                                tagSelector.value = tags.join(",");
                                autocompleteTags.classList.add("d-none");
                                highlightedIndexes.set(autocompleteTags, -1);
                                tagSelector.focus();
                            }
                            tags[tags.length - 1] = newTag;
                            tagSelector.value = tags.join(",");
                            autocompleteTags.classList.add("d-none");
                            highlightedIndexes.set(autocompleteTags, -1);
                            tagSelector.focus();
                        }
                    };
                        };
                        
                        // Set up mouseover handler for this dropdown
                        autocompleteTags.onmouseover = function(event) {
                            if (event.target.classList.contains("tag-suggested")) {
                                var index = parseInt(event.target.getAttribute("data-index"));
                                highlightSuggestion(autocompleteTags, index);
                            }
                        };
                    } else if (autocompleteTags !== undefined) {
                        // Input has changed, hide the dropdown
                        autocompleteTags.classList.add("d-none");
                        highlightedIndexes.set(autocompleteTags, -1);
                    }
                })
                .catch(function (error) {
                    // Clear active request on error (unless aborted)
                    if (activeRequests.get(tagSelector) === controller) {
                        activeRequests.delete(tagSelector);
                    }
                    
                    // Set up mouseover handler for this dropdown
                    autocompleteTags.onmouseover = function(event) {
                        if (event.target.classList.contains("tag-suggested")) {
                            var index = parseInt(event.target.getAttribute("data-index"));
                            highlightSuggestion(autocompleteTags, index);
                        }
                    };
                } else if (autocompleteTags !== undefined) {
                    // Input has changed, hide the dropdown
                    autocompleteTags.classList.add("d-none");
                    highlightedIndexes.set(autocompleteTags, -1);
                }
            })
            .catch(function (error) {
                console.log("Fetch error:", error);
            });
                    // Don't log aborted requests as errors
                    if (error.name !== 'AbortError') {
                        console.log("Fetch error:", error);
                    }
                });
        }, 300);
        
        debounceTimers.set(tagSelector, newTimer);
    } else {
        if (autocompleteTags !== undefined) {
            autocompleteTags.classList.add("d-none");
diff --git a/static/js/autocomplete.js b/static/js/autocomplete.js
index 5593795..30b079a 100644
--- a/static/js/autocomplete.js
+++ b/static/js/autocomplete.js
@@ -3,6 +3,8 @@ var tagSelector = document.getElementById("tag-selector");
var autocompleteTags = document.getElementById("autocomplete-tags");
var highlightedIndex = -1;
var suggestions = [];
var debounceTimer;
var activeRequest;

// Function to highlight a suggestion
function highlightSuggestion(index) {
@@ -111,36 +113,67 @@ tagSelector.addEventListener("keyup", function(event) {
        return;
    }
    
    // Cancel previous request if active
    if (activeRequest) {
        activeRequest.abort();
        activeRequest = null;
    }
    
    // Clear existing debounce timer
    if (debounceTimer) {
        clearTimeout(debounceTimer);
    }
    
    tags = this.value.split(",");
    tag = tags[tags.length - 1].trim();

    if (tag !== "") {
        // Store the query we're searching for
        var searchQuery = tag;
        
        fetch(url + "&q=" + tag)
            .then(function (response) {
                if (!response.ok) {
                    console.log("Network response was not ok");
                }
                return response.json();
            })
            .then(function (data) {
                // Check if the current input still matches what we searched for
                var currentTags = tagSelector.value.split(",");
                var currentTag = currentTags[currentTags.length - 1].trim();
                
                if (currentTag === searchQuery) {
                    showSuggestions(data);
                } else {
                    // Input has changed, hide the dropdown
                    autocompleteTags.classList.add("d-none");
                    highlightedIndex = -1;
                }
            })
            .catch(function (error) {
                console.log("Fetch error:", error);
            });
        // Debounce the request for 300ms
        debounceTimer = setTimeout(function() {
            // Store the query we're searching for
            var searchQuery = tag;
            
            // Create AbortController for request cancellation
            var controller = new AbortController();
            activeRequest = controller;
            
            fetch(url + "&q=" + tag, { signal: controller.signal })
                .then(function (response) {
                    if (!response.ok) {
                        console.log("Network response was not ok");
                    }
                    return response.json();
                })
                .then(function (data) {
                    // Clear active request on completion
                    if (activeRequest === controller) {
                        activeRequest = null;
                    }
                    
                    // Check if the current input still matches what we searched for
                    var currentTags = tagSelector.value.split(",");
                    var currentTag = currentTags[currentTags.length - 1].trim();
                    
                    if (currentTag === searchQuery) {
                        showSuggestions(data);
                    } else {
                        // Input has changed, hide the dropdown
                        autocompleteTags.classList.add("d-none");
                        highlightedIndex = -1;
                    }
                })
                .catch(function (error) {
                    // Clear active request on error (unless aborted)
                    if (activeRequest === controller) {
                        activeRequest = null;
                    }
                    
                    // Don't log aborted requests as errors
                    if (error.name !== 'AbortError') {
                        console.log("Fetch error:", error);
                    }
                });
        }, 300);
    } else {
        autocompleteTags.classList.add("d-none");
        highlightedIndex = -1;
-- 
2.49.1
Applied.

To git@git.code.netlandish.com:~netlandish/links
   4f1684a..5f89d68  master -> master