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.6.2
// @run-at document-start
// @description Hover tooltip of image displaying alt attribute, original title, some accessibility-related properties, and URL info.
// @license CC0
// ==/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
* Upon "hover" set image's title attribute. Luckily tooltips delay catches augmented value.
*
* § 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>
*
*/
// 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
}
const originalTitles = new WeakMap();
let lastSetTitle = '';
const docEl = document.documentElement;
const listenerConf = { capture: true, passive: true };
docEl.addEventListener('mouseenter', altToTitle, listenerConf);
docEl.addEventListener('mouseleave', restoreTitle, listenerConf);
function altToTitle (event) {
const tgt = event.target
if (tgt.tagName && tgt.tagName == 'IMG') {
if(originalTitles.has(tgt) || (tgt.title && tgt.title === lastSetTitle)) {
// few times I got situations when mouseout was not triggered
// presumably because someting covered the image
// or whole context were temporarily replaced or covered
// or perhaps it was reconstructed from dirty snapshot
// so this should prevent exoponentially growing title
return
}
originalTitles.set(tgt, tgt.getAttribute('title'));
altPic(tgt);
}
}
function restoreTitle (event) {
const tgt = event.target;
if (originalTitles.has(tgt)) {
let ot = originalTitles.get(tgt);
if(ot === null) {
tgt.removeAttribute('title');
} else {
tgt.title = ot;
}
originalTitles.delete(tgt);
}
}
/**
* @param {HTMLImageElement} img
*/
function altPic (img) {
try {
const separator = '---';
const info = [];
const alt = img.getAttribute('alt');
let altText = alt;
const title = getClosestTitle(img);
const role = img.getAttribute('role');
const isPresentation = role === 'presentation';
if (role) {
info.push('Role: ' + role);
}
switch (alt) {
case null:
info.push(isPresentation ? `(Alt missing but not needed for this role.)` : `⚠ Alt missing`);
break;
case '':
info.push(`Alt: ""`);
break;
default:
if( alt != alt.trim() ) {
// "quote" characters are generally useful only to reveal leading/trailing whitespace
altText = `»${alt}«`;
}
if (alt == title) {
info.push(`Alt (=title): ${altText}`);
} else {
info.push(`Alt: ${altText}`);
}
}
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);
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} CSSpx${findRatio(img.width, img.height)}`;
var _width_ratio, _height_ratio;
if (img.naturalWidth && img.naturalHeight) {
// SVG have zero naturals
if (img.naturalWidth == img.width && img.naturalHeight == img.height) {
CSSsizes += ` (Natural)`;
} else {
_width_ratio = '~' + (img.width/img.naturalWidth*100).toFixed(0) + '% of ';
_height_ratio = '~' + (img.height/img.naturalHeight*100).toFixed(0) + '% of ';
if(_height_ratio == _width_ratio) {
_height_ratio = '';
}
CSSsizes += ` (${_width_ratio}${img.naturalWidth} × ${_height_ratio}${img.naturalHeight} natural px${findRatio(img.naturalWidth,img.naturalHeight)})`;
}
}
info.push('Size: ' + CSSsizes);
img.title = info.join('\n');
lastSetTitle = img.title;
} catch (e) {
// console.error('altPic ERROR', e, img);
}
}
/**
* @param {HTMLElement} el
*/
function getClosestTitle (el) {
do {
if (el.title) {
return el.title;
}
} while (el = el.parentElement);
return ''
}
/**
* @param {HTMLElement} el
*/
function getClosestEl (el, tagName) {
do {
if (el.tagName == tagName) {
return el;
}
} while (el = el.parentElement);
return false
}
function findRatio(x,y) {
var smallest = Math.min(x,y);
var n = 0;
var res = n;
while (++n <= smallest ) {
if( x % n == 0 && y % n == 0) res = n;
}
if( res == 1 ) {
return ''
}
return ' [' + x/res + ':' + y/res + ']'
}