Skip to content

Instantly share code, notes, and snippets.

@eugeny-dementev
Last active February 19, 2025 10:33
Show Gist options
  • Save eugeny-dementev/c9c61e7a5d1e2b2e35f2402febabb110 to your computer and use it in GitHub Desktop.
Save eugeny-dementev/c9c61e7a5d1e2b2e35f2402febabb110 to your computer and use it in GitHub Desktop.
Path of Exile 2 Trade Copy Item Button
// ==UserScript==
// @name PoE2 Trade Item Copy
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description Enable copy item button on https://www.pathofexile.com/trade2/search/poe2
// @author Eugeny Dementev
// @match https://www.pathofexile.com/trade2*
// @icon https://www.google.com/s2/favicons?sz=64&domain=pathofexile.com
// @grant none
// ==/UserScript==
(function () {
'use strict';
// Set up the MutationObserver for each item row appearing on trade site
const observer = new MutationObserver((mutationsList) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
// Check if the added node is a <div class="row">
if (node.nodeType === 1 && node.matches('div.row')) {
showCopyItemButton(node);
}
});
}
});
});
// Start observing the entire document body
observer.observe(document.body, {
childList: true, // Observe direct child additions/removals
subtree: true, // Observe all descendants
});
}
)();
const SEPARATOR = '--------';
function extractItemClass(middleElem) {
return middleElem.querySelector('.property .lc').textContent.trim();
}
function extractItemName(middleElem) {
return middleElem.querySelector('.itemName .lc').textContent.trim();
}
function extractItemBase(middleElem) {
return middleElem.querySelector('.itemName.typeLine .lc').textContent.trim();
}
function extractItemRarity(middleElem) {
const itemPopupContainer = middleElem.querySelector('.itemPopupContainer');
if (itemPopupContainer.classList.contains('uniquePopup')) {
return 'Unique';
} else if (itemPopupContainer.classList.contains('rarePopup')) {
return 'Rare';
} else if (itemPopupContainer.classList.contains('magicPopup')) {
return 'Magic';
} else if (itemPopupContainer.classList.contains('normalPopup')) {
return 'Normal';
} else {
return 'Unknown';
}
}
const ANY_WEAPON_CLASSES = [ // have phys/chaos damage, crit chance and attack per speed
'Bow',
'Crossbow',
'Axe',
'One Hand Mace',
'Two Hand Mace',
'Flail',
'Quarterstaff',
'Spear',
'Claw',
'Dagger',
// 'Trap', // not exactly an attack weapon
'Sword',
];
const ARMOUR_CLASSES = [
'Body Armour',
'Gloves',
'Helmets',
'Boots',
];
const SCEPTRE_CLASSES = [
'Sceptre',
];
class Item {
// all items has more or less the same section for them
// - Base (item class, rarity, name, base)
// - Item Base stats (evasion, armour, energy shield, spirit, quality)
// - Requirements, level, dex, int, str
// - (optional) Amount of sockets in item
// - Item Level (separate section specifically for item level)
// - (Optional) Enchant from corrupt (enchant)
// - (Optional) Runes effects (rune)
// - (Optional) Implicit mod (implicit)
// - Mods section, prefixes and suffixes
// - (Optional) Item description (only for items with instruction on how to use and unique items)
// - Corrupt/Mirror tag
getSections() {
return [
this.getBaseInfoSection()?.trim(),
this.getBaseStatsSection()?.trim(),
this.getRequirementsSection()?.trim(),
this.getAmountOfSocketsSection()?.trim(),
this.getItemLevelSection()?.trim(),
this.getEnchantSection()?.trim(),
this.getRunesEffectSection()?.trim(),
this.getImplicitModSection()?.trim(),
this.getModsSection()?.trim(),
this.getDescriptionSection()?.trim(),
this.getCorruptMirrorSection()?.trim(),
].filter(v => Boolean(v));
}
toString() {
return this
.getSections()
.join(`\n${SEPARATOR}\n`);
}
middleElem
itemClass
itemRarity
itemName
itemBase
itemQuality
itemLvlRequirement
itemDexRequirement
itemIntRequirement
itemStrRequirement
itemMods // array of text lines
constructor(middleElem) {
this.middleElem = middleElem;
this.extractBaseInfo();
this.extractRequirements();
}
extractBaseInfo() {
this.itemClass = extractItemClass(this.middleElem);
this.itemRarity = extractItemRarity(this.middleElem);
this.itemName = extractItemName(this.middleElem);
this.itemBase = extractItemBase(this.middleElem)
this.itemQuality = this.middleElem.querySelector('[data-field="quality"]')?.textContent.trim() || undefined;
this.itemImplicitMod = this.middleElem.querySelector('.implicitMod .lc.s')?.textContent.trim() || undefined;
this.itemMods = this.extractItemMods();
}
extractItemMods() {
return Array.from(this.middleElem.querySelectorAll('.explicitMod .lc.s')).map(modElem => modElem.textContent.trim());
}
getBaseInfoSection() {
return [
`Item Class: ${this.itemClass}`,
`Rarity: ${this.itemRarity}`,
this.itemName,
this.itemBase,
].join('\n');
}
getDescriptionSection() { }
getCorruptMirrorSection() {
const corrupted = this.middleElem.querySelector('.unmet')?.textContent.trim();
if (corrupted) {
return corrupted;
}
const mirrored = this.middleElem.querySelector('.augmented')?.textContent.trim();
if (mirrored) {
return mirrored;
}
}
getBaseDataField(selector) {
const selectedElem = this.middleElem.querySelector(selector)
let elemeTextValue = selectedElem?.textContent.trim() || undefined;
if (!elemeTextValue) {
return;
}
if (selectedElem.querySelector('.colourAugmented')) {
elemeTextValue = `${elemeTextValue} (augmented)`;
}
return elemeTextValue;
}
getElemDamageDataField() {
const selectedElem = this.middleElem.querySelector('[data-field="edamage"]');
const elemDamage = selectedElem?.textContent.trim() || undefined;
if (!elemDamage) {
return;
}
return `${elemDamage} (augmented)`;
}
getRadiusDataField() {
const selectedElems = this.middleElem.querySelectorAll('.property .lc');
if (selectedElems.length < 1) {
return;
}
const radiusPropElem = Array.from(selectedElems).find(elem => elem.textContent.includes('Radius'))
if (!radiusPropElem) {
return;
}
let elemeTextValue = radiusPropElem.textContent;
if (radiusPropElem.querySelector('.colourAugmented')) {
elemeTextValue = `${elemeTextValue} (augmented)`;
}
return elemeTextValue;
}
getLimitDataField() {
const selectedElems = this.middleElem.querySelectorAll('.property .lc');
if (selectedElems.length < 1) {
return;
}
const limitedToPropElem = Array.from(selectedElems).find(elem => elem.textContent.includes('Limited to'))
if (!limitedToPropElem) {
return;
}
let elemeTextValue = limitedToPropElem.textContent;
if (limitedToPropElem.querySelector('.colourAugmented')) {
elemeTextValue = `${elemeTextValue} (augmented)`;
}
return elemeTextValue;
}
getBaseStatsSection() {
const physicalDamage = this.getBaseDataField('[data-field="pdamage"]');
const elementalDamage = this.getElemDamageDataField();
const chaosDamage = this.getBaseDataField('[data-field="cdamage"]');
const critChance = this.getBaseDataField('[data-field="crit"]');
const attacksPerSecond = this.getBaseDataField('[data-field="aps"]');
const armour = this.getBaseDataField('[data-field="ar"]');
const evasion = this.getBaseDataField('[data-field="ev"]');
const energyShield = this.getBaseDataField('[data-field="es"]');
const spirit = this.getBaseDataField('[data-field="spirit"]');
const limitedTo = this.getLimitDataField();
const radius = this.getRadiusDataField();
return [
this.itemQuality && `${this.itemQuality} (augmented)`,
physicalDamage,
elementalDamage,
chaosDamage,
critChance,
attacksPerSecond,
armour,
evasion,
energyShield,
spirit,
limitedTo,
radius,
].filter(v => Boolean(v)).join('\n');
}
getRequirementsSection() {
return [
this.itemLvlRequirement,
this.itemStrRequirement,
this.itemDexRequirement,
this.itemIntRequirement,
].filter(v => Boolean(v)).join('\n');
}
getAmountOfSocketsSection() {
const countSockets = this.middleElem.parentElement.querySelector('.left').querySelectorAll('.socket.socket--rune').length;
const value = new Array(countSockets).fill('S').join(' ');
if (value.length === 0) {
return;
}
return `Sockets: ${value}`;
}
getItemLevelSection() {
return this.middleElem.querySelector('[data-field="ilvl"]')?.textContent.trim();
}
getEnchantSection() {
const enchantMods = Array
.from(this.middleElem.querySelectorAll('.enchantMod .lc.s'))
.map(enchantModElem => enchantModElem.textContent.trim())
.map(enchantMod => `${enchantMod} (enchant)`);
return enchantMods.join('\n');
}
getRunesEffectSection() {
const runeMods = Array
.from(this.middleElem.querySelectorAll('.runeMod .lc.s'))
.map(runeModElem => runeModElem.textContent.trim())
.map(mod => `${mod} (rune)`);
return runeMods.join('\n');
}
getModsSection() {
return this.itemMods.join('\n')
}
getImplicitModSection() {
const implicitMods = Array
.from(this.middleElem.querySelectorAll('.implicitMod .lc.s'))
.map(implicitModElem => implicitModElem.textContent.trim())
.map(implicitMod => `${implicitMod} (implicit)`);
return implicitMods.join('\n');
}
extractRequirements() {
this.itemLvlRequirement = this.middleElem.querySelector('[data-field="lvl"]')?.textContent.split(' ').join(': ') || undefined;
this.itemDexRequirement = this.middleElem.querySelector('[data-field="dex"]')?.textContent.split(' ').reverse().join(': ') || undefined;
this.itemIntRequirement = this.middleElem.querySelector('[data-field="int"]')?.textContent.split(' ').reverse().join(': ') || undefined;
this.itemStrRequirement = this.middleElem.querySelector('[data-field="str"]')?.textContent.split(' ').reverse().join(': ') || undefined;
}
}
function showCopyItemButton(row) {
const copyButton = row.querySelector('button.copy')
if (copyButton && copyButton.classList.contains('hidden')) {
copyButton.classList.remove('hidden');
copyButton.style = undefined
copyButton.onclick = () => {
const middleElem = row.querySelector('div.middle');
const itemInfoNew = extractItemDetails2(middleElem);
console.log(itemInfoNew);
navigator
.clipboard
.writeText(itemInfoNew)
.catch((err) => {
console.log('Failed to copy item');
console.error(err);
})
}
}
}
function getItemClass(itemClass) {
return Item;
}
function extractItemDetails2(middleElem) {
const item = new Item(middleElem);
return `${item}`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment