Guest User

Untitled

a guest
Oct 28th, 2026
28
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 4.53 KB | None | 0 0
  1. // ==UserScript==
  2. // @name ejchan
  3. // @namespace ejchan-popup-anchor
  4. // @version 1.0
  5. // @description Всплывающий пост (.pv) появляется под ссылкой, из которой открыт, и прокручивается вместе со страницей
  6. // @match *://*/*
  7. // @run-at document-idle
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. // ---- настройки ----
  15. const CENTER_CAP = 40; // на сколько px правее левого края ссылки начинается пост (максимум; обычно середина ссылки)
  16. const OFFSET_Y = 0; // зазор между нижним краем ссылки и верхом поста
  17. const MARGIN = 8; // минимальный отступ от краёв окна
  18. const SELECTOR_LINK = 'a, .post__ref';
  19.  
  20. let lastLink = null; // последняя ссылка, на которую наводили курсор
  21. const anchors = new WeakMap(); // всплывающий пост -> ссылка-источник
  22.  
  23. document.addEventListener('mouseover', (e) => {
  24. const t = e.target;
  25. const a = t && t.closest ? t.closest(SELECTOR_LINK) : null;
  26. if (a) lastLink = a;
  27. }, true);
  28.  
  29. // Где в координатах окна находится точка (0,0) для absolute-позиционирования этого элемента
  30. function origin(pv) {
  31. const op = pv.offsetParent;
  32. if (
  33. op &&
  34. op !== document.documentElement &&
  35. (op !== document.body || getComputedStyle(op).position !== 'static')
  36. ) {
  37. const pr = op.getBoundingClientRect();
  38. return {
  39. x: pr.left + op.clientLeft - op.scrollLeft,
  40. y: pr.top + op.clientTop - op.scrollTop,
  41. };
  42. }
  43. return { x: -window.scrollX, y: -window.scrollY };
  44. }
  45.  
  46. function place(pv) {
  47. const a = anchors.get(pv);
  48. if (!a || !a.isConnected) return;
  49.  
  50. const r = a.getBoundingClientRect();
  51. const s = pv.style;
  52.  
  53. // absolute вместо fixed: пост прокручивается вместе со страницей
  54. s.setProperty('position', 'absolute', 'important');
  55. s.setProperty('right', 'auto', 'important');
  56. s.setProperty('bottom', 'auto', 'important');
  57. s.setProperty('margin', '0', 'important');
  58. s.setProperty('transform', 'none', 'important');
  59.  
  60. const w = pv.offsetWidth;
  61. const vw = document.documentElement.clientWidth;
  62.  
  63. let x = r.left + Math.min(r.width / 2, CENTER_CAP);
  64. if (w) x = Math.min(x, vw - w - MARGIN); // не вылезать за правый край окна
  65. x = Math.max(MARGIN, x);
  66. const y = r.bottom + OFFSET_Y;
  67.  
  68. const o = origin(pv);
  69. s.setProperty('left', (x - o.x) + 'px', 'important');
  70. s.setProperty('top', (y - o.y) + 'px', 'important');
  71. }
  72.  
  73. const mo = new MutationObserver((records) => {
  74. const todo = new Set();
  75.  
  76. for (const m of records) {
  77. if (m.type === 'childList') {
  78. m.addedNodes.forEach((n) => {
  79. if (n.nodeType !== 1) return;
  80. const list = [];
  81. if (n.matches('.pv')) list.push(n);
  82. n.querySelectorAll('.pv').forEach((el) => list.push(el));
  83. list.forEach((pv) => {
  84. anchors.set(pv, lastLink); // новый пост — запоминаем ссылку-источник
  85. todo.add(pv);
  86. });
  87. });
  88. } else if (m.type === 'attributes') {
  89. const t = m.target;
  90. if (t.classList && t.classList.contains('pv')) {
  91. if (!anchors.has(t)) anchors.set(t, lastLink);
  92. todo.add(t); // сайт перекинул пост по своим координатам — возвращаем на место
  93. }
  94. }
  95. }
  96.  
  97. todo.forEach(place);
  98. mo.takeRecords(); // выкидываем записи о наших собственных изменениях, чтобы не зациклиться
  99. });
  100.  
  101. mo.observe(document.documentElement, {
  102. childList: true,
  103. subtree: true,
  104. attributes: true,
  105. attributeFilter: ['style', 'class'],
  106. });
  107. })();
Advertisement
Add Comment
Please, Sign In to add comment