// ==UserScript== // @name ヨドバシ検索結果で量あたり単価を表示 // @description Shift+A/Shift+B:量あたり単価上限で絞り込み .:価格上限入力フォームにフォーカス // @match *://*.yodobashi.com/* // @version 0.2.5 // @grant none // @namespace https://greasyfork.org/users/181558 // @downloadURL none // ==/UserScript== (function() { const ddg_google_ratio = 0.0; // 0-1 const debug = 0; //Math.random() > 0.8; // trueでデバッグモード1 const debug2 = 0; //Math.random() > 0.8; // trueでデバッグモード2 const enableBeta = 1; // 1でポイント還元後価格(想像)を表示 var gStaY = 0; function sta(str, pointer = 0) { // 右下ステータス表示 return $('' + str + '').appendTo(document.body); } if (debug) sta("debug1"); if (debug2) sta("debug2"); var isorder = location.href.match(/https?:\/\/order\./); var issearch = location.href.match(/[\?\&]word\=/); var iscate = location.href.match(/category\//); var isproduct = location.href.match(/\/product\//); var parentLimit = isorder ? 5 : 4; const titleXPath = '//div[@class="pName"]/p[2]|.//h1[@id="products_maintitle"]/span|.//span[@class="js_c_commodityName"]|.//a[@id="LinkProduct01"]|.//div[@class="product js_productName"]|.//a[@class="js_productListPostTag js-clicklog js-taglog-schRlt"]/p[2]'; const priceXPath = '//span[@class="productPrice"]|.//span[@id="js_scl_unitPrice"]|.//div[@class="price red"]/strong|.//li[@class="Special"]/em|.//span[@class="red js_ppSalesPrice"]'; const pointrateXPath = '//span[@class="spNone"]|.//span[@id="js_scl_pointrate"]|.//div[@class="point orange"]|.//li[@class="Point"]|.//span[@class="orange js_ppPoint"]|.//div[@class="pInfo liMt05"]/ul/li/span[@class="orange ml10"]'; var cppLimit = [0, 0]; function inputcpplimit(e, type, autonumber = null) { e.stopPropagation(); e.preventDefault(); var ret = proInput("量あたり価格上限を入力してください", autonumber || cppLimit[type]); if (ret === null || ret == cppLimit[type]) return false; cppLimit[type] = ret; sessionStorage.setItem("cppLimit" + type, cppLimit[type] || "") || 0; location.reload(); return false; } if (issearch || iscate) { cppLimit[1] = sessionStorage.getItem("cppLimit1") || 0; if (cppLimit[1]) $(sta("limit1(Shift+A): " + cppLimit[1], 1)).appendTo(document.body).attr("title", "クリックかShift+Aでこの量単価の上限で絞り込む").click(e => inputcpplimit(e, 1)); cppLimit[2] = sessionStorage.getItem("cppLimit2") || 0; if (cppLimit[2]) $(sta("limit2(Shift+B): " + cppLimit[2], 1)).appendTo(document.body).attr("title", "クリックかShift+Bでこの量単価の上限で絞り込む").click(e => inputcpplimit(e, 2)); document.addEventListener("keydown", function(e) { if (/input|textarea/i.test(e.target.tagName)) return; var pressed = (e.ctrlKey ? 'c-' : '') + (e.altKey ? 'a-' : '') + (e.shiftKey ? 's-' : '') + String(e.key); if (pressed == "s-A") inputcpplimit(e, 1); // shift+a 量あたり価格上限 if (pressed == "s-B") inputcpplimit(e, 2); // shift+b 量あたり価格上限 }, false); } // .キーで上限絞り込みにフォーカス、全選択状態 $(document).keypress((e) => { if (/input|textarea/i.test(e.target.tagName)) return; if (String(e.key) == ".") { var ele = eleget0('//input[@id="js_upperPrice"]'); e.preventDefault(); if (ele) { ele.focus(); $(ele).select(); ele.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); } } }); $('input#js_upperPrice').css('ime-mode', 'inactive'); // 上限価格はIME offにする const niwait = 100; //50; setTimeout(() => { run(document); }, niwait); document.body.addEventListener('DOMNodeInserted', function(evt) { setTimeout(() => { run(evt.target); }, niwait); }, false); function run(node) { for (let titleEle of elegeta(titleXPath, node)) with({ ppr: ppr, ppr2: ppr2 }) { var rndcolor = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6); var title = titleEle.innerText if (titleEle.dataset.yCpP) continue; else titleEle.dataset.yCpP = "1"; debugEle(titleEle, rndcolor); var parentEle = titleEle; if (issearch || isproduct) { // 「店頭でのみ販売しています|予定数の販売を終了しました|販売を終了しました」を非表示 for (let ele of elegeta('//div[@class="pInfo"]/ul/li', node)) { if (ele.innerText.match(/店頭でのみ販売しています|販売を終了しました/)) { debugRemove(ele.parentNode.parentNode.parentNode.parentNode); continue; } } } for (var i = 0; i < parentLimit; i++) { parentEle = parentEle.parentNode; let f = elegeta(priceXPath, parentEle).length; if (f == 1) break; if (f > 1) i = parentLimit + 1; } if (i > parentLimit) continue; debugEle(parentEle, rndcolor); if (i == parentLimit) continue; if ((issearch || isproduct || iscate) && ((parentEle.parentNode.innerText)).match(/内蔵SSD|内蔵ハードディスク|PCパーツ>CPU|PCパーツ>グラフィックボード|USBメモリ/)) { // userbenchmarkリンク var ele = parentEle.parentNode.insertBefore(document.createElement("a"), parentEle.nextSibling); var iflsite = (Math.random() > ddg_google_ratio) ? "https://duckduckgo.com/?q=!ducky+" : "https://www.google.com/webhp?#btnI=I&q="; ele.innerHTML += ' UserBenchmark'; } for (let site of [ ["kopfhoerer.com", "www.kopfhoerer.com", "#137db0", /用ヘッドセット|型ヘッドホン|Bluetooth対応ヘッドホン|ゲーミングヘッドセット|Bluetoothヘッドセット|イヤホンマイク>3.5mmミニプラグ|インナーイヤーヘッドホン|ヘッドセット・ヘッドホン|ヘッドホン>完全ワイヤレスイヤホン/], ["Kopfhoerer.de", "www.kopfhoerer.de", "#2b2a3a", /型ヘッドホン|Bluetooth対応ヘッドホン|インナーイヤーヘッドホン|ヘッドセット・ヘッドホン|ヘッドホン>完全ワイヤレスイヤホン/], ["RTINGS", "www.rtings.com", "#609070", /用ヘッドセット|型ヘッドホン|Bluetooth対応ヘッドホン|ゲーミングヘッドセット|Bluetoothヘッドセット|イヤホンマイク>3.5mmミニプラグ|インナーイヤーヘッドホン|ヘッドセット・ヘッドホン|ヘッドホン>完全ワイヤレスイヤホン/], ["TFT CENTRAL", "www.tftcentral.co.uk", "#2e2e2e", />ディスプレイ・モニター/] ]) { if ((issearch || isproduct || iscate) && ((parentEle.parentNode.innerText)).match(site[3])) { // RTINGS/kopfhoererリンク var ele = parentEle.parentNode.insertBefore(document.createElement("a"), parentEle.nextSibling); var iflsite = (Math.random() > ddg_google_ratio) ? "https://duckduckgo.com/?q=!ducky+" : "https://www.google.com/webhp?#btnI=I&q="; ele.innerHTML += '' + site[0] + ''; } } var priceEle = eleget0(priceXPath, parentEle); if (!priceEle) continue; debugEle(priceEle, rndcolor); var price = Number(priceEle.innerText.match(/\D([0-9\,]+)/)[1].replace(/\,/g, "")); var pointEle = eleget0(pointrateXPath, parentEle); if (pointEle) { var pointtext = pointEle.innerText.replace(/\,/g, ""); if (pointtext.match(/([0-9]+)(?:%)/)) { debugEle(pointEle, rndcolor); var pointPer = Number(pointtext.match(/([0-9,]+)(?:%)/)[1] / 100); } else if (pointtext.match(/([0-9]+)(?:ポイント)/)) { debugEle(pointEle, rndcolor); var pointPer = Number(pointtext.match(/([0-9]+)(?:ポイント)/)[1] / price); /* if (debug) */ pointEle.innerHTML += "(" + Math.round(pointPer * 100) + "%?)"; } if (pointPer) { var point = Math.ceil(price - (price * (price / (price + price * pointPer)))); var pricef = Math.round(price - point).toLocaleString(); if (enableBeta) priceEle.innerHTML += " " + (isorder ? "
還元後:" : "(還元後:") + "¥" + pricef + (isorder ? "" : ")") + "
"; } } else { var point = -1; } // type1 var pass1 = 0; if (title.match(/ゴミ袋|ポリ袋/) || ((isproduct || issearch) && ((parentEle.innerText)).match(/ゴミ袋|ポリ袋/))) var ryou = title.replace(/\,/g, "").match(/\D([0-9\.]+)(P)/); else if (title.match(/ラップ/) || ((isproduct || issearch) && ((parentEle.innerText)).match(/ラップ/))) var ryou = title.replace(/\,/g, "").match(/\D([0-9\.]+)(m)/); else if (title.match(/USBメモリ|(外付け|外付|ポータブル|内蔵|バルク|接続)(SSD|HDD|ハードディスク)|バルクドライブ|2\.5.?(inc|インチ)|7mm|9.5mm/) || ((isproduct || issearch) && ((parentEle.innerText)).match(/(内蔵|外付け|ポータブル)(SSD|HDD|ハードディスク)/))) var ryou = title.replace(/\,/g, "").match(/\D([0-9\.]+)(mg|㎎|g|ml|mL|ml|GB)|(?:[^A-Z0-9\.\-])([0-9\.]+)(L|kg|㎏|Kg|TB)/); else var ryou = title.replace(/\,/g, "").match(/\D([0-9\.]+)(mg|㎎|g|ml|mL|ml)|(?:[^A-Z0-9\.\-])([0-9\.]+)(L|kg|㎏|Kg)/); if (ryou && (ryou[1] > 0 || ryou[3] > 0)) { if (ryou[4]) ryou[4] = ryou[4].replace(/kg|㎏|Kg/, "g").replace(/L/, "ml").replace(/TB/, "GB"); var ryout = Number(ryou[1]) || Number(ryou[3]) * 1000; var mul = (title.match(/×[0-9\.\,]+/m) && !(title.match(/[\((\[].*×.*[\))\]]/m)) && !(title.match(/×[\d\s]*(cm|mm)/))) ? Number(title.match(/×([0-9\.\,]+)/)[1]) : 1; if (point > -1) { var ppr = Math.round(100 * (price - point) / Number(ryout * mul)) / 100; var ele = $('¥' + ppr + '/' + (ryou[2] || ryou[4]) + '').appendTo(titleEle); if ((!isproduct) && (!isorder)) $(ele).css("cursor", "pointer").attr("title", "クリックかShift+Aでこの量単価の上限で絞り込む").click(e => inputcpplimit(e, 1, ppr)); pass1 = ((iscate || issearch) && cppLimit[1] && ppr <= cppLimit[1]) ? 1 : 0; } } // type2 var pass2 = 0; var ryou1 = ryou; var ryou = title.replace(/\,/g, "").match(/\D([0-9\.]+)(枚|粒|錠|包|杯|本|個|袋|組入|ポート|色|日分|ヶ入|食|巻(入|セット|缶|函))/); if (ryou && (ryou[1] > 0 || ryou[3] > 0)) { if (ryou[4]) ryou[4] = ryou[4].replace(/kg|㎏|Kg/, "g").replace(/L/, "ml").replace(/TB/, "GB"); var ryout = Number(ryou[1]) || Number(ryou[3]) * 1000; var mul = 1; //(title.match(/×[0-9\.\,]+/m) && !(title.match(/[\((\[].*×.*[\))\]]/m)) && !(title.match(/×[\d\s]*(cm|mm)/)))?Number(title.match(/×([0-9\.\,]+)/)[1]):1; if (point > -1) { var ppr2 = Math.round(100 * (price - point) / Number(ryout * mul)) / 100; var ele = $('¥' + ppr2 + '/' + (ryou[2] || ryou[4]) + '').appendTo(titleEle); if ((!isproduct) && (!isorder)) $(ele).css("cursor", "pointer").attr("title", "クリックかShift+Bでこの量単価の上限で絞り込む").click(e => inputcpplimit(e, 2, ppr2)); pass2 = ((iscate || issearch) && cppLimit[2] && ppr2 <= cppLimit[2]) ? 1 : 0; } } if ((cppLimit[1] > 0 && pass1 == 0) || (cppLimit[1] > 0 && (!ryou1))) debugRemove(parentEle.parentNode); if ((cppLimit[2] > 0 && pass2 == 0) || (cppLimit[2] > 0 && (!ryou))) debugRemove(parentEle.parentNode); } } function elegeta(xpath, node = document) { var ele = document.evaluate("." + xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); var array = []; for (var i = 0; i < ele.snapshotLength; i++) array[i] = ele.snapshotItem(i); return array; } function eleget0(xpath, node = document) { var ele = document.evaluate("." + xpath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); return ele.snapshotLength > 0 ? ele.snapshotItem(0) : ""; } function proInput(prom, defaultval, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { var inp = window.prompt(prom, defaultval); if (inp === undefined || inp === null) return inp; return Math.min(Math.max(Number(inp.replace(/[A-Za-z0-9.]/g, function(s) { return String.fromCharCode(s.charCodeAt(0) - 65248); }).replace(/[^-^0-9^\.]/g, "")), min), max); } function debugEle(ele, col) { if (debug) { ele.style.outline = "3px dotted " + col; ele.style.boxShadow = " 0px 0px 4px 4px " + col + "30, inset 0 0 100px " + col + "20" } } function debugRemove(ele) { if (debug2) { ele.style.opacity = "0.5"; } else ele.remove(); } })()