UNPKG

jattac.react.recents

Version:

A React component and utility for tracking and displaying recent browser locations and URLs.

327 lines (308 loc) 17.2 kB
'use strict'; var React = require('react'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const defaultNamespace = 'global'; class OnBrowserLocationTracker { constructor(config) { this.storageKey = 'recents'; this.titleObserver = null; this.config = config; if (!OnBrowserLocationTracker._namespace) { if (this.config.namespace) { OnBrowserLocationTracker._namespace = this.config.namespace; } else { OnBrowserLocationTracker._namespace = defaultNamespace; } } } static get namespace() { if (this._namespace) { return this._namespace; } else { return defaultNamespace; } } get fullStorageKey() { return `${OnBrowserLocationTracker.namespace}_${this.storageKey}`; } getStoredRecents() { return __awaiter(this, void 0, void 0, function* () { const data = localStorage.getItem(this.fullStorageKey); const result = (data ? JSON.parse(data) : []); const validRecents = result .filter((a) => this.isValid(a)) .sort((a, b) => new Date(b.lastVisited).getTime() - new Date(a.lastVisited).getTime()); const uniqueByDisplayLabel = [...new Map(validRecents.map((item) => [item.windowTitle, item])).values()]; return uniqueByDisplayLabel; }); } isValid(location) { return location.url && location.windowTitle ? true : false; } saveRecents(recents) { return __awaiter(this, void 0, void 0, function* () { const validRecents = recents.filter((a) => this.isValid(a)); localStorage.setItem(this.fullStorageKey, JSON.stringify(validRecents)); }); } shouldTrack(location) { var _a; if (!this.config.dontTrack) return true; // If no dontTrack rules, always track const { withPrefix, withSuffix, contains } = this.config.dontTrack; // Helper function to check a condition const matchesCondition = (value, patterns) => { if (!value || !patterns) return false; return patterns.some((pattern) => { const { urlOrTitle: title, url } = pattern; return (title && value.startsWith(title)) || (url && value.startsWith(url)); }); }; const matchables = [withPrefix, withSuffix, contains]; for (let i = 0; i < matchables.length; i++) { const specificMatchable = (_a = matchables[i]) !== null && _a !== void 0 ? _a : []; const foundMatch = matchesCondition(location.url, specificMatchable) || matchesCondition(location.windowTitle, specificMatchable); if (foundMatch) { return false; } } return true; } trackVisit(location) { return __awaiter(this, void 0, void 0, function* () { location.url = this.processUrlForTracking(location.url); if (!this.shouldTrack(location)) return; // Skip tracking if location matches dontTrack rules const recents = yield this.getStoredRecents(); // Remove any existing entry for the same URL const updatedRecents = recents.filter((r) => r.url !== location.url); // Add the new location to the top updatedRecents.unshift(location); // Enforce the limit if (updatedRecents.length > this.config.maxRecents) { updatedRecents.pop(); } yield this.saveRecents(updatedRecents); }); } getRecentsAsync() { return __awaiter(this, void 0, void 0, function* () { return yield this.getStoredRecents(); }); } removeRecent(url) { return __awaiter(this, void 0, void 0, function* () { const recents = yield this.getStoredRecents(); const updatedRecents = recents.filter((recent) => recent.url !== url); yield this.saveRecents(updatedRecents); }); } clearRecents() { return __awaiter(this, void 0, void 0, function* () { localStorage.removeItem(this.fullStorageKey); }); } getCurrentLocation() { // Ensure document is ready before accessing title if (!window.location.href) return null; // Check if the document is fully loaded before accessing <title> if (document.readyState === 'complete') { this.observeTitleChange(); } else { // If the document is not fully loaded, listen for the load event window.addEventListener('load', () => this.observeTitleChange()); } return null; // MutationObserver will handle updates } /** * Processes the URL for tracking, taking into account the disregardQueryStrings configuration. * @param url The URL to process. * @returns The processed URL. */ processUrlForTracking(url) { if (!this.config.disregardQueryStrings) return url; const urlObj = new URL(url); const path = urlObj.pathname; this.config.disregardQueryStrings.forEach(({ path: disregardPath, queryStrings }) => { if (path.startsWith(disregardPath)) { queryStrings.forEach((queryString) => { urlObj.searchParams.delete(queryString); }); } }); return urlObj.toString(); } observeTitleChange() { const titleElement = document.querySelector('title'); // Fallback if the title is not available if (!titleElement) { // If no title tag found, observe the <head> element instead const headElement = document.querySelector('head'); if (!headElement) return; if (!this.titleObserver) { this.titleObserver = new MutationObserver(() => { const title = document.title; this.updateLocation(title); }); // Observe changes in the <head> element to detect changes in the title this.titleObserver.observe(headElement, { childList: true, // Watch for changes in child elements like <title> subtree: true, // Watch all descendants, including the title tag }); } } else { // Directly update if the <title> is available const title = document.title; this.updateLocation(title); } } updateLocation(title) { return __awaiter(this, void 0, void 0, function* () { const maxTitleLength = 45; if (!title) return; // Truncate title if it exceeds max length and add ellipses if (title.length > maxTitleLength) { title = title.substring(0, maxTitleLength) + '...'; } const location = { windowTitle: title, url: window.location.href, lastVisited: new Date().toISOString(), }; // Track the visit and persist the location yield this.trackVisit(location); }); } } OnBrowserLocationTracker._namespace = ''; function styleInject(css, ref) { if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = ".Recents-module_recentsContainer__87baP {\r\n font-family: \"Arial\", sans-serif;\r\n padding: 1.5rem;\r\n border: 1px solid #e5e5e5;\r\n border-radius: 8px;\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\r\n background: #fff;\r\n max-width: 600px;\r\n margin: 0 auto;\r\n}\r\n\r\n.Recents-module_recentsTitle__t8LqW {\r\n font-size: 1.5rem;\r\n font-weight: bold;\r\n margin-bottom: 1rem;\r\n color: #333;\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n}\r\n\r\n.Recents-module_removeAllButton__30r2o {\r\n background-color: #e53935;\r\n color: #fff;\r\n border: none;\r\n padding: 0.5rem 1rem;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n font-size: 0.9rem;\r\n transition: background-color 0.2s ease;\r\n}\r\n\r\n.Recents-module_removeAllButton__30r2o:hover {\r\n background-color: #d32f2f;\r\n}\r\n\r\n.Recents-module_recentItem__AIFny {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 1rem;\r\n opacity: 0;\r\n transform: translateY(10px);\r\n animation: Recents-module_fadeIn__fxi5P 0.3s ease forwards;\r\n}\r\n\r\n.Recents-module_recentItem__AIFny:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.Recents-module_recentLink__LnBqE {\r\n text-decoration: none;\r\n color: #0073e6;\r\n font-size: 1rem;\r\n flex: 1;\r\n margin-right: 1rem;\r\n transition: color 0.2s ease;\r\n}\r\n\r\n.Recents-module_recentLink__LnBqE:hover {\r\n color: #005bb5;\r\n}\r\n\r\n.Recents-module_timeLabel__Paq1c {\r\n font-size: 0.9rem;\r\n color: #666;\r\n margin-right: 1rem;\r\n}\r\n\r\n.Recents-module_removeButton__Ugt7l {\r\n background-color: transparent;\r\n color: #e53935;\r\n border: none;\r\n font-size: 1rem;\r\n cursor: pointer;\r\n padding: 0.2rem 0.5rem;\r\n transition: color 0.2s ease;\r\n}\r\n\r\n.Recents-module_removeButton__Ugt7l:hover {\r\n color: #d32f2f;\r\n}\r\n\r\n.Recents-module_noRecents__OySGm {\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 2rem 1rem;\r\n text-align: center;\r\n}\r\n\r\n.Recents-module_noRecentsIcon__YSMTH {\r\n width: 48px;\r\n height: 48px;\r\n color: #ccc;\r\n margin-bottom: 1rem;\r\n}\r\n\r\n.Recents-module_noRecentsText__kT8tq {\r\n font-size: 1rem;\r\n color: #666;\r\n line-height: 1.5;\r\n}\r\n\r\n@keyframes Recents-module_fadeIn__fxi5P {\r\n to {\r\n opacity: 1;\r\n transform: translateY(0);\r\n }\r\n}\r\n"; var styles = {"recentsContainer":"Recents-module_recentsContainer__87baP","recentsTitle":"Recents-module_recentsTitle__t8LqW","removeAllButton":"Recents-module_removeAllButton__30r2o","recentItem":"Recents-module_recentItem__AIFny","fadeIn":"Recents-module_fadeIn__fxi5P","recentLink":"Recents-module_recentLink__LnBqE","timeLabel":"Recents-module_timeLabel__Paq1c","removeButton":"Recents-module_removeButton__Ugt7l","noRecents":"Recents-module_noRecents__OySGm","noRecentsIcon":"Recents-module_noRecentsIcon__YSMTH","noRecentsText":"Recents-module_noRecentsText__kT8tq"}; styleInject(css_248z); const formatFriendlyTime = (isoDate) => { const date = new Date(isoDate); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); const diffInMinutes = Math.floor(diffInSeconds / 60); const diffInHours = Math.floor(diffInSeconds / 3600); const diffInDays = Math.floor(diffInSeconds / 86400); const diffInWeeks = Math.floor(diffInSeconds / 604800); const diffInMonths = Math.floor(diffInSeconds / 2592000); if (diffInSeconds < 60) return `${diffInSeconds} seconds ago`; if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`; if (diffInHours < 24) return `${diffInHours} hours ago`; if (diffInDays === 1) return "Yesterday"; if (diffInDays < 7) return `${diffInDays} days ago`; if (diffInWeeks < 4) return `${diffInWeeks} weeks ago`; if (diffInMonths < 12) return `${diffInMonths} months ago`; return date.toLocaleDateString(); // Return a more standard date format if it's more than a year ago }; // NoRecents.svg - a cleaner, modern icon const NoRecents = () => (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 64 64", width: "64", height: "64", fill: "none" }, React.createElement("g", null, React.createElement("path", { fill: "#888", d: "M32 8C18.746 8 8 18.746 8 32s10.746 24 24 24 24-10.746 24-24-10.746-24-24-24zm0 44c-11.046 0-20-8.954-20-20s8.954-20 20-20 20 8.954 20 20-8.954 20-20 20z" }), React.createElement("path", { fill: "#888", d: "M32 12c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm-9 10c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm18 0c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3z" })))); const Recents = ({ onBeforeRemove, namespace }) => { const [recents, setRecents] = React.useState([]); React.useEffect(() => { const tracker = new OnBrowserLocationTracker({ maxRecents: 10, namespace: namespace }); const loadRecents = () => __awaiter(void 0, void 0, void 0, function* () { const storedRecents = yield tracker.getRecentsAsync(); setRecents(storedRecents); }); loadRecents(); }, []); const handleRemoveRecent = (recent) => { const shouldRemove = onBeforeRemove ? onBeforeRemove([recent]) : true; if (!shouldRemove) return; setRecents((prevRecents) => prevRecents.filter((item) => item.url !== recent.url)); // Update tracker storage if needed const tracker = new OnBrowserLocationTracker({ maxRecents: 10 }); tracker.removeRecent(recent.url); }; const handleRemoveAll = () => { const shouldRemoveAll = onBeforeRemove ? onBeforeRemove(recents) : true; if (!shouldRemoveAll) return; setRecents([]); // Update tracker storage if needed const tracker = new OnBrowserLocationTracker({ maxRecents: 10 }); tracker.clearRecents(); }; return (React.createElement("div", { className: styles.recentsContainer }, React.createElement("h2", { className: styles.recentsTitle }, "Recently Visited", recents.length > 0 && (React.createElement("button", { onClick: handleRemoveAll, className: styles.removeAllButton, title: "Remove all recents" }, "Clear All"))), recents.length === 0 ? (React.createElement("div", { className: styles.noRecents }, React.createElement(NoRecents, null), React.createElement("p", { className: styles.noRecentsText }, "No recent activity yet. Start browsing to see your recent history here!"))) : (recents.map((item, index) => (React.createElement("div", { key: index, className: styles.recentItem }, React.createElement("a", { href: item.url, className: styles.recentLink }, item.windowTitle), React.createElement("span", { className: styles.timeLabel }, formatFriendlyTime(item.lastVisited)), React.createElement("button", { onClick: () => handleRemoveRecent(item), className: styles.removeButton, title: "Remove this recent" }, "\u2715"))))))); }; exports.OnBrowserLocationTracker = OnBrowserLocationTracker; exports.Recents = Recents; //# sourceMappingURL=index.js.map