Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions interface/lib/cookieSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Enhanced search functionality with fuzzy matching for cookies.
*/
export class CookieSearch {
/**
* Performs fuzzy search on cookie name.
* @param {string} searchTerm The term to search for.
* @param {string} cookieName The cookie name to match against.
* @return {number} Score between 0-1, where higher means better match.
*/
static fuzzyMatch(searchTerm, cookieName) {
searchTerm = searchTerm.toLowerCase();
cookieName = cookieName.toLowerCase();

// Exact match gets highest score
if (cookieName === searchTerm) {
return 1;
}

// Contains match gets high score
if (cookieName.includes(searchTerm)) {
return 0.8;
}

// Fuzzy match - check if all characters appear in order
let score = 0;
let termIndex = 0;

for (
let i = 0;
i < cookieName.length && termIndex < searchTerm.length;
i++
) {
if (cookieName[i] === searchTerm[termIndex]) {
score += 1;
termIndex++;
}
}

// If we matched all characters, return proportional score
if (termIndex === searchTerm.length) {
return (score / cookieName.length) * 0.6;
}

return 0;
}

/**
* Search cookies with multiple criteria.
* @param {Array} cookies Array of cookie objects to search.
* @param {string} searchTerm The search term.
* @param {object} options Search options.
* @return {Array} Filtered and sorted cookies.
*/
static search(cookies, searchTerm, options = {}) {
const {
searchInValue = false,
searchInDomain = false,
minScore = 0.3,
} = options;

if (!searchTerm) {
return cookies;
}

const results = [];

for (const cookie of cookies) {
let maxScore = this.fuzzyMatch(searchTerm, cookie.name);

if (searchInValue && cookie.value) {
maxScore = Math.max(
maxScore,
this.fuzzyMatch(searchTerm, cookie.value) * 0.7
);
}

if (searchInDomain && cookie.domain) {
maxScore = Math.max(
maxScore,
this.fuzzyMatch(searchTerm, cookie.domain) * 0.5
);
}

if (maxScore >= minScore) {
results.push({ cookie, score: maxScore });
}
}

return results.sort((a, b) => b.score - a.score).map(r => r.cookie);
}

/**
* Highlight matching parts in text.
* @param {string} text The text to highlight.
* @param {string} searchTerm The term to highlight.
* @return {string} HTML with highlighted matches.
*/
static highlight(text, searchTerm) {
if (!searchTerm) {
return text;
}

const regex = new RegExp(`(${this.escapeRegex(searchTerm)})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
Comment thread
levouinse marked this conversation as resolved.

/**
* Escapes special regex characters.
* @param {string} string The string to escape.
* @return {string} Escaped string.
*/
static escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
29 changes: 29 additions & 0 deletions interface/popup/cookie-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ <h1 class="container">
role="button"
aria-expanded="false"
>
<input type="checkbox" class="cookie-checkbox" aria-label="Select cookie" />
<svg class="icon arrow">
<use href="../sprites/solid.svg#angle-down"></use>
</svg>
Expand All @@ -213,6 +214,16 @@ <h1 class="container">
<div class="expando" aria-hidden="true" role="region">
<div class="wrapper">
<div class="action-btns">
<button
class="copy"
data-tooltip="Copy"
aria-label="Copy"
type="button"
>
<svg class="icon">
<use href="../sprites/solid.svg#copy"></use>
</svg>
</button>
<button
class="delete"
data-tooltip="Delete"
Expand Down Expand Up @@ -397,6 +408,24 @@ <h1 class="container">
<input id="searchField" type="text" placeholder="Search" />
</div>
</li>
<li id="bulk-actions-bar" style="display: none;">
<div class="bulk-actions-container">
<label class="bulk-select-all">
<input type="checkbox" id="selectAllCheckbox" />
<span id="bulkCounter">0 selected</span>
</label>
<div class="bulk-buttons">
<button id="bulkDelete" class="bulk-btn danger">
<svg class="icon"><use href="../sprites/solid.svg#trash"></use></svg>
Delete
</button>
<button id="bulkExport" class="bulk-btn">
<svg class="icon"><use href="../sprites/solid.svg#file-export"></use></svg>
Export
</button>
</div>
</div>
</li>
</template>

<template id="tmp-export-options">
Expand Down
146 changes: 146 additions & 0 deletions interface/popup/cookie-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
let notificationElement;
let loadedCookies = {};
let disableButtons = false;
const selectedCookies = new Set();

const notificationQueue = [];
let notificationTimeout;
Expand Down Expand Up @@ -75,6 +76,128 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
return false;
}

/**
* Handles clicks on the copy button of a cookie.
* @param {Element} e Copy button element.
* @return {false} returns false to prevent click event propagation.
*/
function copyButton(e) {
e.preventDefault();
const listElement = e.target.closest('li');
const cookieId = listElement.id;
const cookie = loadedCookies[cookieId];

if (cookie) {
const cookieJson = JSON.stringify(cookie.cookie, null, 2);
copyText(cookieJson);
sendNotification('Cookie copied to clipboard');
}
return false;
}

/**
* Handles checkbox click for bulk selection.
* @param {Event} e Click event.
*/
function handleCheckboxClick(e) {
e.stopPropagation();
const checkbox = e.target;
const listElement = checkbox.closest('li');
const cookieId = listElement.id;

if (checkbox.checked) {
selectedCookies.add(cookieId);
} else {
selectedCookies.delete(cookieId);
}

updateBulkActionsBar();
}

/**
* Updates the bulk actions bar visibility and counter.
*/
function updateBulkActionsBar() {
const bulkBar = document.getElementById('bulk-actions-bar');
const counter = document.getElementById('bulkCounter');
const selectAllCheckbox = document.getElementById('selectAllCheckbox');

if (!bulkBar) return;

const count = selectedCookies.size;

if (count > 0) {
bulkBar.style.display = 'block';
counter.textContent = `${count} selected`;

const totalCookies = Object.keys(loadedCookies).length;
selectAllCheckbox.checked = count === totalCookies;
selectAllCheckbox.indeterminate = count > 0 && count < totalCookies;
} else {
bulkBar.style.display = 'none';
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
}

/**
* Handles select all checkbox.
*/
function handleSelectAll() {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const checkboxes = cookiesListHtml.querySelectorAll('.cookie-checkbox');

if (selectAllCheckbox.checked) {
checkboxes.forEach(cb => {
cb.checked = true;
const cookieId = cb.closest('li').id;
selectedCookies.add(cookieId);
});
} else {
checkboxes.forEach(cb => {
cb.checked = false;
});
selectedCookies.clear();
}

updateBulkActionsBar();
}

/**
* Handles bulk delete action.
*/
function handleBulkDelete() {
if (selectedCookies.size === 0) return;

const count = selectedCookies.size;
for (const cookieId of selectedCookies) {
if (loadedCookies[cookieId]) {
removeCookie(loadedCookies[cookieId].cookie.name);
}
}

selectedCookies.clear();
updateBulkActionsBar();
sendNotification(`${count} cookies deleted`);
}

/**
* Handles bulk export action.
*/
function handleBulkExport() {
if (selectedCookies.size === 0) return;

const selectedCookieData = {};
for (const cookieId of selectedCookies) {
if (loadedCookies[cookieId]) {
selectedCookieData[cookieId] = loadedCookies[cookieId];
}
}

copyText(JsonFormat.format(selectedCookieData));
sendNotification(`${selectedCookies.size} cookies exported`);
}

/**
* Handles saving a cookie from a form.
* @param {element} form Form element that contains the cookie fields.
Expand Down Expand Up @@ -257,6 +380,9 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
target = target.parentNode;
}

if (target.classList.contains('cookie-checkbox')) {
return handleCheckboxClick(e);
}
if (
target.classList.contains('header') ||
target.classList.contains('header-name') ||
Expand All @@ -267,6 +393,9 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
if (target.classList.contains('delete')) {
return deleteButton(e);
}
if (target.classList.contains('copy')) {
return copyButton(e);
}
if (target.classList.contains('save')) {
return saveCookieForm(e.target.closest('li').querySelector('form'));
}
Expand Down Expand Up @@ -539,6 +668,23 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
hideNotification();
});

// Bulk actions event listeners
document.addEventListener('click', e => {
if (e.target.id === 'selectAllCheckbox') {
handleSelectAll();
} else if (
e.target.id === 'bulkDelete' ||
e.target.closest('#bulkDelete')
) {
handleBulkDelete();
} else if (
e.target.id === 'bulkExport' ||
e.target.closest('#bulkExport')
) {
handleBulkExport();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This adds a third document.addEventListener('click', ...) to the file. For better maintainability and to avoid potential conflicts or ordering issues with event handlers, it's recommended to consolidate all document-level click listeners into a single one. You could merge the logic for handling bulk actions with the existing listeners for the main menu and export menu.


adjustWidthIfSmaller();

if (chrome && chrome.runtime && chrome.runtime.getBrowserInfo) {
Expand Down
Loading