/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {computed, inject, Injectable, signal} from '@angular/core'; import {LOCAL_STORAGE} from '../providers'; import {SearchResultItem} from '../interfaces'; // Add version postfix to the key in case (if ever) the data model changes in the future. export const SEARCH_HISTORY_LS_KEY = 'docs-search-history-v1'; export const MAX_RECENT_HISTORY_SIZE = 10; // Represents V1 history item export interface HistoryItem { id: string; labelHtml: string; subLabelHtml?: string; url: string; isFavorite: boolean; createdAt: number; } function cleanUpHtml(label: string | null): string { return (label || '').replace(/<\/?mark>/g, ''); } @Injectable({providedIn: 'root'}) export class SearchHistory { private readonly localStorage = inject(LOCAL_STORAGE); private readonly history = signal>(new Map()); private readonly allItems = computed(() => Array.from(this.history().values()).sort((a, b) => b.createdAt - a.createdAt), ); readonly items = computed<{recent: HistoryItem[]; favorite: HistoryItem[]}>(() => ({ recent: this.allItems().filter((v) => !v.isFavorite), favorite: this.allItems().filter((v) => v.isFavorite), })); readonly hasItems = computed(() => this.allItems().length > 0); constructor() { this.loadHistory(); } addItem(item: SearchResultItem | HistoryItem): void { // We don't want to reset nor update the creation date of favorites if (this.history().get(item.id)?.isFavorite) { return; } this.updateHistory((map) => { map.set(item.id, { id: item.id, labelHtml: cleanUpHtml(item.labelHtml), subLabelHtml: item.subLabelHtml ? cleanUpHtml(item.subLabelHtml) : undefined, url: item.url, isFavorite: false, createdAt: Date.now(), }); // `items` still hasn't been updated so we should use `>=`. if (this.items().recent.length >= MAX_RECENT_HISTORY_SIZE) { const {id} = this.items().recent.at(-1)!; map.delete(id); } }); } removeItem(item: SearchResultItem | HistoryItem): void { this.updateHistory((map) => { map.delete(item.id); }); } makeFavorite(item: SearchResultItem | HistoryItem): void { this.updateHistory((map) => { const updated = map.get(item.id); if (updated) { map.set(item.id, { ...updated, isFavorite: true, createdAt: Date.now(), }); } }); } private loadHistory(): void { let parsedData: HistoryItem[]; try { const historyData = this.localStorage?.getItem(SEARCH_HISTORY_LS_KEY) as string | null; parsedData = JSON.parse(historyData ?? '[]') as HistoryItem[]; } catch { parsedData = []; } const history = new Map(); for (const item of parsedData) { history.set(item.id, item); } this.history.set(history); } private updateHistory(updateFn: (map: Map) => void): void { const history = new Map(this.history()); updateFn(history); this.history.set(history); try { this.localStorage?.setItem(SEARCH_HISTORY_LS_KEY, JSON.stringify(this.allItems())); } catch {} } }