Skip to content

Instantly share code, notes, and snippets.

@sanjarcode
Last active November 15, 2023 19:17
Show Gist options
  • Save sanjarcode/84815d472ade218b6da33db3c80c45ce to your computer and use it in GitHub Desktop.
Save sanjarcode/84815d472ade218b6da33db3c80c45ce to your computer and use it in GitHub Desktop.
Custom JS for the browser
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
data/
id_ed25519_temp
id_ed25519_temp.pub
*.min.js

Custom JS for the browser

Setup

  1. Install 'Shortkeys (Custom Keyboard Shortcuts)' Chrome extension
  2. Install 'Custom JavaScript for Websites 2'

Flow (shortcuts)

  • I toggle features using Shortcut keys, where I run some JS to store stuff to localStorage.
  • Inspect localStorage keys and run actual JS for the task

Minification

Minify the .js you're interested in before pasting into CJS2 extension.

  • Need npm package uglifyjs, install by running npm install uglify-js -g
  • To minify, run
    uglifyjs someFile.js ## prints output
    uglifyjs someFile.js -o nameYouWant.js ## creates a file with output

Usage

Minify the file and copy-paste it into the extension, press Save. And reload.

/*====
See https://gist.github.com/sanjarcode/84815d472ade218b6da33db3c80c45ce#file-selectivefetchintercept-js
Usage:
Step 1: add code in `interceptIf` function
Step 2: add code in `preRequestAction` or `postResponseAction` or both
Step 3: Copy and paste the file into the browser console, and press Enter.
Done!
====*/
/**
* @param {String} url
* @param {Object} options
* All args from request creator are available
*
* @returns {Boolean} want to intercept?
*/
function interceptIf(...allArgs) {
const [url, options] = allArgs;
return url.startsWith("https://chat.openai.com/backend-api/conversation/");
}
/**
* Run some code before the call is made.
* Make sure the code is such that it wouldn't affect the request creator.
*
* @param {String} url
* @param {Object} options
* All args from request creator are available
* Have made it async just in case custom code needs to make an API call.
* Be careful as it might trigger timeout from main app
*
* @returns {void}
*/
async function preRequestAction(...allArgs) {
const [url, options] = allArgs;
console.log(`Intercepted API call: ${url}`);
}
/**
* Run some code after the call is made. Doesn't block handover process. Your code runs in a separate flow.
* Make sure the code is such that it wouldn't affect the request creator, or the original app flow.
*
* @param {Response} response
* @param {String} url
* @param {Object} options
* Response, all args from request creator are available
* Have made it async just in case custom code needs to make an API call.
* Be careful as it might trigger timeout from main app
*
* @returns {void}
*/
async function postResponseAction(response, reqAllArgs) {
const [url, options] = reqAllArgs;
const data = await response.json();
// console.log("API Response:", data);
console.log("Post intercept");
/**
* [{
* id: '',
* message: { create_time: 1699187911.883348 },
* parent: '',
* children: ['']
* }]
*/
const k = {
MAPPING: "mapping",
MESSAGE: "message",
CREATE_TIME: "create_time",
NODE_ATTRIB: "data-message-id",
};
const chatBubbles = Object.values(data?.[k.MAPPING]);
const bubbleIdsAndTime = chatBubbles
.map((bubble) => ({
id: bubble.id,
time: 1000 * bubble?.[k.MESSAGE]?.[k.CREATE_TIME],
// 1699187911.883348, size 10
// need size 13. Example: Date.now() - 1699193623484
}))
.filter((bubble) => bubble.time); // get bubbles that have the time path
console.log({ bubbleIdsAndTime });
bubbleIdsAndTime.forEach(({ id, time }) => {
// print out the bubble id and time
// console.log(`${id}: ${new Date(time).toLocaleTimeString()}`)
// add timestamp to the bubble
// at the beginning
// if bubble is long, add at the end of the bubble too
// `*[data-message-id="aaa28ddf-1e9a-4f1e-887d-ff9c834b1e72"]`
const bubbleUINode = document.querySelector(`*[${k.NODE_ATTRIB}="${id}"]`);
if (!bubbleUINode) {
console.log(`Bubble node ${id} not found, skipping`);
return;
}
const timeNodeHTML = `<div style="color: orange; text-align: right;">${getHumanRelevantDateTime(
time
)}</div>`;
const preTimeNodeHTML = timeNodeHTML;
const postTimeNodeHTML =
bubbleUINode.clientHeight >= document.body.clientHeight / 2
? timeNodeHTML
: "";
bubbleUINode.innerHTML =
preTimeNodeHTML + bubbleUINode.innerHTML + postTimeNodeHTML;
});
}
/**
* Create a proxy for fetch to intercept API calls
*/
const originalFetch = window.fetch;
/**
* modified fetch
* The intent is to make run some custom code, but avoid intefering with the main app code too.
* The main app shouldn't be able to detect if this is modified or not.
* Avoid too expensive ops. For expensive ops, push data to own external data structure and run there.
*
*/
const modifiedFetch = async (...allArgs) => {
const [url, options] = allArgs;
const shouldIntercept = interceptIf(url, options);
if (!shouldIntercept) return originalFetch(...allArgs); // not interested in this API call, let it run
preRequestAction();
let response = null;
try {
response = await originalFetch(...allArgs);
// post-intercept code
// clone the response object coz it behaves impurely in browser JS, fine.
// run custom code on cloned response
const expendableResponse = response.clone();
postResponseAction(expendableResponse, ...allArgs); // no await, to avoid blocking
} catch (error) {
console.error("Error fetching the API:", error);
} finally {
return response; // the app gets response as expects
}
};
window.fetch = modifiedFetch;
// utils
/**
* Shows relevant human date - time, `time, yesterday`, `time, date`, `time, date, year`
*
* @param {Date | Number | String } date - The date object to be formatted.
* @returns {string} - Formatted date and time string according to the defined rules.
*
* // Example usage:
const myDate = new Date('2023-11-04T15:30:00Z'); // Replace this with your Date object
const formattedDateTime = formatDateTime(myDate);
console.log(formattedDateTime);
*/
function getHumanRelevantDateTime(date) {
if (!(date instanceof Date)) date = new Date(date);
const now = new Date(); // Get current date and time
const options = {
hour: "numeric",
minute: "numeric",
hour12: true,
};
// Check if the date is from yesterday
if (date.toDateString() === new Date(now - 86400000).toDateString()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, Yesterday`;
}
if (date.toDateString() === now.toDateString()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return timeWithoutSeconds;
}
const day = date.getDate();
const month = date.toLocaleString("en-US", { month: "short" });
const year = date.getFullYear();
if (date.getFullYear() === now.getFullYear()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, ${day} ${month}`;
} else {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, ${day} ${month}, ${year}`;
}
}
document.querySelectorAll('span.Label--warning[title^="Label"]').forEach(pill => pill?.click())
{
"name": "custom-js-84815d472ade218b6da33db3c80c45ce",
"version": "1.0.0",
"description": "1. Install 'Shortkeys (Custom Keyboard Shortcuts)' Chrome extension 2. Install 'Custom JavaScript for Websites 2'",
"main": "collapse-PR-outdated.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"gen": "uglifyjs udemy-cjs.js -o udemy-cjs.min.js"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gist.github.com/84815d472ade218b6da33db3c80c45ce.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://gist.github.com/84815d472ade218b6da33db3c80c45ce"
},
"homepage": "https://gist.github.com/84815d472ade218b6da33db3c80c45ce",
"dependencies": {
"uglify-js": "^3.17.4"
}
}
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
uglify-js:
specifier: ^3.17.4
version: 3.17.4
packages:
/uglify-js@3.17.4:
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
engines: {node: '>=0.8.0'}
hasBin: true
dev: false
/*====
Usage:
Step 1: add code in `interceptIf` function
Step 2: add code in `preRequestAction` or `postResponseAction` or both
Step 3: Copy and paste the file into the browser console, and press Enter.
Done!
====*/
/**
* @param {String} url
* @param {Object} options
* All args from request creator are available
*
* @returns {Boolean} want to intercept?
*/
function interceptIf(...allArgs) {
const [url, options] = allArgs;
return url.startsWith("https://chat.openai.com/backend-api/conversation/");
}
/**
* Run some code before the call is made.
* Make sure the code is such that it wouldn't affect the request creator.
*
* @param {String} url
* @param {Object} options
* All args from request creator are available
* Have made it async just in case custom code needs to make an API call.
* Be careful as it might trigger timeout from main app
*
* @returns {void}
*/
async function preRequestAction(...allArgs) {
const [url, options] = allArgs;
console.log(`Intercepted API call: ${url}`);
}
/**
* Run some code after the call is made. Doesn't block handover process. Your code runs in a separate flow.
* Make sure the code is such that it wouldn't affect the request creator, or the original app flow.
*
* @param {Response} response
* @param {String} url
* @param {Object} options
* Response, all args from request creator are available
* Have made it async just in case custom code needs to make an API call.
* Be careful as it might trigger timeout from main app
*
* @returns {void}
*/
async function postResponseAction(response, reqAllArgs) {
const [url, options] = reqAllArgs;
const data = await response.json();
console.log("API Response:", data);
}
/**
* Create a proxy for fetch to intercept API calls
*/
const originalFetch = window.fetch;
/**
* modified fetch
* The intent is to make run some custom code, but avoid intefering with the main app code too.
* The main app shouldn't be able to detect if this is modified or not.
* Avoid too expensive ops. For expensive ops, push data to own external data structure and run there.
*
*/
const modifiedFetch = async (...allArgs) => {
const [url, options] = allArgs;
const shouldIntercept = interceptIf(url, options);
if (!shouldIntercept) return originalFetch(...allArgs); // not interested in this API call, let it run
preRequestAction();
let response = null;
try {
response = await originalFetch(...allArgs);
// post-intercept code
// clone the response object coz it behaves impurely in browser JS, fine.
// run custom code on cloned response
const expendableResponse = response.clone();
postResponseAction(expendableResponse, ...allArgs); // no await, to avoid blocking
} catch (error) {
console.error("Error fetching the API:", error);
} finally {
return response; // the app gets response as expects
}
};
window.fetch = modifiedFetch;
[
{
"key": "ctrl+b",
"label": "Toggle sidebar Udemy",
"action": "javascript",
"sites": "/^https:\\/\\/[a-zA-Z0-9-]+\\.udemy\\.com\\/course\\/.*\\/learn\\/lecture\\/.*$/",
"sitesArray": [
"/^https:\\/\\/[a-zA-Z0-9-]+\\.udemy\\.com\\/course\\/.*\\/learn\\/lecture\\/.*$/"
],
"blacklist": "whitelist",
"activeInInputs": false,
"code": "try {\n theatreModeButton = document.querySelector(\n '*[data-purpose=\"theatre-mode-toggle-button\"]'\n );\n theatreModeButton?.click();\nconsole.log('Udemy sidebar toggle successful');\n} catch (error) {\n console.log(\"Sidebar toggle shortcut error\");\n console.log(error);\n}"
},
{
"key": "ctrl+shift+s",
"label": "Tutorial mode toggle (YouTube, Udemy)",
"action": "javascript",
"activeInInputs": false,
"blacklist": "whitelist",
"sites": "/^https:\\/\\/www\\.youtube\\.com\\/.*$/\n/^https:\\/\\/[a-zA-Z0-9-]+\\.udemy\\.com\\/course\\/.*\\/learn\\/lecture\\/.*$/",
"code": "console.log(\"Tutorial mode toggled\", location.host);\nfunction toggleTutorialMode() {\n const TUTORIAL_MODE_TOGGLE_KEY =\n \"tutorial-mode-981c7663c0b76cf2c87c61fcf7ffaaa1\";\n\n const currentValue = JSON.parse(\n localStorage.getItem(TUTORIAL_MODE_TOGGLE_KEY) || \"false\"\n );\n\n localStorage.setItem(TUTORIAL_MODE_TOGGLE_KEY, JSON.stringify(!currentValue));\n}\ntoggleTutorialMode();\n",
"sitesArray": [
"/^https:\\/\\/www\\.youtube\\.com\\/.*$/",
"/^https:\\/\\/[a-zA-Z0-9-]+\\.udemy\\.com\\/course\\/.*\\/learn\\/lecture\\/.*$/"
]
}
]
/**
* Show notification (non-blocking) in the browser
*
* @param {String} message
* @param {Number} timeout notification hides after this, if negative the popup stays
*
*/
function showNotification(message = "", timeout = 1500) {
// create the nofication UI node
const notification = document.createElement("div");
notification.id = "sanjar";
notification.style.position = "fixed";
notification.style.top = "10px";
notification.style.right = "10px";
notification.style.padding = "10px";
notification.style.background = "rgba(0, 0, 0, 0.8)";
notification.style.color = "white";
notification.style.borderRadius = "5px";
notification.style.zIndex = "9999";
notification.style.fontSize = "18px"; // Increase font size
notification.textContent = message;
notification.style.border = "3px solid skyblue";
// attach notification node to page
document.body.appendChild(notification);
console.log("Notification attached successfully", { message, timeout });
// remove after timeout
// but let it be if timeout is negative
if (timeout < 0) return;
setTimeout(() => {
try {
document.body.removeChild(notification);
console.log("Notification removed successfully", { message, timeout });
} catch (error) {
console.error("Error removing notification:", {
error,
message,
timeout,
});
}
}, timeout);
}
// meant for the CJS2 Chrome extension. Usually added for udemy.com and youtube.com
// minify before pasting to extension
// uglifyjs myPastedFile.js -o myMinifiedFile.js
const CSJ_UTILS = {
// keyboard handler
controlPlusSomeKeyOnSubmit(givenKey, actionCallback) {
// ctrl + some key
document.addEventListener("keydown", function (event) {
if ((event.ctrlKey || event.metaKey) && event.key === givenKey) {
actionCallback(event);
}
});
},
controlPlusShiftPlusSomeKeyOnSubmit(givenKey, actionCallback) {
// ctrl + shift + some key
document.addEventListener("keydown", function (event) {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === givenKey
) {
actionCallback(event);
}
});
},
/**
* Show notification (non-blocking) in the browser
*
* @param {String} message
* @param {Number} timeout notification hides after this, if negative the popup stays
*
*/
showNotification(message = "", timeout = 1500) {
// create the nofication UI node
const notification = document.createElement("div");
notification.id = "sanjar";
notification.style.position = "fixed";
notification.style.top = "10px";
notification.style.right = "10px";
notification.style.padding = "10px";
notification.style.background = "rgba(0, 0, 0, 0.8)";
notification.style.color = "white";
notification.style.borderRadius = "5px";
notification.style.zIndex = "9999";
notification.style.fontSize = "18px"; // Increase font size
notification.textContent = message;
notification.style.border = "3px solid skyblue";
// attach notification node to page
document.body.appendChild(notification);
console.log("Notification attached successfully", { message, timeout });
// remove after timeout
// but let it be if timeout is negative
if (timeout < 0) return;
setTimeout(() => {
try {
document.body.removeChild(notification);
console.log("Notification removed successfully", { message, timeout });
} catch (error) {
console.error("Error removing notification:", {
error,
message,
timeout,
});
}
}, timeout);
},
};
const allFeatures = {
toggleUdemySideBar() {
const toggleUdemySideBarAction = () => {
try {
theatreModeButton = document.querySelector(
'*[data-purpose="theatre-mode-toggle-button"]'
);
theatreModeButton?.click();
console.log("Udemy sidebar toggle successful");
} catch (error) {
console.log("Sidebar toggle shortcut error");
console.log(error);
}
};
CSJ_UTILS.controlPlusSomeKeyOnSubmit("b", toggleUdemySideBarAction);
},
addTitleToTOTLExtension() {
// Why - need to watch Udemy in "full-screen-in-window",
// but Chrome doesn't allow hiding the titlebar and omnibox in window mode.
// Arc browser allows hiding them. But full-screen doesn't work in window mode. So used a browser extension for that.
// steps below
// Setup:
// 1. Install the 'Turn Off the Lights Extension' browser extension
// - Set it up - Open extension options, Advanced -> scroll somewhat --> enable Video toolbar checkbox
// 2. Install the 'Custom JavaScript for Websites 2' browser extension
// - Set it up - create an entry for udemy.com and paste this file. Remember to press 'Save'.
// 4. Install the arc browser - hide the side bar and make the window small.
// 3. Use it - go to udemy, and start watching a video. You should see a video toolbar on top.
// It will now have the full lecture name printed in white
// Approach - updates the video title every one second (using setTimeout). There's no other way since Udemy
// is an SPA and the video event isn't properly known
// UI element markers discovered from extension repo
// https://github.com/turnoffthelights/Turn-Off-the-Lights-Chrome-extension/
// Performance is OK - not noticeable.
function addTitleBox(text = "") {
// aria-label one has section + lecture, second has title only
let gottenTitle =
document
.querySelector('[class*="lecture-view--container"]')
?.getAttribute("aria-label") ||
document.querySelector('[class*="video-viewer--title-overlay"]')
?.textContent ||
"title not found";
let existingVideoTitle = document.querySelector("#sanjarTOTLtitle");
let panelNotFound;
const usingExisting = !!existingVideoTitle;
if (!existingVideoTitle) {
const sanjarPanels = [...document.querySelectorAll(".stefanvdvis")];
const sanjarLastPanel = sanjarPanels.at(-1);
const sanjarVideoTitle = document.createElement("span");
sanjarVideoTitle.setAttribute("id", "sanjarTOTLtitle");
sanjarVideoTitle.style.color = "white";
sanjarVideoTitle.style.fontWeight = "600";
sanjarVideoTitle.style.fontSize = "18px";
sanjarLastPanel?.appendChild(sanjarVideoTitle);
panelNotFound = !sanjarLastPanel;
existingVideoTitle = sanjarVideoTitle;
}
// alert('main ran')
existingVideoTitle.textContent = text || gottenTitle;
console.log("addTitleBox Ran", { panelNotFound, existingVideoTitle });
return {
existingVideoTitle,
id: "sanjarTOTLtitle",
existingText: existingVideoTitle.textContent,
usingExisting,
extensionFound: !!document.querySelector(".stefanvdvis"),
};
}
function updater() {
addTitleBox();
// alert('Updater Ran')
const t = setTimeout(() => {
updater();
clearTimeout(t);
}, 1000);
}
updater();
// function main() {
// document.addEventListener("DOMContentLoaded", (event) => {
// console.log("DOM fully loaded and parsed");
// updater();
// // alert('main ran')
// });
// }
// main();
},
toggleStudyPauseMode() {
/* For study-tutorial mode */
// Task code - paste in Custom JavaScript extension under tutorial site
// saves the toggle state to localStorage
// changes setting permanently
const eventListenerRemover = new AbortController();
function toggleTutorialModeSetting(localStorageKey) {
// reducer
const currentValue = JSON.parse(
localStorage.getItem(localStorageKey) || "false"
);
const newValue = !currentValue;
localStorage.setItem(localStorageKey, JSON.stringify(newValue));
if (newValue) {
windowTabSwitchWatcherSetup(localStorageKey); // add event handlers
} else {
eventListenerRemover.abort(); // remove existing event handlers
}
console.log(
`Tutorial mode is now ${newValue ? "ON" : "OFF"}, localStorage toggled`,
location.host
);
// show notification
CSJ_UTILS.showNotification(
`Tutorial mode ${newValue ? "ON" : "OFF"}`,
1000
);
}
function windowTabSwitchWatcherSetup(localStorageKey) {
// does UI change (does not know about localStorage or state)
function toggleVideo(value = null) {
const videoElements = document.querySelectorAll("video"); // contains false positives too
if (!videoElements?.length) throw new Error("No video elements found");
const mainVideoElement = Array.from(videoElements).find(
(videoElement) => videoElement.src.includes("blob:https")
); // works for Udemy, YouTube
if (!mainVideoElement) throw new Error("No main video found");
switch (value) {
case "play":
mainVideoElement.play();
break;
case "pause":
mainVideoElement.pause();
break;
default:
mainVideoElement.paused
? mainVideoElement.play()
: mainVideoElement.pause();
break;
}
console.log("Video state toggled");
}
window.addEventListener(
"focus",
function () {
// document.title = "focused";
console.log("Tab focus toggled: now focused");
if (JSON.parse(localStorage.getItem(localStorageKey)))
toggleVideo("play");
},
{ signal: eventListenerRemover.signal }
);
window.addEventListener(
"blur",
function () {
console.log("Tab focus toggled: unfocused");
// document.title = "not focused";
if (JSON.parse(localStorage.getItem(localStorageKey)))
toggleVideo("pause");
},
{ signal: eventListenerRemover.signal }
);
console.log("Tab focus watcher setup done.");
}
// 1. Decide the localStorage key
const TUTORIAL_MODE_TOGGLE_KEY = "tutorial-mode-sanjar";
// 2. watcher setup (for on page load, since I won't enable feature every time I visit the page)
const currentValue = JSON.parse(
localStorage.getItem(TUTORIAL_MODE_TOGGLE_KEY) || "false"
);
if (currentValue) windowTabSwitchWatcherSetup(TUTORIAL_MODE_TOGGLE_KEY);
// 3. preferences change from keyboard shortcut - done
CSJ_UTILS.controlPlusShiftPlusSomeKeyOnSubmit("u", () =>
toggleTutorialModeSetting(TUTORIAL_MODE_TOGGLE_KEY)
);
},
};
// run on page load
// mostly setups
const featuresToRun = [
allFeatures.toggleUdemySideBar,
allFeatures.addTitleToTOTLExtension,
allFeatures.toggleStudyPauseMode,
];
featuresToRun.forEach((f) => f());
const CSJ_UTILS = {
// keyboard handler
controlPlusSomeKeyOnSubmit(givenKey, actionCallback) {
// ctrl + some key
document.addEventListener("keydown", function (event) {
if ((event.ctrlKey || event.metaKey) && event.key === givenKey) {
actionCallback(event);
}
});
},
controlPlusShiftPlusSomeKeyOnSubmit(givenKey, actionCallback) {
// ctrl + shift + some key
document.addEventListener("keydown", function (event) {
if (
(event.ctrlKey || event.metaKey) &&
event.shiftKey &&
event.key === givenKey
) {
actionCallback(event);
}
});
},
/**
* Show notification (non-blocking) in the browser
*
* @param {String} message
* @param {Number} timeout notification hides after this, if negative the popup stays
*
*/
showNotification(message = "", timeout = 1500) {
// create the nofication UI node
const notification = document.createElement("div");
notification.id = "sanjar";
notification.style.position = "fixed";
notification.style.top = "10px";
notification.style.right = "10px";
notification.style.padding = "10px";
notification.style.background = "rgba(0, 0, 0, 0.8)";
notification.style.color = "white";
notification.style.borderRadius = "5px";
notification.style.zIndex = "9999";
notification.style.fontSize = "18px"; // Increase font size
notification.textContent = message;
notification.style.border = "3px solid skyblue";
// attach notification node to page
document.body.appendChild(notification);
console.log("Notification attached successfully", { message, timeout });
// remove after timeout
// but let it be if timeout is negative
if (timeout < 0) return;
setTimeout(() => {
try {
document.body.removeChild(notification);
console.log("Notification removed successfully", { message, timeout });
} catch (error) {
console.error("Error removing notification:", {
error,
message,
timeout,
});
}
}, timeout);
},
/**
* Shows relevant human date - time, `time, yesterday`, `time, date`, `time, date, year`
*
* @param {Date | Number | String } date - The date object to be formatted.
* @returns {string} - Formatted date and time string according to the defined rules.
*
* // Example usage:
const myDate = new Date('2023-11-04T15:30:00Z'); // Replace this with your Date object
const formattedDateTime = getHumanRelevantDateTime(myDate);
console.log(formattedDateTime);
*/
getHumanRelevantDateTime(date) {
if (!(date instanceof Date)) date = new Date(date);
const now = new Date(); // Get current date and time
const options = {
hour: "numeric",
minute: "numeric",
hour12: true,
};
// Check if the date is from yesterday
if (date.toDateString() === new Date(now - 86400000).toDateString()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, Yesterday`;
}
if (date.toDateString() === now.toDateString()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return timeWithoutSeconds;
}
const day = date.getDate();
const month = date.toLocaleString("en-US", { month: "short" });
const year = date.getFullYear();
if (date.getFullYear() === now.getFullYear()) {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, ${day} ${month}`;
} else {
const timeWithoutSeconds = date.toLocaleString("en-US", options);
return `${timeWithoutSeconds}, ${day} ${month}, ${year}`;
}
},
};
@sanjarcode
Copy link
Author

sanjarcode commented Aug 15, 2023

works, but hard testing left. also, move this to some better place (Gist isn't enough), since CJS2 needs minified files (plain file hits the character limit).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment