StackOverflow extended

Copy code to clipboard; hiding and saving the state of the "Blog", "Meta" blocks by clicking; adding links to all questions of the author and all questions only with tags of the current question to the user's card; stretching and restoring page content for better reading of code listings; redirecting from localized versions of the site to an English-language domain with a search for the current question.

  1. // ==UserScript==
  2. // @name StackOverflow extended
  3. // @namespace https://github.com/XelaNimed
  4. // @version 0.10.1-SNAPSHOT
  5. // @description Copy code to clipboard; hiding and saving the state of the "Blog", "Meta" blocks by clicking; adding links to all questions of the author and all questions only with tags of the current question to the user's card; stretching and restoring page content for better reading of code listings; redirecting from localized versions of the site to an English-language domain with a search for the current question.
  6. // @author XelaNimed
  7. // @copyright 2021, XelaNimed (https://github.com/XelaNimed)
  8. // @match https://*.stackoverflow.com/*
  9. // @match https://*.meta.stackoverflow.com/*
  10. // @grant GM_getResourceText
  11. // @grant GM_addStyle
  12. // @homepageURL https://github.com/XelaNimed/ruSO
  13. // @supportURL https://github.com/XelaNimed/ruSO/issues
  14. // @iconURL https://www.google.com/s2/favicons?domain=stackoverflow.com&sz=32
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js#sha512-aVKKRRi/Q/YV+4mjoKBsE4x3H+BkegoM/em46NNlCqNTmUYADjBbeNefNxYV7giUp0VxICtqdrbqU7iVaeZNXA==
  16. // @require https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.1/js/iziModal.min.js#sha512-lR/2z/m/AunQdfBTSR8gp9bwkrjwMq1cP0BYRIZu8zd4ycLcpRYJopB+WsBGPDjlkJUwC6VHCmuAXwwPHlacww==
  17. // @resource IZI_MODAL https://cdnjs.cloudflare.com/ajax/libs/izimodal/1.6.1/css/iziModal.min.css#sha512-3c5WiuZUnVWCQGwVBf8XFg/4BKx48Xthd9nXi62YK0xnf39Oc2FV43lIEIdK50W+tfnw2lcVThJKmEAOoQG84Q==
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. const $ = window.jQuery;
  22.  
  23. $.fn.extend({
  24. toggleText: function(a, b){
  25. return this.text(this.text() == b ? a : b);
  26. }
  27. });
  28.  
  29. /**
  30. * Root object
  31. */
  32. const ruSO = {
  33. $sidebar: $('#sidebar'),
  34. $content: $('#content'),
  35. $container: $('body>.container'),
  36. $fullWidthBtn: null,
  37. /**
  38. * Keys for access to Local Storage values.
  39. */
  40. lsKeys: {
  41. metaBlockIsVisible: 'so-ext-metaBlockIsVisible',
  42. nativeLang: 'so-ext-nativeLanguage',
  43. useSearchRedirectBtn: 'so-ext-useSearchRedirectBtn',
  44. addLinkToMeta: 'so-ext-addLinkToMeta',
  45. toggleMetaBlock: 'so-ext-toggleMetaBlock',
  46. appLang: 'so-ext-appLang'
  47. },
  48. strings: {
  49. clickToToggle: 'Скрыть/показать',
  50. setFullWidth: 'Растянуть',
  51. resetFullWidth: 'Восстановить',
  52. copy: 'Копировать',
  53. copied: 'Скопировано',
  54. canNotCopy: 'Упс, ошибка',
  55. intoClipboard: 'В буфер'
  56. },
  57.  
  58. /**
  59. * Checks if a value with the specified key exists in local storage.
  60. * @param {string} key Value key in local storage
  61. * @returns Returns true if the value with the specified key is present in local storage, is not null or an empty string, false otherwise.
  62. */
  63. isLSNotInitForKey: function(key) { return localStorage[key] === undefined || localStorage[key] == null || localStorage[key] === ''; },
  64. setLSDefaults: function() {
  65.  
  66. if(this.isLSNotInitForKey(this.lsKeys.nativeLang)) {
  67. const lang = navigator.language || navigator.userLanguage;
  68. if(this.getSupportedSubDomains().includes(lang)) {
  69. localStorage[this.lsKeys.nativeLang] = lang;
  70. } else {
  71. localStorage[this.lsKeys.useSearchRedirectBtn] = false;
  72. }
  73. }
  74. if(this.isLSNotInitForKey(this.lsKeys.useSearchRedirectBtn)) {
  75. localStorage[this.lsKeys.useSearchRedirectBtn] = true;
  76. }
  77. if(this.isLSNotInitForKey(this.lsKeys.toggleMetaBlock)) {
  78. localStorage[this.lsKeys.toggleMetaBlock] = true;
  79. }
  80. if(this.isLSNotInitForKey(this.lsKeys.metaBlockIsVisible)) {
  81. localStorage[this.lsKeys.metaBlockIsVisible] = true;
  82. }
  83. return this;
  84. },
  85. isUseSearchRedirectBtn: function() { return localStorage[this.lsKeys.useSearchRedirectBtn] == 'true'; },
  86. getNativeLang: function() { return localStorage[this.lsKeys.nativeLang]; },
  87. isNativeLang: function(lang) { return localStorage[this.lsKeys.nativeLang] === lang; },
  88. addLinkToMeta: function() { return localStorage[this.lsKeys.addLinkToMeta] == 'true'; },
  89. toggleMetaBlock: function() { return localStorage[this.lsKeys.toggleMetaBlock] == 'true'; },
  90. getSupportedSubDomains: function() { return ['ru', 'es', 'pt', 'ja']; },
  91. addSettingsModalDialog: function() {
  92.  
  93. let options = this.getSupportedSubDomains()
  94. .flatMap((l) => '<option value="' + l + '"' + (this.isNativeLang(l) ? ' selected="selected"' : '') + '>' + l + '</option>' )
  95. .join('');
  96.  
  97. $(document.body).append(`<div id="iziModal" style="display: none;">
  98.  
  99. <div class="izi-content">
  100.  
  101. <div class="d-flex ai-center jc-space-between p16">
  102. <label class="flex--item s-label p0" for="so-ext-search-btn-toggle">
  103. <div class="d-flex ai-center">Use redirect to enSO</div>
  104. <p class="s-description">When this option is enabled, a button redirecting the current search to the English-language StackOverflow site will be added at the end of the search field.</p>
  105. </label>
  106. <div class="flex--item s-toggle-switch">
  107. <input id="so-ext-search-btn-toggle" type="checkbox"${this.isUseSearchRedirectBtn() ? 'checked="checked"' : ''}>
  108. <div class="s-toggle-switch--indicator"></div>
  109. </div>
  110. </div>
  111.  
  112. <div id="so-ext-native-language-block" class="d-flex ai-center jc-space-between p16${this.isUseSearchRedirectBtn() ? '' : ' o50 pe-none'}">
  113. <label class="s-label flex--item" for="so-ext-native-language">Native language
  114. <p class="s-description">The two-letter code for the subdomain of the regional StackOverflow site. Used when redirecting search queries to the English version and vice versa.</p>
  115. </label>
  116. <div class="d-flex">
  117. <select id="so-ext-native-language" class="flex--item s-input" style="width: 75px;" autofocus="true">
  118. ${options}
  119. </select>
  120. </div>
  121. </div>
  122.  
  123. <div class="d-flex ai-center jc-space-between p16">
  124. <label class="flex--item s-label p0" for="so-ext-add-meta-link">
  125. <div class="d-flex ai-center">Add a link to the side menu</div>
  126. <p class="s-description">If this option is enabled, a link to the Meta will be added to the side menu of the StackOverflow site, and a link to the StackOverflow site will be added to the Meta site.</p>
  127. </label>
  128. <div class="flex--item s-toggle-switch">
  129. <input id="so-ext-add-meta-link" type="checkbox"${this.addLinkToMeta() ? 'checked="checked"' : ''}>
  130. <div class="s-toggle-switch--indicator"></div>
  131. </div>
  132. </div>
  133.  
  134. <div class="d-flex ai-center jc-space-between p16">
  135. <label class="flex--item s-label p0" for="so-ext-toggle-meta-block">
  136. <div class="d-flex ai-center">Minimize the Meta block</div>
  137. <p class="s-description">If this option is enabled, the Meta block with popular questions can be minimized and maximized with the state saved in the local storage.</p>
  138. </label>
  139. <div class="flex--item s-toggle-switch">
  140. <input id="so-ext-toggle-meta-block" type="checkbox"${this.toggleMetaBlock() ? 'checked="checked"' : ''}>
  141. <div class="s-toggle-switch--indicator"></div>
  142. </div>
  143. </div>
  144.  
  145. <div class="d-flex ai-center p16 button-panel">
  146. <button class="flex--item s-btn s-btn__filled" role="button" value="cancel">Cancel</button>
  147. <button class="flex--item s-btn s-btn__filled" role="button" value="save">Save</button>
  148. <button class="flex--item s-btn s-btn__primary" role="button" value="save-reload">Save and reload</button>
  149. </div>
  150.  
  151. </div>
  152.  
  153. </div>`);
  154. return this;
  155. },
  156. addCSSStyles: function() {
  157.  
  158. 'use strict';
  159.  
  160. GM_addStyle(GM_getResourceText("IZI_MODAL"));
  161. GM_addStyle('#iziModal { background-color: var(--theme-content-background-color); box-shadow: 0px 0px 2px 0px var(--theme-button-color); }'+
  162. '#iziModal * { font-family: var(--theme-body-font-family); }' +
  163. '#iziModal .iziModal-header-title, #iziModal label { color: var(--theme-body-font-color); }' +
  164. '#iziModal .iziModal-header-subtitle { color: var(--theme-footer-link-color); }' +
  165. '#iziModal .iziModal-header .iziModal-button-close { background: ' +
  166. 'url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAAAl' +
  167. 'CAYAAADFniADAAAACXBIWXMAAAsTAAALEwEAmpwYAAADIklEQVRYhe2XTWjUQ' +
  168. 'BSA1/qHP+A//oAgqCcVFA9W7WYSu1YPBfWwIFWbmSys+FM9qcfFiqggFMWDJ/' +
  169. 'EmFfWooCfRg4gXZRWUaruzSX8Ei9aTRbu+l0x2s9mNu8lmC0IehCTz8+Z7782' +
  170. 'bvMRikUQSyX8ouYS2xlC0Dq7QLk7UHp2ohw1Z3f1pf8/caQXBBfMyO6PL7DXc' +
  171. 'p+BecF95wn7kCe3XJW1n04G4xA7BYkOlxekfnbCsLtPHOqEP8jJ9AW2j5ZD00' +
  172. 'SBRV4UOU4jFZsCCvUXPEKZjuIbi6dWVYzMteaK2wvg74LHfAn6IE21zqFCg9K' +
  173. 'LD8j7empxXzzwu0S0A98EEk+lEaGBc1o6gh6yLHse2AsnMqjUPx6CHPyfSiyC' +
  174. '0zwXYF96RWtoQUK6tawkoGxchu4xtepyuResNoh70mvdme3o27iWYcxvB9PZj' +
  175. 'y+B5QITyVkNQoOCaCNu7QjI5EzesrRzuv6qBmUCEPXQkw3XTGFltF+Gf5CS1I' +
  176. 'RAQQoBHvqIiQ6YHSm3snmN/TWJGlgGhhxz9TnCY+9QKI7sUCIormiSUj+Ni5b' +
  177. 'CVYLWATCiFddueDwQFXrogFNx397lDhACQ/i+L7xha4V2nDLexFfaYkb1HF/i' +
  178. 'Ggoy5IRa4Wq2/0mPeHnIZO2FCEXWdbyiw/K61n9h5rzFGZ3q++wTH5PinXpkN' +
  179. 'in21IwCUlXlei1Tuobo99dPUK9GNvqF0op1r5p4aIycX+oYyJBqvln1eWVYtK' +
  180. '90egzbVgqZvfQOhlJ1TQnmttK8FBvqeic9NbyAo0zLIPBGObMMnOmEJe15O6V' +
  181. '4fGAogFoOrvwmrr5jK6/j2lTxG+/DbZ5D0cjvr4LoZGMgWrBJECk+BlSfMRX1' +
  182. 'UCWgYFn/CSwMNVwm2FMMoLK33NIaafStk5Ue7noKwbQoFCMUsP+CIMMtfK5TD' +
  183. 'sMhZ/HGoHJtp0RW6Cw9fezw856Be3xYakFPAY51YqBU3MRZ+hL6H5yelGp2Nu' +
  184. 'fr7R/ekVjYFyJZsMjnHIPQ0QLzy+puBvu+40QN9ShoV/HHgMt0HoaUAcwozEn' +
  185. '+rEHzaYSKJJJIQ5C/46/lP65NjdQAAAABJRU5ErkJggg==\') no-repeat 50% 50%;' +
  186. 'width: 48px; }' +
  187. '#iziModal .izi-content { background-color: var(--theme-content-background-color); }' +
  188. '#iziModal .izi-content > div { border-bottom: 1px solid var(--theme-content-border-color); }' +
  189. '#iziModal .izi-content > div:last-child { border-bottom: 0; }' +
  190. '#iziModal label { }' +
  191. '#iziModal .s-description { color: var(--theme-footer-link-color); }' +
  192. '#iziModal .button-panel { justify-content: flex-end; background-color: var(--theme-footer-background-color); }' +
  193. '#iziModal .button-panel button { margin-left: 10px; }' +
  194. 'body > div.iziModal-overlay { backdrop-filter: blur(1px); }' +
  195. 'body > .container, #content { transition: all 0.5s ease-in-out; }' +
  196. 'body > div.container.fullWidth, ' +
  197. 'body > div.container.fullWidth #content { max-width: 100% !important; }');
  198.  
  199. return this;
  200. },
  201.  
  202. init: function() {
  203. return this.setLSDefaults()
  204. .addSettingsModalDialog()
  205. .addCSSStyles();
  206. },
  207. addButtons: function () {
  208. let self = this,
  209. addScriptSettings = function() {
  210. $('<li><ol class="nav-links"><a href="#" class="nav-links--link">UserScript settings</a></li></ol></li>')
  211. .on('click', 'a', function(e) {
  212. e.preventDefault();
  213. e.stopPropagation();
  214. $("#iziModal").iziModal({
  215. title: 'Extended StackOverflow Settings',
  216. subtitle: 'All settings are saved in the local storage and will take effect when the page reloads',
  217. headerColor: 'var(--theme-footer-background-color)',
  218. // background: 'rgba(78, 78, 71, 1)',
  219. // icon: '.close-icon',
  220. // iconText: null,
  221. //iconColor: 'var(--theme-body-font-color)',
  222. width: 600,
  223. radius: 'var(--br-sm)',
  224. borderBottom: false,
  225. zindex: 9999,
  226. focusInput: true,
  227. bodyOverflow: false,
  228. // fullscreen: true,
  229. // openFullscreen: false,
  230. appendToOverlay: 'body', // or false
  231. overlay: true,
  232. overlayClose: true,
  233. // overlayColor: 'rgba(0, 0, 0, 0.3)'
  234. }).iziModal('open');
  235. })
  236. .insertAfter($('#left-sidebar nav > ol > li').last());
  237.  
  238. $('#iziModal')
  239. .on('click', 'button', function(e) {
  240. if(e.target.value.startsWith('save')) {
  241. localStorage[self.lsKeys.nativeLang] = $('#so-ext-native-language option:selected').val();
  242. localStorage[self.lsKeys.useSearchRedirectBtn] = $('#so-ext-search-btn-toggle').is(':checked');
  243. localStorage[self.lsKeys.addLinkToMeta] = $('#so-ext-add-meta-link').is(':checked');
  244. localStorage[self.lsKeys.toggleMetaBlock] = $('#so-ext-toggle-meta-block').is(':checked');
  245. }
  246. if(e.target.value.endsWith('reload')) {
  247. document.location.reload();
  248. }
  249. $('#iziModal').iziModal('close');
  250. })
  251. .on('change', 'input', function(e) {
  252. if(e.target.id == 'so-ext-search-btn-toggle') {
  253. $('#so-ext-native-language-block')[e.target.checked ? 'removeClass' : 'addClass']('o50 pe-none');
  254. }
  255. });
  256. },
  257. addWatchedTags = function () {
  258. let tags = [],
  259. urlPrefix = window.location.origin + '/questions/tagged/';
  260. $('.js-watched-tag-list a.user-tag').each(function (idx, itm) {
  261. let url = itm.href;
  262. tags.push(url.substring(url.lastIndexOf('/') + 1));
  263. });
  264. if (tags.length) {
  265. let url = urlPrefix + tags.join('+or+');
  266. let $header = self.$sidebar.find(".js-tag-preferences-container > div").first().find("h2");
  267. if ($header.length > 0) {
  268.  
  269. $header[0].innerHTML = '<a class="post-tag user-tag" href="' + url + '">' + $header.text() + '</a>';
  270. }
  271. }
  272. },
  273. addMetaToggles = function () {
  274. if(!self.toggleMetaBlock()) {
  275. return;
  276. }
  277. let showHideMetas = function ($elem) {
  278. let isVisible = localStorage[self.lsKeys.metaBlockIsVisible] === 'true';
  279. let $elems = $elem.parent().children('li.s-sidebarwidget--item');
  280. $elems.each(function(idx, itm){
  281. let $itm = $(itm);
  282. if(isVisible)
  283. {
  284. $itm.removeAttr('style');
  285. } else {
  286. $itm.attr('style', 'display: none !important');
  287. }
  288. });
  289. };
  290. self.$sidebar
  291. .find('div.s-sidebarwidget li.s-sidebarwidget--header')
  292. .each(function (idx, itm) {
  293. let $itm = $(itm);
  294. $itm
  295. .attr('title', ruSO.strings.clickToToggle)
  296. .css('cursor', 'pointer')
  297. .on('click', function (e) {
  298. let isVisible = localStorage.getItem(self.lsKeys.metaBlockIsVisible) === 'true';
  299. localStorage.setItem(self.lsKeys.metaBlockIsVisible, !isVisible);
  300. showHideMetas($(e.target));
  301. });
  302. showHideMetas($itm);
  303. });
  304. },
  305. addLinkToMeta = function () {
  306. if(!self.addLinkToMeta()) {
  307. return;
  308. }
  309. const isMeta = window.location.host.includes('meta.');
  310. const link = isMeta ? window.location.host.split('.').filter(part => part !== 'meta').join('.')
  311. : 'meta.' + window.location.host;
  312. const linkText = isMeta ? 'StackOverflow' : 'Meta';
  313. $('<li><ol class="nav-links"><a href="https://' + link + '" class="nav-links--link">' + linkText + '</a></ol></li>').insertAfter($('#left-sidebar nav > ol > li').last());
  314. },
  315. addFullWidth = function () {
  316. let $header = $('#question-header');
  317. self.$fullWidthBtn = $header.find('div').clone();
  318. self.$fullWidthBtn.attr('id', 'set-full-width-btn').find('a')
  319. .removeClass('s-btn__primary')
  320. .addClass('s-btn__filled')
  321. .attr('href', '#')
  322. .text(self.strings.setFullWidth)
  323. .on('click', function (e) {
  324. e.preventDefault();
  325. e.stopPropagation();
  326. self.$container.toggleClass('fullWidth');
  327. $(this).toggleText(self.strings.setFullWidth, self.strings.resetFullWidth);
  328. });
  329. $header.append(self.$fullWidthBtn);
  330. },
  331. addRedirectToSO = function () {
  332. if(!self.isUseSearchRedirectBtn()) {
  333. return;
  334. }
  335. let localPrefix = self.getNativeLang() + '.';
  336. let isLocalSO = location.host.substr(0, 3) === localPrefix;
  337. let btnText = isLocalSO ? 'en' : self.getNativeLang();
  338. let $btn = $('<div class="print:d-none"><a href="#" class="s-btn s-btn__filled s-btn__xs s-btn__icon ws-nowrap">' + btnText + '</a></div>');
  339. $btn.insertAfter($('#search'));
  340. $btn.on('click', function () {
  341. location.host = isLocalSO ? location.host.substr(localPrefix.length)
  342. : localPrefix + location.host;
  343. });
  344. };
  345. addWatchedTags();
  346. addMetaToggles();
  347. addLinkToMeta();
  348. addFullWidth();
  349. addRedirectToSO();
  350. addScriptSettings();
  351. return this;
  352. },
  353. addAuthorQuestionsLinks: function () {
  354. let $userDetails = $('div.owner > div.user-info > div.user-details');
  355. if ($userDetails.length > 0) {
  356. let $postTags = $('div.post-taglist').find('a.post-tag');
  357. let tags = [];
  358. for (const $postTag of $postTags) {
  359. tags.push('[' + $postTag.href.split('/').slice(-1).pop() + ']');
  360. }
  361. let tagsUrl = tags.join('+or+');
  362. for (const userDetail of $userDetails) {
  363. const $userDetail = $(userDetail);
  364. const $userUrl = $userDetail.find('a');
  365. const userName = $userUrl.text();
  366. const userId = $userUrl[0].href.split('/')[4];
  367. const baseSearchUrl = `https://ru.stackoverflow.com/search?tab=newest&q=user%3A${userId}+is%3Aq`;
  368. let elem = `<span>? <a href="${baseSearchUrl}" title="Все вопросы ${userName}">все</a>`;
  369. if (tags.length > 0) {
  370. elem += `, <a href="${baseSearchUrl}+${tagsUrl}" title="Вопросы ${userName} с метками текущего вопроса" такими-же метками</a>`;
  371. }
  372. elem += '</span>';
  373. $(elem).insertAfter($userDetail);
  374. }
  375. }
  376. return this;
  377. },
  378. selectElemText: function(elem) {
  379. const range = document.createRange();
  380. range.selectNodeContents(elem);
  381. const sel = window.getSelection();
  382. sel.removeAllRanges();
  383. sel.addRange(range);
  384. },
  385. getSelectedText: function() {
  386. let text = '';
  387. if (window.getSelection) {
  388. text = window.getSelection();
  389. } else if (document.getSelection) {
  390. text = document.getSelection();
  391. } else if (document.selection) {
  392. text = document.selection.createRange().text;
  393. }
  394. return text;
  395. },
  396. copyToClipboard: function(text) {
  397. if (window.clipboardData && window.clipboardData.setData) {
  398. return window.clipboardData.setData("Text", text);
  399. } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
  400. const textarea = document.createElement("textarea");
  401. textarea.textContent = text;
  402. textarea.style.position = "fixed";
  403. document.body.appendChild(textarea);
  404. textarea.select();
  405. try {
  406. return document.execCommand("copy");
  407. } catch (ex) {
  408. console.warn("Copy to clipboard failed", ex);
  409. return false;
  410. } finally {
  411. document.body.removeChild(textarea);
  412. }
  413. }
  414. },
  415. addCopyToClipboard: function() {
  416.  
  417. const self = this;
  418.  
  419. $('.snippet-ctas').each(function() {
  420. const $el = $(this);
  421. const $availableBtn = $el.find('.copySnippet');
  422. const $snipBtn = $availableBtn.clone();
  423. $snipBtn.val(self.strings.intoClipboard);
  424. $snipBtn.click(function() {
  425.  
  426. let code = "";
  427.  
  428. $snipBtn.closest('.snippet-code').find('pre > code').each(function() {
  429. self.selectElemText(this);
  430. let selectedText = self.getSelectedText();
  431. code += selectedText + '\n';
  432. window.getSelection().removeAllRanges();
  433. });
  434.  
  435. if(self.copyToClipboard(code)) {
  436. $snipBtn.val(self.strings.copied);
  437. } else {
  438. $snipBtn.val(self.strings.canNotCopy);
  439. }
  440.  
  441. setTimeout(function () {
  442. $snipBtn.val(self.strings.intoClipboard);
  443. }, 2000);
  444. });
  445. $availableBtn.after($snipBtn);
  446. });
  447.  
  448. $("pre").each(function () {
  449.  
  450. const $pre = $(this);
  451. const $parent = $pre.parent();
  452.  
  453. if($parent.hasClass('snippet-code')) {
  454. const padding = ($parent.innerWidth() - $parent.width()) / 2;
  455. $pre.wrapAll('<div style="position: relative; padding-bottom: ' + padding + 'px;"></div>');
  456. } else {
  457. $pre.wrapAll('<div style="position: relative;"></div>');
  458. }
  459.  
  460. const $btn = $("<button class='copy-code-button s-btn s-btn__filled s-btn__xs'>" + self.strings.copy + "</button>");
  461. $btn.css({
  462. "position": "absolute",
  463. "top": "6px",
  464. "right": "12px",
  465. "display": "none"
  466. });
  467. $pre.append($btn);
  468.  
  469. let $container = $btn.siblings("code");
  470. $pre.hover(function () {
  471. $btn.css("display", "block");
  472. }, function () {
  473. $btn.css("display", "none");
  474. });
  475.  
  476. setTimeout(function () {
  477. if ($container.length == 0) {
  478. $pre.contents().filter(function () {
  479. return this.className !== "copy-code-button";
  480. }).wrapAll('<code style= "overflow-x: auto; padding: 0px;"></code>');
  481. $container = $btn.siblings("code").get(0);
  482. } else {
  483. $container = $container.get(0);
  484. }
  485. }, 0);
  486.  
  487. $btn.click(function () {
  488. self.selectElemText($container);
  489. const selectedText = self.getSelectedText();
  490. let buttonNewText = "";
  491. if (self.copyToClipboard(selectedText)) {
  492. buttonNewText = self.strings.copied;
  493. } else {
  494. buttonNewText = self.strings.canNotCopy;
  495. }
  496. window.getSelection().removeAllRanges();
  497. $(this).text(buttonNewText);
  498. const that = this;
  499. setTimeout(function () {
  500. $(that).text(self.strings.copy);
  501. }, 400);
  502. });
  503. });
  504. return this;
  505. }
  506. };
  507.  
  508. ruSO.init();
  509.  
  510. window.addEventListener('load', function () {
  511. ruSO
  512. .addButtons()
  513. .addAuthorQuestionsLinks()
  514. .addCopyToClipboard();
  515. }, false);