PageSourceSearch

https://www.anbe.jp/js/recently_viewed.js

js anbe.jp collected 2026-09-25 05:19:53 UTC 10,009 bytes, 387 lines download raw bytes

1(function(){
2  /* =============================================
3     最近見た商品のレコメンド(sps_creative_ver1.0)
4  ============================================= */
5
6  const DISPLAY_ITEMS    = 8; //商品の最大表示数(商品)
7  const EXPIRE_DAYS      = 7; //閲覧履歴の保持期間(日)
8
9  const SECTION_TITLE    = "最近チェックした商品"; //タイトル名
10  const TRACKING_PARAM   = "from=recently_viewed"; //URLパラメータ
11
12  const DEFAULT_SELECTOR = "#wrapper"; //表示位置
13  const DEFAULT_POSITION = "after"; //前(before) 後(after)
14
15  const STORAGE_KEY      = "recently_viewed"; //キー名
16  const HIDE_URL         = ['order.php','regi.php']; //非表示
17
18  const PAGE_SETTINGS    = [ //ページごとの表示位置調整
19    { //スマホ専用カート
20      match: (url) => url.includes("smp_cart.php"),
21      selector: "#contents",
22      position: "after"
23    },
24    { //スマートフォンサイト
25      match: (url) => url.includes("/smp"),
26      selector: "#contents",
27      position: "after"
28    },
29    { //PCカート
30      match: (url) => url.includes("/cart.php"),
31      selector: '#EST_SPS_cart div[align="left"]:has(form[name="SiteSealReference"])', //SSLマーク
32      position: "before"
33    }
34  ];
35	
36  /* =============================================
37     テストモード開始
38  ============================================= */
39
40  const TEST_MODE_ONLY = false; //公開時はfalse
41
42  function isTestMode() {
43    if (!TEST_MODE_ONLY) return true;
44    const params = new URLSearchParams(location.search);
45    return params.get("mode") === "recently_viewed";
46  }
47	
48  function loadTestCSS() {
49    if (!TEST_MODE_ONLY) return;
50    if (!isTestMode()) return;
51    if (document.querySelector('link[data-recently-viewed-css]')) return;
52
53    const link = document.createElement("link");
54    link.rel = "stylesheet";
55    link.href = "/css/recently_viewed.css";
56    link.setAttribute("data-recently-viewed-css", "true");
57    document.head.appendChild(link);
58  }
59
60  /* =============================================
61     テストモード終了
62  ============================================= */
63
64  let INITIAL_IMAGE_SRC = "";
65
66  (function(){
67    const img =
68      document.querySelector(".mainImg img") ||
69      document.querySelector(".mainImg") ||
70      document.querySelector("#mainImg img");
71
72    if (!img) return;
73
74    let src =
75      img.getAttribute && (
76        img.getAttribute("src") ||
77        img.getAttribute("data-src") ||
78        img.getAttribute("data-original")
79      );
80
81    if (!src && img.src) src = img.src;
82
83    if (src) {
84      INITIAL_IMAGE_SRC = src;
85    }
86  })();
87
88
89  const MAX_ITEMS = DISPLAY_ITEMS + 1;
90
91  function normalizeUrl(url) {
92    try {
93      const u = new URL(url, location.origin);
94      return u.origin + u.pathname;
95    } catch (e) {
96      return url.split('?')[0];
97    }
98  }
99
100  function appendTrackingParam(url) {
101    if (!TRACKING_PARAM) return url;
102    const separator = url.includes("?") ? "&" : "?";
103    return url + separator + TRACKING_PARAM;
104  }
105
106  function shouldHide() {
107    return HIDE_URL.some(p => location.href.includes(p));
108  }
109
110  function normalizeImagePath(path) {
111    if (!path) return "";
112    return path.replace(/\/simg\//g, "/");
113  }
114
115  function toAbsolute(url) {
116    if (!url) return "";
117    try {
118      return new URL(url, location.href).href;
119    } catch (e) {
120      return url;
121    }
122  }
123
124  function cleanPriceText(text){
125    if(!text) return "";
126    return text.replace(/\(税込\)/g,"").trim();
127  }
128
129  function getViewed() {
130    const now = Date.now();
131    const expire = EXPIRE_DAYS * 24 * 60 * 60 * 1000;
132
133    let viewed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
134    viewed = viewed.filter(v => !v.ts || now - v.ts <= expire);
135
136    localStorage.setItem(STORAGE_KEY, JSON.stringify(viewed));
137    return viewed;
138  }
139
140  function saveProductIfProductPage() {
141
142    if (!document.querySelector("#itemDetail-wrap")) return;
143    if (!document.querySelector("#pi_cart h1")) return;
144    if (!document.querySelector("table.price")) return;
145
146    let retry = 0;
147    const maxRetry = 30;
148
149    const timer = setInterval(function(){
150
151      const nameEl = document.querySelector("#pi_cart h1");
152
153      if (!nameEl) {
154        retry++;
155        if (retry >= maxRetry) clearInterval(timer);
156        return;
157      }
158
159      clearInterval(timer);
160
161      const productName = nameEl.textContent.trim();
162      if (!productName) return;
163
164      let imageSrc = INITIAL_IMAGE_SRC;
165
166      if (!imageSrc) {
167        const imgEl =
168          document.querySelector(".mainImg img") ||
169          document.querySelector(".mainImg");
170
171        if (imgEl) {
172          imageSrc =
173            imgEl.getAttribute("src") ||
174            imgEl.getAttribute("data-src") ||
175            imgEl.src;
176        }
177      }
178
179      if (!imageSrc) return;
180
181      imageSrc = toAbsolute(normalizeImagePath(imageSrc));
182
183      let priceText = "";
184      let priceClass = "";
185      let discountRateText = "";
186
187      const priceTable = document.querySelector("table.price");
188      if (!priceTable) return;
189
190      const specialPriceEl = priceTable.querySelector(".special_price");
191      const firstSelling   = priceTable.querySelector(".selling_price");
192      const firstDiscount  = priceTable.querySelector(".discount_rate, .discount-rate");
193
194      if (specialPriceEl) {
195        priceText = cleanPriceText(specialPriceEl.textContent.trim());
196        priceClass = "special_price";
197      }
198      else if (firstSelling) {
199        priceText = cleanPriceText(firstSelling.textContent.trim());
200        priceClass = "selling_price";
201      }
202
203      if (firstDiscount) {
204        discountRateText = firstDiscount.textContent.trim();
205      } else {
206        discountRateText = "";
207      }
208
209      const baseUrl = normalizeUrl(location.href);
210
211      let viewed = getViewed();
212      viewed = viewed.filter(v => v.url !== baseUrl);
213
214      viewed.unshift({
215        name: productName,
216        image: imageSrc,
217        price_text: priceText,
218        price_class: priceClass,
219        discount_rate: discountRateText,
220        url: baseUrl,
221        ts: Date.now()
222      });
223
224      if (viewed.length > MAX_ITEMS) {
225        viewed = viewed.slice(0, MAX_ITEMS);
226      }
227
228      localStorage.setItem(STORAGE_KEY, JSON.stringify(viewed));
229
230    }, 100);
231  }
232
233  function resolveInsertSetting() {
234
235    const url = location.href;
236
237    for (let i = 0; i < PAGE_SETTINGS.length; i++) {
238      if (PAGE_SETTINGS[i].match(url)) {
239        return PAGE_SETTINGS[i];
240      }
241    }
242
243    return {
244      selector: DEFAULT_SELECTOR,
245      position: DEFAULT_POSITION
246    };
247  }
248
249  function insertSection(section) {
250
251    const setting = resolveInsertSetting();
252    const target = document.querySelector(setting.selector);
253    if (!target || !target.parentNode) return;
254
255    if (setting.position === "before") {
256      target.parentNode.insertBefore(section, target);
257    } else {
258      target.parentNode.insertBefore(section, target.nextSibling);
259    }
260  }
261
262  function render() {
263
264    if (!isTestMode()) return;
265    if (shouldHide()) return;
266
267    loadTestCSS();
268
269    const existing = document.querySelector(".recentlyViewed");
270    if (existing) existing.remove();
271
272    const currentUrl = normalizeUrl(location.href);
273    const viewed = getViewed();
274
275    const displayItems = viewed
276      .filter(v => v.url !== currentUrl)
277      .slice(0, DISPLAY_ITEMS);
278
279    if (!displayItems.length) return;
280
281    const section = document.createElement("section");
282    section.className = "recentlyViewed";
283    section.innerHTML = `
284      <div class="recentlyViewed__inner">
285        <h2 class="section__title">${SECTION_TITLE}</h2>
286        <div class="product__lists"></div>
287      </div>
288    `;
289
290    const listsDiv = section.querySelector(".product__lists");
291
292    displayItems.forEach(item => {
293
294      const linkUrl = appendTrackingParam(item.url);
295
296      const priceHtml = item.price_text
297        ? `
298          <p class="item-price">
299            <span class="${item.price_class}">
300              ${item.price_text}(税込)
301            </span>
302            ${item.discount_rate
303              ? `<span class="discount-icon">${item.discount_rate}</span>`
304              : ""}
305          </p>
306        `
307        : "";
308
309      const div = document.createElement("div");
310      div.className = "recent-item";
311      div.innerHTML = `
312        <a href="${linkUrl}">
313          <p class="item-img">
314            <img src="${item.image}" alt="${item.name}">
315          </p>
316          <p class="item-name">${item.name}</p>
317          ${priceHtml}
318        </a>
319      `;
320
321      listsDiv.appendChild(div);
322    });
323
324    insertSection(section);
325  }
326
327  document.addEventListener("DOMContentLoaded", function () {
328    saveProductIfProductPage();
329    render();
330  });
331
332})();
333
334// 文字省略対応
335document.addEventListener('DOMContentLoaded', function () {
336
337  // 文字幅を考慮してカット
338  function truncateByWidth(str, maxWidth) {
339    let width = 0;
340    let result = '';
341
342    for (let char of str) {
343      // 全角判定(ざっくり)
344      const charWidth = (char.match(/[^\x01-\x7E]/)) ? 2 : 1;
345
346      if (width + charWidth > maxWidth) {
347        return result + '...';
348      }
349
350      width += charWidth;
351      result += char;
352    }
353
354    return result;
355  }
356
357  function truncateText() {
358    const targets = document.querySelectorAll('.column-set h3 a, section.recentlyViewed p.item-name');
359
360    if (window.innerWidth <= 768) {
361      targets.forEach(function(el) {
362
363        // 元テキスト保持
364        if (!el.dataset.originalText) {
365          el.dataset.originalText = el.textContent.trim();
366        }
367
368        const text = el.dataset.originalText;
369
370        // 文字数調整
371        el.textContent = truncateByWidth(text, 42);
372
373      });
374    } else {
375      // PC戻し
376      targets.forEach(function(el) {
377        if (el.dataset.originalText) {
378          el.textContent = el.dataset.originalText;
379        }
380      });
381    }
382  }
383
384  truncateText();
385  window.addEventListener('resize', truncateText);
386
387});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.