HWM_show_info

Подробная инфа артов/существ/навыков

// ==UserScript==
// @name            HWM_show_info
// @author          Мифист
// @namespace       Мифист
// @version         1.0.0
// @description     Подробная инфа артов/существ/навыков
// @match           https://www.heroeswm.ru/*
// @match           https://*.lordswm.com/*
// @exclude         */war.php*
// @exclude         */roulette.php*
// @run-at          document-end
// @grant           none
// @license         MIT
// @noframes
// ==/UserScript==

(function initModule(view) {
  'use strict';

  if (document.visibilityState === 'hidden') {
    const handler = () => initModule(view);
    document.addEventListener('visibilitychange', handler, { once: true });
    return;
  }

  if (document.readyState === 'loading') {
    const handler = () => initModule(view);
    document.addEventListener('DOMContentLoaded', handler, { once: true });
    return;
  }

  // ==========================

  const MODULE_NAME = 'HWM_show_info';
  const MODULE_VERSION = '1.0.0';

  const modules = (function(symbol) {
    return view[symbol] || (view[symbol] = {
      stack: new Map,
      has(key) { return this.stack.has(key); },
      delete(key) { return this.stack.delete(key); },
      get(key) { return this.stack.get(key); },
      add(key, version, exports) {
        if (this.stack.has(key)) return;
        this.stack.set(key, { version, exports });
      }
    });
  })(Symbol.for('__5781303__'));

  if (modules.has('HWM_auction_upd')) return;

  // ==========================

  let hideFrame = Function.prototype;
  const outerStyleSheet = document.createElement('style');

  const $ = (selector, ctx = document) => ctx.querySelector(selector);

  function fetch(url) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.responseType = 'document';

      xhr.onload = () => {
        if (xhr.status === 200) return resolve(xhr.response);
        reject(new Error(`Error status: ${xhr.status}`));
      };

      xhr.onerror = () => reject(new Error('ERR_INTERNET_DISCONNECTED'));

      xhr.send(null);
    });
  }

  // ==========================

  async function handleTarget(e) {
    const trg = e.target;
    const anchor = getElemAnchor(trg);

    if (!anchor) return;

    e.preventDefault();
    hideFrame();

    const self = InfoFrame.instances[anchor.flag];
    let data = self.cache.get(anchor.id);

    if (!document.contains(self.frame)) {
      if (!navigator.onLine) return onInternetDisconnect();
      await self.onConnect(self.getURL(anchor.id));
    }

    if (data == null) {
      data = await pullRequest(self, anchor.id);
      if (data == null) return;
    }

    self.setHide(trg);
    self.render(data);
    self.shell.classList.add('__shown');
    centerFrame(self, getClosestBlockElem(trg));

    if (self === ArmyInfo) self.initHints();
  }

  function onInternetDisconnect(err) {
    const msg = err ? err.message : 'ERR_INTERNET_DISCONNECTED';
    alert(`${msg}\nНет подключения к Интернету`);
  }

  function pullRequest(self, id) {
    return self.request(id)
      .then(data => self.cache.set(id, data).get(id))
      .catch(onInternetDisconnect);
  }

  // ==========================

  class InfoFrame {
    static __init__() {
      if (this !== InfoFrame) return;

      const instances = this.instances = [PerkInfo, ArtInfo, ArmyInfo];

      instances.forEach((self, ind) => {
        self.flag = ind;

        if (!$(self.selector)) return;

        self.cache = new Map;
        self.shell = document.createElement('div');
        self.frame = document.createElement('iframe');
        self.shell.classList.add(`${MODULE_NAME}-shell`);
        self.frame.classList.add(`${MODULE_NAME}-frame`);
      });

      if (instances.every(self => !self.hasOwnProperty('cache'))) return;

      outerStyleSheet.append(this.outerCSS);
      document.addEventListener('contextmenu', handleTarget);

      const destroyType = MODULE_NAME + '__destroy';
      const destroyHandler = this.__destroy__.bind(this);
      document.addEventListener(destroyType, destroyHandler, { once: true });

      modules.add(MODULE_NAME, MODULE_VERSION, this);
    }

    static __destroy__() {
      hideFrame();

      this.instances.splice(0).forEach(self => {
        if (!self.hasOwnProperty('cache')) return;
        self.cache.clear();
        self.shell.remove();
      });

      outerStyleSheet.remove();
      document.removeEventListener('contextmenu', handleTarget);
      modules.delete(MODULE_NAME);
    }

    static get outerCSS() {
      return /*css*/`
        .${MODULE_NAME}-shell {
          --w: 0;
          --h: 0;
          --x: 0;
          --y: 0;
          width: var(--w);
          height: var(--h);
          display: none;
          position: absolute;
          left: var(--x);
          top: var(--y);
          z-index: 100;
        }
        .${MODULE_NAME}-shell.__shown {
          display: block;
        }
        .${MODULE_NAME}-shell::before,
        .${MODULE_NAME}-shell::after {
          content: "";
          height: 5px;
          position: absolute;
          left: 0;
          right: 0;
          top: -5px;
        }
        .${MODULE_NAME}-shell::after {
          top: auto;
          bottom: -5px;
        }
        .${MODULE_NAME}-frame {
          width: 100%;
          height: 100%;
          display: block;
          border: none;
          outline: 2px solid #72787c;
          resize: none;
          overflow: hidden;
          user-select: none;
        }
        .hwm_hint_css,
        div[style^="z-index:1;top:0;right:0;"] {
          pointer-events: none;
        }
      `.replace(/^ +/gm, '');
    }

    static get frameView() {
      return this.frame.contentWindow;
    }

    static get frameDoc() {
      return this.frame.contentDocument;
    }

    static onConnect(url) {
      const {frame, shell} = this;

      if (!document.contains(outerStyleSheet)) {
        document.head.append(outerStyleSheet);
      }

      frame.src = url;
      shell.append(frame);
      document.body.prepend(shell);

      return new Promise(resolve => {
        frame.onload = () => {
          this.onFrameLoad();
          resolve();
        };
      });
    }

    static onFrameLoad() {
      const {frame, frameView, frameDoc} = this;
      const target = this.target = frameDoc.createElement('div');
      target.id = 'cont';

      clearAsyncQueue(frameView);
      frameView.setTimeout(() => clearAsyncQueue(frameView), 200);

      frameView.addEventListener('error', (e) => {
        console.log(e);
        alert(`@${MODULE_NAME} => ${this.name}:\n${e.message}`);
        if (this === ArmyInfo) this.shell.remove();
      });

      frameDoc.head.innerHTML = /*html*/`
        <base target="_parent">
        <style>${this.innerCCS}</style>
      `;
      frameDoc.body.replaceChildren(target);
    }

    static onFrameHide() {}

    static setHide(trg) {
      const {frame, shell, frameDoc} = this;

      const onKeyUp = (e) => void (e.key === 'Escape' && hide());

      const hide = hideFrame = () => {
        toggleHandlers(false);
        shell.classList.remove('__shown');
        shell.removeAttribute('style');
        hideFrame = Function.prototype;
        this.onFrameHide();
      };

      toggleHandlers(true);

      function toggleHandlers(force) {
        const method = (force ? 'add' : 'remove') + 'EventListener';
        document[method]('click', hide);
        document[method]('keyup', onKeyUp);
        frameDoc[method]('keyup', onKeyUp);
        shell[method]('mouseleave', hide);
        trg[method]('mouseleave', leave);
        trg[method]('click', preventClick, true);
      }

      function leave({type, relatedTarget}) {
        this.removeEventListener(type, leave);
        if (relatedTarget && !relatedTarget.contains(frame)) hide();
      }

      function preventClick(e) {
        e.preventDefault();
        e.stopPropagation();
        hide();
      }
    }

    static render(html) {
      this.target.innerHTML = html;
    }
  }

  // ==========================

  class PerkInfo extends InfoFrame {
    static get innerCCS() {
      return /*css*/`
        * {
          font-family: inherit;
          font-size: inherit;
          box-sizing: border-box;
        }
        :root {
          font-size: 10px;
        }
        body {
          font-family: Verdana, Arial, sans-serif;
          font-size: 1.3rem;
          line-height: 1.3;
          margin: 0;
          background-image: linear-gradient(45deg, #cdc9c0, #fff);
          overflow: hidden;
          user-select: none;
        }
        #cont {
          width: 60rem;
          display: flex;
          flex-wrap: wrap;
          align-items: flex-start;
          justify-content: flex-start;
          padding: 1rem;
        }
        #cont > h1 {
          font-size: 1.1em;
          width: 100%;
          margin: 0 0 1rem;
          color: #435970;
          text-transform: uppercase;
        }
        #cont > div {
          flex: 1;
          padding-left: 1rem;
        }
      `.replace(/^ +/gm, '');
    }

    static get selector() {
      return 'a[href*="showperkinfo."]';
    }

    static getURL(id) {
      return `/showperkinfo.php?name=${id}`;
    }

    static getItemId(el) {
      return new URLSearchParams(el.search).get('name');
    }

    static async request(id) {
      const responseDoc = await fetch(this.getURL(id));
      const img = $('img[src*="/perks/"]', responseDoc);
      let elem = img && img.closest('td');
      elem = elem && elem.nextElementSibling;

      if (!elem) return '';

      return /*html*/`
        <h1>${img.alt.slice(7)}</h1>
        <img src="${img.src}">
        <div>${elem.innerHTML.slice(20)}</div>
      `;
    }
  }

  // ==========================

  class ArtInfo extends InfoFrame {
    static get innerCCS() {
      return /*css*/`
        * {
          font-family: inherit;
          font-size: inherit;
          box-sizing: border-box;
        }
        :root {
          font-size: 10px;
        }
        body {
          font-family: Verdana, Arial, sans-serif;
          font-size: 1.3rem;
          line-height: 1.3;
          margin: 0;
          background-image: linear-gradient(45deg, #cdc9c0, #fff);
          overflow: hidden;
          user-select: none;
        }
        #cont {
          width: 74rem;
          max-height: 42rem;
          display: flex;
          position: relative;
          overflow-y: auto;
          scrollbar-width: thin;
        }
        .global_container_block_header {
          font-size: 1.1em;
          position: absolute;
          right: 2rem;
          top: 1rem;
        }
        .global_container_block_header h1 {
          line-height: normal !important;
          text-transform: uppercase;
          margin: 0;
        }
        .global_container_block_header b {
          color: #435970;
        }
        .art_info_left_block {
          padding: 2rem 1rem;
        }
        .s_art_prop_amount_icon {
          min-height: 2.8rem;
          display: flex;
          align-items: center;
          justify-content: center;
          color: #0a2b4b;
          background-image: linear-gradient(#eee, #bbc4b1);
          border: 1px solid #78878d;
        }
        .s_art_prop_amount_icon:hover {
          filter: saturate(1.5);
        }
        .s_art_prop_amount_icon img {
          width: 2rem;
          margin-right: .5rem;
        }
        .cre_mon_image1 {
          display: none;
        }
        .art_info_desc {
          padding: 3rem 1rem 1rem;
          background: transparent !important;
        }
        .rs {
          margin: 0 2px;
        }
        b {
          color: #332f2f;
        }
        i {
          color: #315473;
        }
        a[href*="=40#"] {
          font-weight: bold;
          font-style: normal;
          text-decoration: none;
        }
      `.replace(/^ +/gm, '');
    }

    static get selector() {
      return location.pathname === '/inventory.php'
        ? '.inv_art_outside'
        : 'a[href*="art_info."]';
    }

    static getURL(id) {
      return `/art_info.php?id=${id}`;
    }

    static getItemId(el) {
      if (el.tagName === 'A') return new URLSearchParams(el.search).get('id');

      const imgPathReg = /\/artifacts\/([^.]+)/;
      const getArtName = (html) => html.match(imgPathReg)[1];
      const artName = getArtName(el.outerHTML);

      const {arts = []} = view;
      const art = arts.find(art => getArtName(art.html) === artName) || {};
      return art.art_id;
    }

    static async request(id) {
      const responseDoc = await fetch(this.getURL(id));
      const elem = $('#set_mobile_max_width', responseDoc);
      return elem ? elem.innerHTML : '';
    }
  }

  // ==========================

  class ArmyInfo extends InfoFrame {
    static get innerCCS() {
      return /*css*/`
        * {
          box-sizing: border-box;
        }
        :root {
          font-size: 10px;
        }
        body {
          font-family: Verdana, Arial, sans-serif;
          font-size: 1.2rem;
          margin: 0;
          background-image: linear-gradient(45deg, #dad1be, #fff);
          overflow: hidden;
          user-select: none;
        }
        .hwm_hint_css {
          font-size: inherit !important;
          max-width: 34rem !important;
          position: fixed;
          display: none;
          padding: .4em .7em;
          color: #ddd;
          background-color: #3a3a3a;
          border: 2px solid #888;
          z-index: 2;
        }
        #cont {
          width: 70rem;
          display: flex;
          flex-wrap: wrap;
        }
        .info_header_content {
          width: 100%;
          height: 3.5em;
          display: flex;
          align-items: center;
          justify-content: center;
          background: #afc2d747;
          border-bottom: 1px solid #757575;
        }
        a:first-child {
          font-size: 1.6em;
          color: #506263;
          text-decoration: none;
        }
        .info_text_content {
          width: calc(100% - 20rem);
          display: flex;
          flex-wrap: wrap;
          border-right: 1px solid #757575;
        }
        .info_text_content > div {
          font-size: 1.1em;
          width: 50%;
          display: flex;
          align-items: center;
          column-gap: .5rem;
          padding: 0 1.4rem;
        }
        .info_text_content img {
          width: 2.4rem;
          height: auto;
        }
        .info_text_content div:last-child {
          margin-left: auto;
        }
        canvas,
        #show_army,
        .konvajs-content {
          width: 20rem !important;
          height: 20rem !important;
        }
        .army_info_skills {
          font-size: 1.1em;
          width: 100%;
          display: flex;
          flex-wrap: wrap;
          column-gap: 0.4em;
          padding: 1rem 1.4rem;
          border-top: 1px solid #757575;
        }
        .army_info_skills > div {
          font-weight: bold;
          margin-right: -0.5em;
        }
        .army_info_skills > span:hover {
          color: brown;
          cursor: help;
        }
      `.replace(/^ +/gm, '');
    }

    static get selector() {
      return 'a[href*="army_info."]';
    }

    static getURL(id) {
      return `/army_info.php?name=${id}`;
    }

    static getItemId(el) {
      return new URLSearchParams(el.search).get('name');
    }

    static async request(id) {
      const responseDoc = await fetch(this.getURL(id));
      const linkHTML = `<a href="${this.getURL(id)}">$1</a>`;
      const html = $('.army_info', responseDoc).innerHTML.trim()
        .replaceAll('\n', '')
        .replace(/\s{2,}/g, ' ')
        .replace(' style="display: show;"', '')
        .replaceAll('> ', '>')
        .replaceAll(' width="48" height="48" alt="" title=""', '')
        .replace(/<div><h1 [^>]+>(.+?)<\/h1><\/div>/, linkHTML)
        .replace(/<div(?:><img| class="corner).+?div>/g, '')
        .replaceAll(' class="scroll_content_half"', '');

      const reg = /info\((.+?)\);/;
      const script = [...responseDoc.scripts].pop();
      const paramsStr = (script.text.match(reg) || ['', ''])[1];

      return [html, paramsStr];
    }

    static render([html, paramsStr]) {
      super.render(html);

      const {frameView} = this;
      const params = new Function(`return [${paramsStr}]`)();
      frameView.setTimeout(() => frameView.init_army_info(...params));
    }

    static onFrameLoad() {
      super.onFrameLoad();

      const {frameView, target} = this;
      const hwmHint = frameView.hwm_hint;

      if (!(hwmHint instanceof frameView.HTMLElement)) return;

      target.after(hwmHint);
    }

    static onFrameHide() {
      const {frameView: ctx} = this;
      const stages = ctx.Konva && ctx.Konva.stages || [];
      stages.splice(0).forEach(stage => ctx.clearInterval(stage.interval));
    }

    static initHints() {
      const initHwmHints = this.frameView.hwm_hints_init;
      if (typeof initHwmHints === 'function') initHwmHints();
    }
  }

  // ==========================

  InfoFrame.__init__();

  // ==========================

  function centerFrame({target, shell}, elem) {
    const {offsetHeight, offsetWidth} = target;
    const {left, right, top, bottom} = elem.getBoundingClientRect();
    const offset = 5;
    const halfw = offsetWidth / 2;
    const height = offsetHeight + offset;
    const maxX = document.documentElement.clientWidth - offset;
    const centerX = left + (right - left) / 2;

    const x = Math.max(offset, Math.min(centerX - halfw, maxX - offsetWidth));
    const y = (bottom + height < view.innerHeight || top - height <= 0)
      ? bottom + offset
      : top - height;

    const setCSS = shell.style.setProperty.bind(shell.style);
    setCSS('--w', `${offsetWidth >> 0}px`);
    setCSS('--h', `${offsetHeight >> 0}px`);
    setCSS('--x', `${x + view.scrollX >> 0}px`);
    setCSS('--y', `${y + view.scrollY >> 0}px`);
  }

  function getElemAnchor(el) {
    const propName = `__cachedInfoFrameAnchor`;
    if (el.hasOwnProperty(propName)) return el[propName];

    const set = (val = null) => (el[propName] = val);
    const artElem = el.closest(ArtInfo.selector);
    const perkElem = artElem ? null : el.closest(PerkInfo.selector);
    const elem = artElem || perkElem || el.closest(ArmyInfo.selector);

    if (!elem) return set();

    const self = perkElem ? PerkInfo : artElem ? ArtInfo : ArmyInfo;
    const id = self.getItemId(elem);

    return !id ? set() : set({ flag: self.flag, id });
  }

  function getClosestBlockElem(elem) {
    if (elem.offsetWidth && elem.offsetHeight) return elem;
    return getClosestBlockElem(elem.parentNode);
  }

  function clearAsyncQueue(ctx) {
    let i = ctx.setTimeout(0);
    while (i) ctx.clearTimeout(i--);
  }
})(document.defaultView);