Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info.
当前为
// ==UserScript==
// @name Image Alt to Title
// @namespace myfonj
// @include *
// @grant none
// @version 1.5.2
// @run-at document-start
// @description Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info.
// ==/UserScript==
/*
* https://greasyfork.org/en/scripts/418348/versions/new
*
* § Trivia:
* ¶ Hover tooltip displays content of nearest element's title attribute (@title).
* ¶ Alt attribute (@alt) is possible only at IMG element.
* ¶ IMG@alt is not displayed in tooltip.
* ¶ IMG cannot have children.
* ¶ @title is possible on any element, including IMG.
* ¶ IMG@src is also valuable.
*
* Goal:
* Display image alt attribute value in images hover tooltip, add valuable @SRC chunks.
*
* Details
* Pull @alt from image and set it so it is readable as @title tooltip
* so that produced title value will not obscure existing parent title
* that would be displayed otherwise. Also include image filename from @src,
* and additionally path or domain.
*
* Means
* Load (and error?) event listener constructing and setting title.
*
* Dangers
* Artificially altered alt or title after image load event will not be taken into account.
* Mitigate with mutationObserver?
*
* Process
* Draw the rest of the owl
*
*
* § Tastcases
*
* FROM:
* <a>
* <img>
* </a>
* TO:
* <a>
* <img title="Alt missing.">
* </a>
*
* FROM:
* <a>
* <img alt="">
* </a>
* TO:
* <a>
* <img alt="" title="Alt: ''">
* </a>
*
* FROM:
* <a>
* <img alt="░">
* </a>
* TO:
* <a>
* <img alt="░" title="Alt: ░">
* </a>
*
* FROM:
* <a>
* <img alt="░" title="▒">
* </a>
* TO:
* <a>
* <img title="Alt: ░, title: ▒">
* </a>
* FROM:
* <a title="▒">
* <img alt="░">
* </a>
* TO:
* <a>
* <img title="Alt: ░, title: ▒">
* </a>
*
* */
/**
* @type {WeakMap.<HTMLImageElement,{originalTitle?:string,augmentedTitle?:string}>}
*/
const processedImages = new WeakMap();
const dppx = window.devicePixelRatio;
// do not run at image-only pages
if (document.querySelector('body>img[alt="' + document.location.href + '"]:only-child')) {
// @ts-ignore (GreaseMonkey script is in fact function body)
return
}
document.documentElement.addEventListener('load', delayAltPic, true);
// document.documentElement.addEventListener('error', altPic, true);
/**
* @param {{target: EventTarget}} event
*/
function delayAltPic (event) {
const img = /**@type {HTMLImageElement}*/(event.target);
if (!img.tagName) {
return
};
if (img.tagName != 'IMG') {
return
};
setTimeout(function(){altPic(img)}, 64);
}
/**
* @param {HTMLImageElement} img
*/
function altPic (img) {
const separator = '---';
var uri;
try {
const info = [];
const alt = img.getAttribute('alt');
// using .getAttribute because .alt == '';
// console.log(alt,img);
var known = processedImages.get(img);
if (known) {
// existing img loaded new src
if (img.title == known.augmentedTitle) {
// reset to not recursively add our own title to new title
// console.log('known, resetting to original', known.originalTitle);
img.title = known.originalTitle;
} else {
// in this case "original" means "augmented by page author"
// console.log('known, title does not match', img.title);
known.originalTitle = img.title;
}
} else {
processedImages.set(img, { originalTitle: img.title });
known = processedImages.get(img);
// console.log('unknown', known);
}
const title = getClosestTitle(img);
switch (alt) {
case null:
info.push('⚠ Alt missing');
break;
case '':
info.push(`Alt: ""`);
break;
default:
if( alt != alt.trim() ) {
// "quote" characters are generally useful only to reveal leading/trailing whitespace
alt = `»${alt}«`;
}
if (alt == title) {
info.push(`Alt (=title): ${alt}`);
} else {
info.push(`Alt: ${alt}`);
}
}
if (title && alt != title) {
info.push(separator);
info.push('Title: ' + title);
}
const descby = img.getAttribute('aria-describedby');
if (descby) {
info.push(separator);
info.push('Described by `' + descby + '`: ' + (document.getElementById(descby) || { textContent: '(element not found)' }).textContent);
}
// depreated, but let's see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/longDesc
// https://www.stylemanual.gov.au/format-writing-and-structure/content-formats/images/alt-text-captions-and-titles-images
const longdesc = img.getAttribute('longdesc');
if (longdesc) {
info.push(separator);
info.push('Long Description: ' + longdesc);
}
const arialabel = img.getAttribute('aria-label');
if (arialabel) {
info.push(separator);
info.push('Label (ARIA): ' + arialabel);
}
// https://html5accessibility.com/stuff/2021/02/09/aria-description-by-public-demand-and-to-thunderous-applause/
const histeve = img.getAttribute('aria-description');
if (histeve) {
info.push(separator);
info.push('Description: ' + histeve);
}
var fig = getClosestEl(img, 'FIGURE');
if( fig ){
let capt = fig.querySelector('figcaption');
if( capt) {
info.push(separator);
info.push('Figcaption: ' + capt.textContent.trim());
}
}
info.push(separator);
const srcURI = new URL(img.currentSrc || img.src, img.baseURI);
uri = srcURI;
const slugRx = /[^/]+$/;
switch (srcURI.protocol) {
case 'http:':
case 'https:': {
if (srcURI.search) {
info.push('Params: ' + srcURI.search);
}
info.push('File: ' + srcURI.pathname.match(slugRx));
let path = srcURI.pathname.replace(slugRx, '');
if(path && path != '/') {
info.push('Path: ' + srcURI.pathname.replace(slugRx, ''));
}
if (document.location.hostname != srcURI.hostname || window != window.top) {
info.push('Host: ' + srcURI.hostname);
}
break;
}
case 'data:': {
let durichunks = srcURI.href.split(',');
info.push(durichunks[0] + ', + ' + durichunks[1].length + ' b.');
break;
}
default:
info.push('Src: ' + srcURI.href);
}
var CSSsizes = `${img.width}×${img.height}px`;
// var physicalSizes = `${img.width*dppx}×${img.height*dppx}px`;
// ↑ hm, this is not working. how to get dimensions in sampling point counts when dppx != 1 / zoomed page?
if (img.naturalWidth && img.naturalHeight) {
// SVG have zero naturals
if (img.naturalWidth == img.width && img.naturalHeight == img.height) {
CSSsizes += ` (Natural)`;
} else {
CSSsizes += ` (Natural: ${img.naturalWidth}×${img.naturalHeight}px)`;
// physicalSizes += ` (Natural: ${img.naturalWidth*dppx}×${img.naturalHeight*dppx}px)`;
}
}
info.push('CSS Size: ' + CSSsizes);
// if (dppx != 1) {
// info.push('Physical: ' + physicalSizes);
// }
img.title = known.augmentedTitle = info.join('\n');
} catch (e) {
// console.error(e, uri, img);
}
}
/**
* @param {HTMLElement} el
*/
function getClosestTitle (el) {
do {
if (el.title) {
return el.title;
}
} while ((el = el.parentElement) && el !== document.documentElement);
return ''
}
/**
* @param {HTMLElement} el
*/
function getClosestEl (el, tagName) {
do {
if (el.tagName == tagName) {
return el;
}
} while ((el = el.parentElement) && el !== document.documentElement);
return false
}