lemmy-keyboard-navigation

Easily navigate Lemmy with your keyboard

当前为 2023-07-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name lemmy-keyboard-navigation
  3. // @match https://*/*
  4. // @grant none
  5. // @version 1.9
  6. // @author vmavromatis
  7. // @author howdy@thesimplecorner.org
  8. // @author InfinibyteF4
  9. // @author aglidden
  10. // @license GPL3
  11. // @icon https://raw.githubusercontent.com/vmavromatis/Lemmy-keyboard-navigation/main/icon.png?inline=true
  12. // @homepageURL https://github.com/vmavromatis/Lemmy-keyboard-navigation
  13. // @namespace https://github.com/vmavromatis/Lemmy-keyboard-navigation
  14. // @description Easily navigate Lemmy with your keyboard
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. //isLemmySite
  19. if (document.querySelectorAll('.lemmy-site').length >= 1){
  20.  
  21. // Vim key toggle
  22. // Default: true
  23. // Set to false for arrow key navigation
  24. const vimKeyNavigation = true;
  25.  
  26. // Set selected entry colors
  27. const backgroundColor = '#373737';
  28. const textColor = 'white';
  29.  
  30. // Set navigation keys with keycodes here: https://www.toptal.com/developers/keycode
  31. let nextKey = 'ArrowDown';
  32. let prevKey = 'ArrowUp';
  33. let nextPageKey = 'ArrowRight';
  34. let prevPageKey = 'ArrowLeft';
  35.  
  36. if (vimKeyNavigation) {
  37. nextKey = 'KeyJ';
  38. prevKey = 'KeyK';
  39. nextPageKey = 'KeyL';
  40. prevPageKey = 'KeyH';
  41. }
  42.  
  43. const expandKey = 'KeyX';
  44. const openCommentsKey = 'KeyC';
  45. const openLinkandcollapseKey = 'Enter';
  46. const parentComment = 'KeyP';
  47. const upvoteKey = 'KeyA';
  48. const downvoteKey = 'KeyZ';
  49. const replycommKey = 'KeyR';
  50. const saveKey = 'KeyS';
  51. const popupKey = 'KeyG';
  52. const contextKey = 'KeyQ';
  53. const smallerimgKey = 'Minus';
  54. const biggerimgKey = 'Equal';
  55. const userKey = 'KeyU';
  56. const editKey = 'KeyE';
  57.  
  58. const modalCommentsKey = 'KeyC';
  59. const modalPostsKey = 'KeyP';
  60. const modalSubscribedKey = 'Digit1';
  61. const modalLocalKey = 'Digit2';
  62. const modalAllKey = 'Digit3';
  63. const modalSavedKey = 'KeyS';
  64. const modalFrontpageKey = 'KeyF';
  65. const modalProfileKey = 'KeyU';
  66. const modalInboxKey = 'KeyI';
  67.  
  68. const escapeKey = 'Escape';
  69. let modalMode = 0;
  70. console.log('modalMode: ' + modalMode);
  71.  
  72. // Stop arrows from moving the page if not using Vim navigation
  73. window.addEventListener("keydown", function(e) {
  74. if (["ArrowUp", "ArrowDown"].indexOf(e.code) > -1 && !vimKeyNavigation) {
  75. e.preventDefault();
  76. }
  77. }, false);
  78.  
  79. // Remove scroll animations
  80. document.documentElement.style = "scroll-behavior: auto";
  81.  
  82. // Set CSS for selected entry
  83. const css = [
  84. ".selected {",
  85. " background-color: " + backgroundColor + " !important;",
  86. " color: " + textColor + ";",
  87. "}"
  88. ].join("\n");
  89.  
  90. // dialog box
  91. let myDialog = document.createElement("dialog");
  92. document.body.appendChild(myDialog);
  93. let para = document.createElement("p");
  94. para.innerText = '--- Frontpage Sort ---\nP = posts\nC = comments\n1 = subscribed\n2 = local\n3 = all\n\n--- Everywhere Else ---\nS = saved\nF = frontpage\nU = profile\nI = inbox\n';
  95. myDialog.appendChild(para);
  96. let button = document.createElement("button");
  97. button.classList.add('CLOSEBUTTON1');
  98. button.innerHTML = 'Press ESC or G to Close';
  99. myDialog.appendChild(button);
  100.  
  101. // Global variables
  102. let currentEntry;
  103. let commentBlock;
  104. let addStyle;
  105. let PRO_addStyle;
  106. let entries = [];
  107. let previousUrl = "";
  108. let expand = false;
  109.  
  110. if (typeof GM_addStyle !== "undefined") {
  111. GM_addStyle(css);
  112. } else if (typeof PRO_addStyle !== "undefined") {
  113. PRO_addStyle(css);
  114. } else if (typeof addStyle !== "undefined") {
  115. addStyle(css);
  116. } else {
  117. let node = document.createElement("style");
  118. node.type = "text/css";
  119. node.appendChild(document.createTextNode(css));
  120. let heads = document.getElementsByTagName("head");
  121. if (heads.length > 0) {
  122. heads[0].appendChild(node);
  123. } else {
  124. // no head yet, stick it whereever
  125. document.documentElement.appendChild(node);
  126. }
  127. }
  128. const selectedClass = "selected";
  129.  
  130. const targetNode = document.documentElement;
  131. const config = {
  132. childList: true,
  133. subtree: true
  134. };
  135.  
  136. const observer = new MutationObserver(() => {
  137. entries = document.querySelectorAll(".post-listing, .comment-node");
  138.  
  139. if (entries.length > 0) {
  140. if (location.href !== previousUrl) {
  141. previousUrl = location.href;
  142. currentEntry = null;
  143. }
  144. init();
  145. }
  146. });
  147.  
  148. observer.observe(targetNode, config);
  149.  
  150. function init() {
  151. // If jumping to comments
  152. if (window.location.search.includes("scrollToComments=true") &&
  153. entries.length > 1 &&
  154. (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
  155. ) {
  156. selectEntry(entries[1], true);
  157. }
  158. // If jumping to comment from anchor link
  159. else if (window.location.pathname.includes("/comment/") &&
  160. (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
  161. ) {
  162. const commentId = window.location.pathname.replace("/comment/", "");
  163. const anchoredEntry = document.getElementById("comment-" + commentId);
  164.  
  165. if (anchoredEntry) {
  166. selectEntry(anchoredEntry, true);
  167. }
  168. }
  169. // If no entries yet selected, default to first
  170. else if (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0) {
  171. selectEntry(entries[0]);
  172. }
  173.  
  174. Array.from(entries).forEach(entry => {
  175. entry.removeEventListener("click", clickEntry, true);
  176. entry.addEventListener('click', clickEntry, true);
  177. });
  178.  
  179. document.removeEventListener("keydown", handleKeyPress, true);
  180. document.addEventListener("keydown", handleKeyPress, true);
  181. }
  182.  
  183. function handleKeyPress(event) {
  184. if (["TEXTAREA", "INPUT"].indexOf(event.target.tagName) > -1) {
  185. return;
  186. }
  187.  
  188. switch (modalMode) {
  189. case modalMode = 0:
  190. switch (event.code) {
  191. case nextKey:
  192. case prevKey:
  193. previousKey(event);
  194. break;
  195. case upvoteKey:
  196. upVote();
  197. break;
  198. case downvoteKey:
  199. downVote();
  200. break;
  201. case expandKey:
  202. toggleExpand();
  203. expand = isExpanded() ? true : false;
  204. break;
  205. case smallerimgKey:
  206. imgresize(0);
  207. break;
  208. case biggerimgKey:
  209. imgresize(1);
  210. break;
  211. case saveKey:
  212. save();
  213. break;
  214. case editKey:
  215. edit();
  216. break;
  217. case openCommentsKey:
  218. comments(event);
  219. break;
  220. case popupKey:
  221. gotodialog(1);
  222. instanceanduser();
  223. break;
  224. case contextKey:
  225. getcontext(event);
  226. break;
  227. case replycommKey:
  228. if (window.location.pathname.includes("/post/")) {
  229. // Allow Mac refresh with CMD+R
  230. if (event.key !== 'Meta') {
  231. reply(event);
  232. }
  233. } else {
  234. community(event);
  235. }
  236. break;
  237. case userKey:
  238. visituser(event);
  239. break;
  240. case openLinkandcollapseKey:
  241. if (window.location.pathname.includes("/post/")) {
  242. toggleExpand();
  243. } else {
  244. const linkElement = currentEntry.querySelector(".col.flex-grow-1>p>a");
  245. if (linkElement) {
  246. if (event.shiftKey) {
  247. window.open(linkElement.href);
  248. } else {
  249. linkElement.click();
  250. }
  251. } else {
  252. comments(event);
  253. }
  254. }
  255. break;
  256. case parentComment: {
  257. let targetBlock;
  258. if (currentEntry.classList.contains("ms-1")) {
  259. targetBlock = getPrevEntry(currentEntry);
  260. } else if (currentEntry.parentElement.parentElement.parentElement.nodeName === "LI") {
  261. targetBlock = currentEntry.parentElement.parentElement.parentElement.getElementsByTagName("article")[0];
  262. }
  263. if (targetBlock) {
  264. if (expand) {
  265. collapseEntry();
  266. }
  267. selectEntry(targetBlock, true);
  268. if (expand) {
  269. expandEntry();
  270. }
  271. }
  272. }
  273. break;
  274. case nextPageKey:
  275. case prevPageKey: {
  276. const pageButtons = Array.from(document.querySelectorAll(".paginator>button"));
  277.  
  278. if (pageButtons && (document.getElementsByClassName('paginator').length > 0)) {
  279. const buttonText = event.code === nextPageKey ? "Next" : "Prev";
  280. pageButtons.find(btn => btn.innerHTML === buttonText).click();
  281. }
  282. // Jump next block of comments
  283. if (event.code === nextPageKey) {
  284. commentBlock = getNextEntrySameLevel(currentEntry);
  285. }
  286. // Jump previous block of comments
  287. if (event.code === prevPageKey) {
  288. commentBlock = getPrevEntrySameLevel(currentEntry);
  289. }
  290. if (commentBlock) {
  291. if (expand) {
  292. collapseEntry();
  293. }
  294. selectEntry(commentBlock, true);
  295. if (expand) {
  296. expandEntry();
  297. }
  298. }
  299. }
  300. }
  301. break;
  302. case modalMode = 1:
  303. switch (event.code) {
  304. case escapeKey:
  305. modalMode = 0;
  306. console.log('modalMode: ' + modalMode);
  307. break;
  308. case popupKey:
  309. gotodialog(0);
  310. break;
  311. case modalSubscribedKey:
  312. let subelement = document.querySelectorAll('[title="Shows the communities you\'ve subscribed to"]')[0];
  313. subelement.click();
  314. gotodialog(0);
  315. break;
  316. case modalLocalKey:
  317. let localelement = document.querySelectorAll('[title="Shows only local communities"]')[0];
  318. localelement.click();
  319. gotodialog(0);
  320. break;
  321. case modalAllKey:
  322. let allelement = document.querySelectorAll('[title="Shows all communities, including federated ones"]')[0];
  323. allelement.click();
  324. gotodialog(0);
  325. break;
  326. case modalSavedKey:
  327. if (window.location.pathname.includes("/u/")) {
  328. let savedelement = document.getElementsByClassName("btn btn-outline-secondary pointer")[3];
  329. if (savedelement) {
  330. savedelement.click();
  331. gotodialog(0);
  332. }
  333. } else {
  334. instanceanduser(2);
  335. }
  336. break;
  337. case modalFrontpageKey:
  338. frontpage();
  339. break;
  340. case modalProfileKey:
  341. let profileelement = document.querySelectorAll('[title="Profile"]')[0];
  342. if (profileelement) {
  343. profileelement.click();
  344. gotodialog(0);
  345. } else {
  346. instanceanduser(1);
  347. }
  348. break;
  349. case modalInboxKey:
  350. let notifelement = document.getElementsByClassName("nav-link d-inline-flex align-items-center d-md-inline-block")[2];
  351. if (notifelement) {
  352. notifelement.click();
  353. gotodialog(0);
  354. } else {
  355. console.log('Not logged in!');
  356. }
  357. break;
  358. case modalCommentsKey:
  359. let commentsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[1];
  360. commentsbutton.click();
  361. gotodialog(0);
  362. break;
  363. case modalPostsKey:
  364. let postsbutton = document.getElementsByClassName("pointer btn btn-outline-secondary")[0];
  365. postsbutton.click();
  366. gotodialog(0);
  367. break;
  368. }
  369. }
  370. }
  371.  
  372. function getNextEntry(e) {
  373. const currentEntryIndex = Array.from(entries).indexOf(e);
  374.  
  375. if (currentEntryIndex + 1 >= entries.length) {
  376. return e;
  377. }
  378.  
  379. return entries[currentEntryIndex + 1];
  380. }
  381.  
  382. function getPrevEntry(e) {
  383. const currentEntryIndex = Array.from(entries).indexOf(e);
  384.  
  385. if (currentEntryIndex - 1 < 0) {
  386. return e;
  387. }
  388.  
  389. return entries[currentEntryIndex - 1];
  390. }
  391.  
  392. function getNextEntrySameLevel(e) {
  393. const nextSibling = e.parentElement.nextElementSibling;
  394.  
  395. if (!nextSibling || nextSibling.getElementsByTagName("article").length < 1) {
  396. return getNextEntry(e);
  397. }
  398.  
  399. return nextSibling.getElementsByTagName("article")[0];
  400. }
  401.  
  402. function getPrevEntrySameLevel(e) {
  403. const prevSibling = e.parentElement.previousElementSibling;
  404.  
  405. if (!prevSibling || prevSibling.getElementsByTagName("article").length < 1) {
  406. return getPrevEntry(e);
  407. }
  408.  
  409. return prevSibling.getElementsByTagName("article")[0];
  410. }
  411.  
  412. function clickEntry(event) {
  413. const e = event.currentTarget;
  414. const target = event.target;
  415.  
  416. // Deselect if already selected, also ignore if clicking on any link/button
  417. if (e === currentEntry && e.classList.contains(selectedClass) &&
  418. !(
  419. target.tagName.toLowerCase() === "button" || target.tagName.toLowerCase() === "a" ||
  420. target.parentElement.tagName.toLowerCase() === "button" ||
  421. target.parentElement.tagName.toLowerCase() === "a" ||
  422. target.parentElement.parentElement.tagName.toLowerCase() === "button" ||
  423. target.parentElement.parentElement.tagName.toLowerCase() === "a"
  424. )
  425. ) {
  426. e.classList.remove(selectedClass);
  427. } else {
  428. selectEntry(e);
  429. }
  430. }
  431.  
  432. function selectEntry(e, scrollIntoView = false) {
  433. if (currentEntry) {
  434. currentEntry.classList.remove(selectedClass);
  435. }
  436. currentEntry = e;
  437. currentEntry.classList.add(selectedClass);
  438.  
  439. if (scrollIntoView) {
  440. scrollIntoViewWithOffset(e, 15);
  441. }
  442. }
  443.  
  444. function isExpanded() {
  445. if (
  446. currentEntry.querySelector("a.d-inline-block:not(.thumbnail)") ||
  447. currentEntry.querySelector("#postContent") ||
  448. currentEntry.querySelector(".card-body")
  449. ) {
  450. return true;
  451. }
  452.  
  453. return false;
  454. }
  455.  
  456. function previousKey(event) {
  457. let selectedEntry;
  458. // Next button
  459. if (event.code === nextKey) {
  460. if (event.shiftKey && vimKeyNavigation) {
  461. selectedEntry = getNextEntrySameLevel(currentEntry);
  462.  
  463. } else {
  464. selectedEntry = getNextEntry(currentEntry);
  465. }
  466. }
  467. // Previous button
  468. if (event.code === prevKey) {
  469. if (event.shiftKey && vimKeyNavigation) {
  470. selectedEntry = getPrevEntrySameLevel(currentEntry);
  471.  
  472. } else {
  473. selectedEntry = getPrevEntry(currentEntry);
  474. }
  475. }
  476. if (selectedEntry) {
  477. if (expand) {
  478. collapseEntry();
  479. }
  480. selectEntry(selectedEntry, true);
  481. if (expand) {
  482. expandEntry();
  483. }
  484. }
  485. }
  486.  
  487. function upVote() {
  488. const upvoteButton = currentEntry.querySelector("button[aria-label='Upvote']");
  489.  
  490. if (upvoteButton) {
  491. upvoteButton.click();
  492. }
  493. }
  494.  
  495. function downVote() {
  496. const downvoteButton = currentEntry.querySelector("button[aria-label='Downvote']");
  497.  
  498. if (downvoteButton) {
  499. downvoteButton.click();
  500. }
  501. }
  502.  
  503. function gotodialog(n) {
  504.  
  505. const closeButton = document.getElementsByClassName("CLOSEBUTTON1")[0];
  506. closeButton.addEventListener("click", () => {
  507. myDialog.close();
  508. modalMode = 0;
  509. console.log('modalMode: ' + modalMode);
  510. });
  511. if (n === 1) {
  512. myDialog.showModal();
  513. modalMode = 1;
  514. console.log('modalMode: ' + modalMode);
  515. }
  516.  
  517. if (n === 0) {
  518. myDialog.close();
  519. modalMode = 0;
  520. console.log('modalMode: ' + modalMode);
  521. }
  522. }
  523.  
  524. function instanceanduser(n) {
  525. let currentinstance = window.location.origin;
  526. let dropdownuser = document.getElementsByClassName("btn dropdown-toggle")[0];
  527. let username = dropdownuser.textContent;
  528.  
  529. if (n === 0) {
  530. window.location.replace(currentinstance);
  531. }
  532. if (n === 1) {
  533. if (username) {
  534. let userlink = currentinstance + "/u/" + username;
  535. window.location.replace(userlink);
  536. } else {
  537. console.log('Not logged in!');
  538. frontpage();
  539. }
  540. }
  541. if (n === 2) {
  542. if (username) {
  543. let savedlink = currentinstance + "/u/" + username + "?page=1&sort=New&view=Saved";
  544. window.location.replace(savedlink);
  545. } else {
  546. console.log('Not logged in!');
  547. frontpage();
  548. }
  549. }
  550. }
  551.  
  552. function frontpage() {
  553. let homeelement = document.getElementsByClassName("d-flex align-items-center navbar-brand me-md-3 active")[0];
  554. if (homeelement) {
  555. homeelement.click();
  556. gotodialog(0);
  557. } else {
  558. instanceanduser(0);
  559. }
  560. }
  561.  
  562. function reply(event) {
  563. const replyButton = currentEntry.querySelector("button[data-tippy-content='reply']");
  564.  
  565. if (replyButton) {
  566. event.preventDefault();
  567. replyButton.click();
  568. }
  569. }
  570.  
  571. function community(event) {
  572. if (event.shiftKey) {
  573. window.open(
  574. currentEntry.querySelector("a.community-link").href,
  575. );
  576. } else {
  577. currentEntry.querySelector("a.community-link").click();
  578. }
  579. }
  580.  
  581. function visituser(event) {
  582. if (event.shiftKey) {
  583. window.open(
  584. currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].href,
  585. );
  586. } else {
  587. currentEntry.getElementsByClassName("person-listing d-inline-flex align-items-baseline text-info")[0].click();
  588. }
  589. }
  590.  
  591. function comments(event) {
  592. if (event.shiftKey) {
  593. window.open(
  594. currentEntry.querySelector("a.btn[title*='Comment']").href,
  595. );
  596. } else {
  597. currentEntry.querySelector("a.btn[title*='Comment']").click();
  598. }
  599. }
  600.  
  601. function getcontext(event) {
  602. if (event.shiftKey) {
  603. window.open(
  604. currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].href,
  605. );
  606. } else {
  607. currentEntry.getElementsByClassName("btn btn-link btn-animate text-muted btn-sm")[0].click();
  608. }
  609. }
  610.  
  611. let maxsize = 0;
  612. console.log('maxsize ' + maxsize);
  613.  
  614. function imgresize(n) {
  615. let expandedimg = currentEntry.getElementsByClassName("overflow-hidden pictrs-image img-fluid img-expanded slight-radius")[0];
  616. let expandedheight = expandedimg.height;
  617. let expandedwidth = expandedimg.width;
  618. let expandedheightbefore = expandedheight;
  619. let expandedwidthbefore = expandedwidth;
  620.  
  621. if (n === 0) {
  622. expandedheight = expandedheight / 1.15;
  623. expandedwidth = expandedwidth / 1.15;
  624. expandedimg.style.height = expandedheight + 'px';
  625. expandedimg.style.width = expandedwidth + 'px';
  626. maxsize = 0;
  627. console.log('maxsize ' + maxsize);
  628. }
  629.  
  630. if (n === 1) {
  631. expandedheight = expandedheight * 1.15;
  632. expandedwidth = expandedwidth * 1.15;
  633. expandedimg.style.width = expandedwidth + 'px';
  634. expandedimg.style.height = expandedheight + 'px';
  635.  
  636. if (maxsize === 1) {
  637. expandedimg.style.width = expandedwidthbefore + 'px';
  638. expandedimg.style.height = expandedheightbefore + 'px';
  639. }
  640. if (expandedimg.width !== Math.round(expandedwidth) || expandedimg.height !== Math.round(expandedheight)) {
  641. maxsize = 1;
  642. console.log('maxsize ' + maxsize);
  643. }
  644. }
  645. }
  646.  
  647. function save() {
  648. const saveButton = currentEntry.querySelector("button[aria-label='save']");
  649. const unsaveButton = currentEntry.querySelector("button[aria-label='unsave']");
  650. const moreButton = currentEntry.querySelector("button[aria-label='more']");
  651. if (saveButton) {
  652. saveButton.click();
  653. } else if (unsaveButton) {
  654. unsaveButton.click();
  655. } else {
  656. moreButton.click();
  657. if (saveButton) {
  658. saveButton.click();
  659. } else if (unsaveButton) {
  660. unsaveButton.click();
  661. }
  662. }
  663. }
  664.  
  665. function edit() {
  666. let editButton = currentEntry.querySelector("button[aria-label='Edit']");
  667. let moreButton = currentEntry.querySelector("button[aria-label='more']");
  668.  
  669. if (editButton) {
  670. editButton.click();
  671. } else {
  672. moreButton.click();
  673. }
  674. }
  675.  
  676. function toggleExpand() {
  677. const expandButton = currentEntry.querySelector("button[aria-label='Expand here']");
  678. const textExpandButton = currentEntry.querySelector(".post-title>button");
  679. const commentExpandButton = currentEntry.querySelector(".ms-2>div>button");
  680. const moreExpandButton = currentEntry.querySelector(".ms-1>button");
  681.  
  682. if (expandButton) {
  683. expandButton.click();
  684.  
  685. // Scroll into view if picture/text preview cut off
  686. const imgContainer = currentEntry.querySelector("a.d-inline-block");
  687.  
  688. if (imgContainer) {
  689. // Check container positions once image is loaded
  690. imgContainer.querySelector("img").addEventListener("load", function() {
  691. scrollIntoViewWithOffset(
  692. imgContainer,
  693. currentEntry.offsetHeight - imgContainer.offsetHeight + 10
  694. );
  695. }, true);
  696. currentEntry.getElementsByClassName("offset-sm-3 my-2 d-none d-sm-block")[0].className = "my-2 d-none d-sm-block";
  697. }
  698. }
  699.  
  700. if (textExpandButton) {
  701. textExpandButton.click();
  702.  
  703. const textContainers = [currentEntry.querySelector("#postContent"), currentEntry.querySelector(".card-body")];
  704. textContainers.forEach(container => {
  705. if (container) {
  706. scrollIntoViewWithOffset(
  707. container,
  708. currentEntry.offsetHeight - container.offsetHeight + 10
  709. );
  710. }
  711. });
  712. }
  713.  
  714. if (commentExpandButton) {
  715. commentExpandButton.click();
  716. }
  717.  
  718. if (moreExpandButton) {
  719. moreExpandButton.click();
  720. selectEntry(getPrevEntry(currentEntry), true);
  721. }
  722. }
  723.  
  724. function expandEntry() {
  725. if (!isExpanded()) {
  726. toggleExpand();
  727. }
  728. }
  729.  
  730. function collapseEntry() {
  731. if (isExpanded()) {
  732. toggleExpand();
  733. }
  734. }
  735.  
  736. function scrollIntoViewWithOffset(e, offset) {
  737. if (e.getBoundingClientRect().top < 0 ||
  738. e.getBoundingClientRect().bottom > window.innerHeight
  739. ) {
  740. const y = e.getBoundingClientRect().top + window.pageYOffset - offset;
  741. window.scrollTo({
  742. top: y
  743. });
  744. }
  745.  
  746. }
  747.  
  748. }