POE Trade Improvements

Improvements to the trade site. Adds a button for "add resistances" and "add attributes" to create stat weight groups

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         POE Trade Improvements
// @namespace    Kylixen
// @version      2025-01-27
// @description  Improvements to the trade site. Adds a button for "add resistances" and "add attributes" to create stat weight groups
// @author       Kylixen
// @match        https://www.pathofexile.com/trade2/search/poe2/Standard
// @match        https://www.pathofexile.com/trade2/search/poe2/Standard/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pathofexile.com
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

// debugger;

var typingSpeed = 100; // milliseconds per character
async function simulateTyping(element, text, delay = 100) {
  let index = 0;

  function fireEvent(eventType, key) {
    const event = new KeyboardEvent(eventType, {
      key: key,
      code: key.charCodeAt(0),
      bubbles: true,
      cancelable: true
    });
    element.dispatchEvent(event);
  }

  function fireInputEvent() {
    const inputEvent = new InputEvent("input", {
      bubbles: true,
      cancelable: true
    });
    element.dispatchEvent(inputEvent);
  }

  function fireChangeEvent() {
    const inputEvent = new InputEvent("change", {
      bubbles: true,
      cancelable: true
    });
    element.dispatchEvent(inputEvent);
  }

  async function typeNextChar() {
    if (index < text.length) {
      const char = text[index];
      fireEvent("keydown", char);
      fireEvent("keypress", char);

      if (element.isContentEditable) {
        element.textContent += char;
      } else {
        element.value += char;
      }

      fireInputEvent();
      fireEvent("keyup", char);

      index++;
      await setTimeout(null, delay)
      await typeNextChar();
    } else {
      fireChangeEvent();
    }
  }

  await typeNextChar();
}

function errorOnUndef(f, errorMessage) {
  return function (...args) { 
    var res = f(...args)
    if(res === undefined)
      throw errorMessage
    return res;
  }
}

function errorHandled(f) {
  return function(...args) {
    try {
      return f(...args);
    } catch(error) {
      console.error(error);
    }
  }
}

function asyncErrorOnUndef(f, errorMessage) {
  return async function (...args) { 
    var res = await f(...args)
    if(res === undefined)
      throw errorMessage
    return res;
  }
}

function asyncErrorHandled(f) {
  return async function(...args) {
    try {
      return await f(...args);
    } catch(error) {
      console.error(error);
    }
  }
}

function first(iterable) {
  if(!iterable || iterable.length == 0)
    return undefined;
  return iterable[0];
}

function last(iterable) {
  if(!iterable || iterable.length == 0)
    return undefined; 
  return iterable[iterable.length -1 ];
}

const getAddStatGroupDiv = errorOnUndef(function() {
  return document.querySelector(".filter-group-select")
}, "Couldn't find the +Add Stat Group <div>");

async function addNewStatGroup(statGroupType) {

  var divAddStatGroup = getAddStatGroupDiv();

  var options = Array.from(divAddStatGroup.querySelectorAll("li>span") || []);

  if(!options)
    throw "Couldn't find options in Add Stat Group div";

  var groupMatch = new RegExp(`^\s*${statGroupType}\s*$`, "i");

  var option = first(options.filter(function(opt) {
    if(groupMatch.test(opt.textContent))
      return true;
    return false;
  }))

  if(!option)
    throw "Couldn't find the stat group type " + statGroupType;

  option.click();

  await setTimeout(null, 3);

  return last(document.querySelectorAll("div.filter-group"));
};

const addWeightedResistances = asyncErrorHandled(async function() {

  // Add the new panel
  const statGroupDiv = await addNewStatGroup("weighted sum v2");

  // type "resistance" into the input so we get all the elements we need)
  const statFilterDiv = last(document.querySelectorAll("div.filter-select-mutate"));
  const statInput = statFilterDiv.querySelector("input");

  statInput.focus();
  await simulateTyping(statInput, "resistance", 0);

  // click all the resistances
  statFilterDiv.querySelectorAll("span.multiselect__option").forEach((optionSpan) => {
    if(!((optionSpan.textContent || "").match(/^(explicit|implicit) #% to (Cold|Fire|Lightning|Chaos|All Elemental) resistances?$/i)))
      return;
    console.debug(optionSpan.textContent);
    optionSpan.click();
  });

  await setTimeout(null, 6);

  // Set the weight for all/chaos
  statGroupDiv.querySelectorAll("span.filter-body:has(.weight)").forEach(async (optionSpan) => {
    if((optionSpan.innerText || "").match(/All Elemental/i)) {
      const weightInput = optionSpan.querySelector("input.weight");
      weightInput.focus();
      await simulateTyping(weightInput, "3", 0);
      await setTimeout(null, 6);
      weightInput.blur();
    }
  });
});

const addWeightedAttributes = asyncErrorHandled(async function() {

  // Add the new panel
  const statGroupDiv = await addNewStatGroup("weighted sum v2");

  // type "resistance" into the input so we get all the elements we need)
  const statFilterDiv = last(document.querySelectorAll("div.filter-select-mutate"));
  const statInput = statFilterDiv.querySelector("input");

  // statInput.focus();
  // await simulateTyping(statInput, "resistance", 0);

  // click all the resistances
  statFilterDiv.querySelectorAll("span.multiselect__option").forEach((optionSpan) => {
    if(!((optionSpan.textContent || "").match(/^(explicit|implicit) #% to (Cold|Fire|Lightning|Chaos|All Elemental) resistances?$/i)))
      return;
    console.debug(optionSpan.textContent);
    optionSpan.click();
  });

  await setTimeout(null, 6);

  // Set the weight for all/chaos
  statGroupDiv.querySelectorAll("span.filter-body:has(.weight)").forEach(async (optionSpan) => {
    if((optionSpan.innerText || "").match(/All Elemental/i)) {
      const weightInput = optionSpan.querySelector("input.weight");
      weightInput.focus();
      await simulateTyping(weightInput, "3", 0);
      await setTimeout(null, 6);
      weightInput.blur();
    }
  });

});
;
function addResistancesClick() {
  addWeightedResistances();
  // Clean up button
}

function addAttributesClick() {
  addWeightedAttributes();
  // Clean up button
}

function waitFor(waitForFunction, successCallback) {
  (new MutationObserver(function (changes, observer) {
    if (!waitForFunction())
      return;
    observer.disconnect();
    successCallback();
  })).observe(document, { childList: true, subtree: true });
}

(function () {
  'use strict';
  (new MutationObserver(function (changes, observer) {
    if (!document.querySelector('.filter-group-select'))
      return;
    observer.disconnect();
    onLoad();
  })).observe(document, { childList: true, subtree: true });

  function onLoad() {
    const searchDiv = document.querySelector("div.search-advanced-pane");
    const newElement = document.createElement('div');
    newElement.className = "filter-group expanded";
    newElement.innerHTML = `
    <div class="filter-group-body">
      <button id='add-resistance-button' type="button" class='btn search-btn' onclick='addResistancesClick()'>
          <span>Add Resistances</span>
      </button>
      <button id='add-attributes-button' type="button" class='btn search-btn' onclick='addAttributesClick()'>
          <span>Add Attributes</span>
      </button>
    </div>
    `;
    searchDiv.prepend(newElement);
  }
})();

window.addResistancesClick = addResistancesClick;
window.addAttributesClick = addAttributesClick;

window.ky = {
  addResistancesClick,
  addWeightedResistances,
  addWeightedAttributes,
  addNewStatGroup,
  simulateTyping,
  last,
  first,
}