ブックマーカー図鑑ツールチップ
by
Lhankor_Mhy
08/10 [2026/08/10 17:38:17]
はてブで https://bookmarker-encyclopedia.netlify.app のキャッチフレーズをツールチップで表示します。アイコンの上で2秒ぐらいホバーしてください。
@@ -2,7 +2,7 @@
// @name ブックマーカー図鑑ツールチップ
// @title ブックマーカー図鑑ツールチップ
// @namespace https://let.hatelabo.jp/Lhankor_Mhy/let/laGdx-DigsAA
-// @version 0.2
+// @version 0.3
// @description はてブで https://bookmarker-encyclopedia.netlify.app のキャッチフレーズをツールチップで表示します。アイコンの上で2秒ぐらいホバーしてください。
// @author Lhankor_Mhy
// @match https://b.hatena.ne.jp/entry/*
@@ -12,13 +12,9 @@
// @grant none
// ==/UserScript==
-
-
-// The quota has been exceeded.エラーを確認。あとで対応する
-
(async function () {
'use strict';
- const ID = "bookmarker-encyclopedia-tooltip";
+ const ID = "bookmarkerEncyclopediaTooltip";
// polyfill for interestfor
const supported = Object.hasOwn(
@@ -29,10 +25,15 @@
await import('https://cdn.jsdelivr.net/npm/interestfor@1.0.8/src/interestfor.min.js');
}
+ const { default: localforage } = await import('https://cdn.jsdelivr.net/npm/localforage@1.10.0/+esm');
+ const store = localforage.createInstance({
+ name: ID,
+ storeName: ID,
+ });
+
// insert tooltip elements and styles
- // document.querySelectorAll('a:has(img[src*="profile.png"])')
document.body.insertAdjacentHTML('beforeend', `
- <div id="${ID}" popover>...</div>
+ <div id="${ID}" popover interestfor="ideologyTooltip">...</div>
<style>
#${ID} {
--ink: #1c1a17;
@@ -84,6 +85,19 @@
}
}
}
+ #ideologyTooltip{
+ font-size: 0.6rem;
+ color: var(--ink);
+ max-width: 700px;
+ margin: 0;
+ background: var(--card);
+ border: 1px solid var(--line);
+ border-radius: 7px;
+
+ width: fit-content;
+ position-area: bottom center;
+ align-self: start;
+ }
[interestfor="${ID}"] {
interest-delay: 1.5s .1s;
--interest-delay: 1.5s .1s;
@@ -95,6 +109,10 @@
}
}
}
+ [interestfor="ideologyTooltip"] {
+ interest-delay: 3s .1s;
+ --interest-delay: 3s .1s;
+ }
</style>
`);
const tooltip = document.getElementById(ID);
@@ -111,27 +129,37 @@
const icon = e.source;
const userId = icon.getAttribute('href').split('/')[1].toLowerCase();
- let profileJSON = localStorage.getItem(`bookmarker-encyclopedia_profile_${userId}`);
+ let profile = await store.getItem(`${userId}`);
async function callAPI() {
const response = await fetch(`https://bookmarker-encyclopedia.netlify.app/api/profiles/${userId}.json`);
- let profile = response.ok ? JSON.parse(await response.text()) : null;
- profile = { profile, expired: new Date().getTime() + 1000 * 60 * 60 * 24 * 7 }; // expire in 7 days
- const updatedProfileJSON = JSON.stringify(profile);
- localStorage.setItem(`bookmarker-encyclopedia_profile_${userId}`, updatedProfileJSON);
- return updatedProfileJSON;
+ const profile = {
+ profile: response.ok ? JSON.parse(await response.text()) : null,
+ expired: new Date().getTime() + 1000 * 60 * 60 * 24 * 7
+ }; // expire in 7 days
+ await store.setItem(`${userId}`, profile);
+ return profile;
}
- if (!profileJSON || !(new Date().getTime() < JSON.parse(profileJSON)?.expired)) {
- profileJSON = await callAPI();
+ if (!profile || !(new Date().getTime() < profile?.expired)) {
+ profile = await callAPI();
}
- const { generated: { catchphrase } } = JSON.parse(profileJSON).profile;
+ if (!profile?.profile) {
+ tooltip.textContent = "プロフィールが見つかりませんでした。";
+ return;
+ }
+ const { profile: { generated: { catchphrase, ideologyProse } } } = profile;
tooltip.textContent = catchphrase;
const clonedIcon = icon.cloneNode(true);
clonedIcon.setAttribute('interestfor', null);
clonedIcon.setAttribute('href', `https://bookmarker-encyclopedia.netlify.app/profiles/${userId}`);
- tooltip.insertBefore(clonedIcon, tooltip.firstChild);
+ const ideologyTooltip = document.createElement('div');
+ ideologyTooltip.setAttribute('id', `ideologyTooltip`);
+ ideologyTooltip.textContent = ideologyProse;
+ ideologyTooltip.setAttribute('popover', '');
+ tooltip.insertAdjacentElement('beforeend', ideologyTooltip);
+ tooltip.insertAdjacentElement('afterbegin', clonedIcon);
});
tooltip.addEventListener("loseinterest", (e) => {
tooltip.textContent = "...";
// ==UserScript==
// @name ブックマーカー図鑑ツールチップ
// @title ブックマーカー図鑑ツールチップ
// @namespace https://let.hatelabo.jp/Lhankor_Mhy/let/laGdx-DigsAA
// @version 0.3
// @description はてブで https://bookmarker-encyclopedia.netlify.app のキャッチフレーズをツールチップで表示します。アイコンの上で2秒ぐらいホバーしてください。
// @author Lhankor_Mhy
// @match https://b.hatena.ne.jp/entry/*
// @icon https://b.hatena.ne.jp/favicon.ico
// @license CC0
// @noframes
// @grant none
// ==/UserScript==
(async function () {
'use strict';
const ID = "bookmarkerEncyclopediaTooltip";
// polyfill for interestfor
const supported = Object.hasOwn(
HTMLButtonElement.prototype,
"interestForElement",
);
if (!supported) {
await import('https://cdn.jsdelivr.net/npm/interestfor@1.0.8/src/interestfor.min.js');
}
const { default: localforage } = await import('https://cdn.jsdelivr.net/npm/localforage@1.10.0/+esm');
const store = localforage.createInstance({
name: ID,
storeName: ID,
});
// insert tooltip elements and styles
document.body.insertAdjacentHTML('beforeend', `
<div id="${ID}" popover interestfor="ideologyTooltip">...</div>
<style>
#${ID} {
--ink: #1c1a17;
--paper: #f5f0e6;
--card: #fffdf7;
--gold: #b8893a;
--gold-soft: #e8d9b5;
--accent: #2f6f6b;
--danger: #b14a3b;
--muted: #8a8071;
--line: #d8cdb4;
font-size: 0.8rem;
font-weight: 700;
color: var(--gold);
max-width: 700px;
margin: -7px;
padding: 0px;
background: var(--card);
border: 3px solid var(--ink);
border-width: 0px;
border-radius: 7px;
box-shadow: 0px 0px 0 rgba(28, 26, 23, .18);
width: fit-content;
height: 0px;
overflow: hidden;
position-area: center span-right;
align-self: start;
transition: all .3s .5s allow-discrete, box-shadow .3s 1.1s allow-discrete, translate .3s 1.1s allow-discrete;
interpolate-size: allow-keywords;
translate: 10px 12px;
&:popover-open {
box-shadow: 10px 12px 0 rgba(28, 26, 23, .18);
translate: 0px 0px;
height: auto;
padding: 3.5px;
border-width: 3px;
@starting-style {
height: 0px;
box-shadow: 0px 0px 0 rgba(28, 26, 23, .18);
translate: 10px 12px;
// border-radius: 0px;
padding: 0px;
border-width: 0px;
}
}
}
#ideologyTooltip{
font-size: 0.6rem;
color: var(--ink);
max-width: 700px;
margin: 0;
background: var(--card);
border: 1px solid var(--line);
border-radius: 7px;
width: fit-content;
position-area: bottom center;
align-self: start;
}
[interestfor="${ID}"] {
interest-delay: 1.5s .1s;
--interest-delay: 1.5s .1s;
img{
transition: rotate .3s .5s,scale .3s .5s;
&:hover{
rotate: -10deg;
scale: 1.2;
}
}
}
[interestfor="ideologyTooltip"] {
interest-delay: 3s .1s;
--interest-delay: 3s .1s;
}
</style>
`);
const tooltip = document.getElementById(ID);
function init() {
// add interest invokers
document.querySelectorAll('.entry-user-icon').forEach(icon => {
icon.setAttribute('interestfor', tooltip.id);
})
}
// add interest event listener
tooltip.addEventListener("interest", async (e) => {
const icon = e.source;
const userId = icon.getAttribute('href').split('/')[1].toLowerCase();
let profile = await store.getItem(`${userId}`);
async function callAPI() {
const response = await fetch(`https://bookmarker-encyclopedia.netlify.app/api/profiles/${userId}.json`);
const profile = {
profile: response.ok ? JSON.parse(await response.text()) : null,
expired: new Date().getTime() + 1000 * 60 * 60 * 24 * 7
}; // expire in 7 days
await store.setItem(`${userId}`, profile);
return profile;
}
if (!profile || !(new Date().getTime() < profile?.expired)) {
profile = await callAPI();
}
if (!profile?.profile) {
tooltip.textContent = "プロフィールが見つかりませんでした。";
return;
}
const { profile: { generated: { catchphrase, ideologyProse } } } = profile;
tooltip.textContent = catchphrase;
const clonedIcon = icon.cloneNode(true);
clonedIcon.setAttribute('interestfor', null);
clonedIcon.setAttribute('href', `https://bookmarker-encyclopedia.netlify.app/profiles/${userId}`);
const ideologyTooltip = document.createElement('div');
ideologyTooltip.setAttribute('id', `ideologyTooltip`);
ideologyTooltip.textContent = ideologyProse;
ideologyTooltip.setAttribute('popover', '');
tooltip.insertAdjacentElement('beforeend', ideologyTooltip);
tooltip.insertAdjacentElement('afterbegin', clonedIcon);
});
tooltip.addEventListener("loseinterest", (e) => {
tooltip.textContent = "...";
});
// add mutation observer for lazy-loaded bookmarks
const targetNode = document.querySelector('.js-bookmarks-recent');
const mutationConfig = { childList: true };
const mutationCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
init();
}
}
};
const mutationObserver = new MutationObserver(mutationCallback);
mutationObserver.observe(targetNode, mutationConfig);
init();
})();
- Permalink
- このページへの個別リンクです。
- RAW
- 書かれたコードへの直接のリンクです。
- Packed
- 文字列が圧縮された書かれたコードへのリンクです。
- Userscript
- Greasemonkey 等で利用する場合の .user.js へのリンクです。
- Loader
- @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
- Metadata
- コード中にコメントで @xxx と書かれたメタデータの JSON です。