Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
121 changes: 121 additions & 0 deletions interface/lib/cookieSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* 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 safeText = text
.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const regex = new RegExp(`(${this.escapeRegex(searchTerm)})`, 'gi');
return safeText.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
173 changes: 158 additions & 15 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 @@ -476,32 +605,46 @@ import { CookieHandlerPopup } from './cookieHandlerPopup.js';
mainMenuContent.classList.toggle('visible');
});

// Consolidated document click handler
document.addEventListener('click', function (e) {
// Clicks in the main menu should not dismiss it.
// Handle main menu blur
if (
document.querySelector('#main-menu').contains(e.target) ||
!mainMenuContent.classList.contains('visible')
document.querySelector('#main-menu') &&
!document.querySelector('#main-menu').contains(e.target) &&
mainMenuContent.classList.contains('visible')
) {
console.log('main menu blur');
mainMenuContent.classList.remove('visible');
return;
}
console.log('main menu blur');
mainMenuContent.classList.remove('visible');
});

document.addEventListener('click', function (e) {
// Handle export menu blur
const exportMenu = document.querySelector('#export-menu');
// Clicks in the export menu should not dismiss it.
if (!exportMenu || exportMenu.contains(e.target)) {
return;
}

const exportButton = document.querySelector('#export-cookies');
if (!exportButton || exportButton.contains(e.target)) {
if (
exportMenu &&
!exportMenu.contains(e.target) &&
(!exportButton || !exportButton.contains(e.target))
) {
console.log('export menu blur');
hideExportMenu();
return;
}

console.log('export menu blur');
hideExportMenu();
// Handle bulk actions
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();
}
});

document
Expand Down
Loading