Greasy Fork is available in English.

ヨドバシ検索結果で量あたり単価を表示

Shift+A/Shift+B:量あたり単価上限で絞り込み .:価格上限入力フォームにフォーカス

Versión del día 20/6/2020. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name ヨドバシ検索結果で量あたり単価を表示
// @description Shift+A/Shift+B:量あたり単価上限で絞り込み .:価格上限入力フォームにフォーカス
// @match *://*.yodobashi.com/*
// @version 0.2.6
// @grant none
// @namespace https://greasyfork.org/users/181558
// ==/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 $('<span style="all:initial; ' + (pointer ? 'cursor:pointer; ' : '') + 'position: fixed; right:1em; bottom: ' + (gStaY += 2.5) + 'em; z-index:2147483647; opacity:1; font-size:90%; font-weight:bold; margin:0px 1px; text-decoration:none !important; text-align:center; padding:1px 6px 1px 6px; border-radius:12px; background-color:#6080ff; color:white; white-space: nowrap; ">' + str + '</span>').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 : 3;

  const titleXPath = '//div[contains(@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; }
          if (ele.innerText.match(/店頭でのみ販売しています|販売を終了しました/)) { debugRemove(ele.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;
      for (let site of [
          ["UserBenchmark", "www.userbenchmark.com", "#609070", /内蔵SSD|内蔵ハードディスク|PCパーツ>CPU|PCパーツ>グラフィックボード|USBメモリ/],
          ["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) && ((titleEle.parentNode.parentNode.parentNode.innerText)).match(site[3])) { // RTINGS/kopfhoererリンク
          //          var ele = parentEle.parentNode.insertBefore(document.createElement("a"), parentEle.nextSibling);
          var ele = titleEle.parentNode.parentNode.parentNode.insertBefore(document.createElement("a"), titleEle.parentNode.parentNode.nextSibling);
          var iflsite = (Math.random() > ddg_google_ratio) ? "https://duckduckgo.com/?q=!ducky+" : "https://www.google.com/webhp?#btnI=I&q=";
          ele.innerHTML += '<a rel=\"noopener noreferrer nofollow\" href="' + iflsite + (titleEle.innerText.replace(/[\/\?\+\[\]\(\)\u30a0-\u30ff\u3040-\u309f\u3005-\u3006\u30e0-\u9fcf]+/gmi, " ") + ' ').replace(/\s{2,9}/gm, " ") + 'site:' + site[1] + '" style="font-weight:bold; font-size:80%;display:inline-block;margin:1px 3px;  padding:0.03em 0.5em 0.03em 0.5em; border-radius:99px; background-color:' + site[2] + '; color:white; white-space: nowrap; ">' + site[0] + '</a>';
        }
      }

      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 += "<span style='background-color:#fff8e8;'>(" + Math.round(pointPer * 100) + "%?)</span>";
        }
        if (pointPer) {
          var point = Math.ceil(price - (price * (price / (price + price * pointPer))));
          var pricef = Math.round(price - point).toLocaleString();

          if (enableBeta) priceEle.innerHTML += " <span style='background-color:#fff0f0;'>" + (isorder ? "<br>還元後:" : "(還元後:") + "¥" + pricef + (isorder ? "" : ")") + "</span>";
        }
      } 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(/\s([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|m)/))) ? Number(title.match(/×([0-9\.\,]+)/)[1]) : 1;

        if (point > -1) {
          var ppr = Math.round(100 * (price - point) / Number(ryout * mul)) / 100;

          var ele = $('<span style="all:initial; display:inline-block; font-weight:bold; font-size:90%;margin:0.5px 0px 0.5px 3px; text-decoration:none !important;  padding:0.03em 0.5em 0.03em 0.2em; border-radius:99px; background-color:#6080b0; color:white; white-space: nowrap; ">¥' + ppr + '/' + (ryou[2] || ryou[4]) + '</span>').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 = $('<span style="all:initial; display:inline-block; font-weight:bold; font-size:90%;margin:0.5px 0px 0.5px 3px; text-decoration:none !important;  padding:0.03em 0.5em 0.03em 0.2em; border-radius:99px; background-color:#6080b0; color:white; white-space: nowrap; ">¥' + ppr2 + '/' + (ryou[2] || ryou[4]) + '</span>').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);
      if ((cppLimit[2] > 0 && pass2 == 0) || (cppLimit[2] > 0 && (!ryou))) debugRemove(parentEle);
    }
  }

  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();
  }
})()