app/template/default/Product/detail.twig line 1

Open in your IDE?
  1. {#
  2. This file is part of EC-CUBE
  3. Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
  4. http://www.ec-cube.co.jp/
  5. For the full copyright and license information, please view the LICENSE
  6. file that was distributed with this source code.
  7. #}
  8. {% extends 'default_frame.twig' %}
  9. {% set body_class = 'product_page' %}
  10. {% block stylesheet %}
  11.     <style>
  12.         .hidden{
  13.             display: none;
  14.         }
  15.         .slick-slider {
  16.             margin-bottom: 30px;
  17.         }
  18.         .slick-dots {
  19.             position: absolute;
  20.             bottom: -45px;
  21.             display: block;
  22.             width: 100%;
  23.             padding: 0;
  24.             list-style: none;
  25.             text-align: center;
  26.         }
  27.         .slick-dots li {
  28.             position: relative;
  29.             display: inline-block;
  30.             width: 20px;
  31.             height: 20px;
  32.             margin: 0 5px;
  33.             padding: 0;
  34.             cursor: pointer;
  35.         }
  36.         .slick-dots li button {
  37.             font-size: 0;
  38.             line-height: 0;
  39.             display: block;
  40.             width: 20px;
  41.             height: 20px;
  42.             padding: 5px;
  43.             cursor: pointer;
  44.             color: transparent;
  45.             border: 0;
  46.             outline: none;
  47.             background: transparent;
  48.         }
  49.         .slick-dots li button:hover,
  50.         .slick-dots li button:focus {
  51.             outline: none;
  52.         }
  53.         .slick-dots li button:hover:before,
  54.         .slick-dots li button:focus:before {
  55.             opacity: 1;
  56.         }
  57.         .slick-dots li button:before {
  58.             content: " ";
  59.             line-height: 20px;
  60.             position: absolute;
  61.             top: 0;
  62.             left: 0;
  63.             width: 12px;
  64.             height: 12px;
  65.             text-align: center;
  66.             opacity: .25;
  67.             background-color: black;
  68.             border-radius: 50%;
  69.         }
  70.         .slick-dots li.slick-active button:before {
  71.             opacity: .75;
  72.             background-color: black;
  73.         }
  74.         .slick-dots li button.thumbnail img {
  75.             width: 0;
  76.             height: 0;
  77.         }
  78.         .price_no_tax{
  79.             color: black;
  80.         }
  81.         .price_no_tax_first{
  82.             margin-left: 30px;
  83.         }
  84.         .product_plate_type_print img{
  85.             width: unset;
  86.         }
  87.         .ec-productRole .ec-productRole__category{
  88.             border-bottom: unset;
  89.         }
  90.     </style>
  91. {% endblock %}
  92. {# --- この商品がカテゴリID=124か判定 --- #}
  93.     {% set is_calendar = false %}
  94.     {% for pc in Product.ProductCategories %}
  95.         {% if pc.Category.id == 124 %}
  96.             {% set is_calendar = true %}
  97.         {% endif %}
  98.     {% endfor %}
  99. {% block javascript %}
  100.     <script>
  101.         var stocks = {{stocks|json_encode|raw}};
  102.         eccube.classCategories = {{ class_categories_as_json(Product)|raw }};
  103.         // 規格2に選択肢を割り当てる。
  104.         function fnSetClassCategories(form, classcat_id2_selected) {
  105.             var $form = $(form);
  106.             var product_id = $form.find('input[name=product_id]').val();
  107.             var $sele1 = $form.find('select[name=classcategory_id1]');
  108.             var $sele2 = $form.find('select[name=classcategory_id2]');
  109.             eccube.setClassCategories($form, product_id, $sele1, $sele2, classcat_id2_selected);
  110.         }
  111.         var code = jQuery('.product-code-default').text();
  112.         if (Number(stocks[code]) < 0){
  113.             jQuery('.product-stock-quantity-default').html('<b>あり</b>');
  114.         }
  115.         if (stocks[code] != undefined && stocks[code] > 0){
  116.             jQuery('.product-stock-quantity-default').html(stocks[code]);
  117.         }else if (stocks[code] != undefined && stocks[code] == 0){
  118.             jQuery('.product-stock-quantity-default').html('<b>なし</b>');
  119.         }
  120.         {% if form.classcategory_id2 is defined %}
  121.             fnSetClassCategories(
  122.                 $('#form1'), {{ form.classcategory_id2.vars.value|json_encode|raw }}
  123.             );
  124.         {% elseif form.classcategory_id1 is defined %}
  125.             $('input[name="classcategory_option"]').on('change', function(e) {
  126.                 $('#classcategory_id1').val($(e.target).val()).change();
  127.             })
  128.             eccube.checkStock($('#form1'), {{ Product.id }}, {{ form.classcategory_id1.vars.value|json_encode|raw }}, null);
  129.         {% endif %}
  130.     </script>
  131.     <script>
  132.         $(function() {
  133.             // bfcache無効化
  134.             $(window).bind('pageshow', function(event) {
  135.                 if (event.originalEvent.persisted) {
  136.                     location.reload(true);
  137.                 }
  138.             });
  139.             // Core Web Vital の Cumulative Layout Shift(CLS)対策のため
  140.             // img タグに width, height が付与されている.
  141.             // 630px 未満の画面サイズでは縦横比が壊れるための対策
  142.             // see https://github.com/EC-CUBE/ec-cube/pull/5023
  143.             $('.ec-grid2__cell').hide();
  144.             var removeSize = function () {
  145.                 $('.slide-item').height('');
  146.                 $('.slide-item img')
  147.                     .removeAttr('width')
  148.                     .removeAttr('height')
  149.                     .removeAttr('style');
  150.             };
  151.             var slickInitial = function(slick) {
  152.                 $('.ec-grid2__cell').fadeIn(1500);
  153.                 var baseHeight = $(slick.target).height();
  154.                 var baseWidth = $(slick.target).width();
  155.                 var rate = baseWidth / baseHeight;
  156.                 $('.slide-item').height(baseHeight * rate); // 余白を削除する
  157.                 // transform を使用することでCLSの影響を受けないようにする
  158.                 $('.slide-item img')
  159.                     .css(
  160.                         {
  161.                             'transform-origin': 'top left',
  162.                             'transform': 'scaleY(' + rate + ')',
  163.                             'transition': 'transform .1s'
  164.                         }
  165.                     );
  166.                 // 正しいサイズに近くなったら属性を解除する
  167.                 setTimeout(removeSize, 500);
  168.             };
  169.             $('.item_visual').on('init', slickInitial);
  170.             // リサイズ時は CLS の影響を受けないため属性を解除する
  171.             $(window).resize(removeSize);
  172.             $('.item_visual').slick({
  173.                 dots: false,
  174.                 arrows: false,
  175.                 responsive: [{
  176.                     breakpoint: 768,
  177.                     settings: {
  178.                         dots: true
  179.                     }
  180.                 }]
  181.             });
  182.             $('.slideThumb').on('click', function() {
  183.                 var index = $(this).attr('data-index');
  184.                 $('.item_visual').slick('slickGoTo', index, false);
  185.             })
  186.         });
  187.     </script>
  188.     <script>
  189.         $(function() {
  190.             function htmlQty(type, printerUnit, orderUnit) {
  191.                 var html = ``;
  192.                 if(type === 1) {
  193.                     {% if Product.printer_unit %}
  194.                         html += `<select id="quantity" name="quantity" style="min-width:unset;" placeholder="数量" class="form-select">`;
  195.                             html += `<option value="" selected="selected">数量</option>`;
  196.                             for(let i = 1; i < 31; i++) {
  197.                                 html += `<option value="${ i*printerUnit }">${ i*printerUnit }</option>`;
  198.                             }
  199.                         html += `</select>`;
  200.                     {% else %}
  201.                         if(orderUnit > 0) {
  202.                             if(printerUnit === 0) {
  203.                                 printerUnit = orderUnit;
  204.                             }
  205.                             html += `<select id="quantity" name="quantity" style="min-width:unset;" placeholder="数量" class="form-select">`;
  206.                                 html += `<option value="" selected="selected">数量</option>`;
  207.                                 for(let i = 1; i < 31; i++) {
  208.                                     html += `<option value="${ i*printerUnit }">${ i*printerUnit }</option>`;
  209.                                 }
  210.                             html += `</select>`;
  211.                         } else {
  212.                             html += `<input type="number" id="quantity" name="quantity" required="required" min="1" maxlength="9" style="min-width:unset; display: block !important;" placeholder="数量" class="form-control" value="1">`;
  213.                         }
  214.                     {% endif %}
  215.                 } else {
  216.                     {% if Product.order_unit %}
  217.                         html += `<select id="quantity" name="quantity" style="min-width:unset;" placeholder="数量" class="form-select">`;
  218.                             html += `<option value="" selected="selected">数量</option>`;
  219.                             for(let i = 1; i < 31; i++) {
  220.                                 html += `<option value="${ i*orderUnit }">${ i*orderUnit }</option>`;
  221.                             }
  222.                         html += `</select>`;
  223.                     {% else %}
  224.                         html += `<input type="number" id="quantity" name="quantity" required="required" min="1" maxlength="9" style="min-width:unset; display: block !important;" placeholder="数量" class="form-control" value="1">`;
  225.                     {% endif %}
  226.                 }
  227.                 return html;
  228.             }
  229.             $(".group-input-qty").html('');
  230.             $(".group-input-qty").append(htmlQty(0, {{ Product.printer_unit | default(0) }}, {{ Product.order_unit | default(0) }}));
  231.             $('.add-cart').on('click', function(event) {
  232.                 {% if form.classcategory_id1 is defined %}
  233.                 // 規格1フォームの必須チェック
  234.                 if ($('#classcategory_id1').val() == '__unselected' || $('#classcategory_id1').val() == '') {
  235.                     $('#classcategory_id1')[0].setCustomValidity('{{ '項目が選択されていません'|trans }}');
  236.                     return true;
  237.                 } else {
  238.                     $('#classcategory_id1')[0].setCustomValidity('');
  239.                 }
  240.                 {% endif %}
  241.                 {% if form.classcategory_id2 is defined %}
  242.                 // 規格2フォームの必須チェック
  243.                 if ($('#classcategory_id2').val() == '__unselected' || $('#classcategory_id2').val() == '') {
  244.                     $('#classcategory_id2')[0].setCustomValidity('{{ '項目が選択されていません'|trans }}');
  245.                     return true;
  246.                 } else {
  247.                     $('#classcategory_id2')[0].setCustomValidity('');
  248.                 }
  249.                 {% endif %}
  250.                 // 個数フォームのチェック
  251.                 var isPrint = false;
  252.                 if ($('#is_print').val() == 1) {
  253.                     isPrint = true;
  254.                     if (!$('#product_print_color').val()) {
  255.                         $('#product_print_color')[0].setCustomValidity('{{ 'ネームプリントの色を選択してください。'|trans }}');
  256.                         return true;
  257.                     } else {
  258.                         $('#product_print_color')[0].setCustomValidity('');
  259.                     }
  260.                 }
  261.                 // 個数フォームのチェック
  262.                     if (!$('#quantity').val()) {
  263.                         if ($('#quantity').prop('nodeName') == 'SELECT') {
  264.                             $('#quantity')[0].setCustomValidity('{{ '数量を選択してください。'|trans }}');
  265.                         } else {
  266.                             $('#quantity')[0].setCustomValidity('{{ '数量を入力してください。'|trans }}');
  267.                         }
  268.                         return true;
  269.                     } else {
  270.                         $('#quantity')[0].setCustomValidity('');
  271.                     }
  272.                 event.preventDefault();
  273.                 $form = $('#form1');
  274.                 $.ajax({
  275.                     url: $form.attr('action'),
  276.                     type: $form.attr('method'),
  277.                     data: $form.serialize(),
  278.                     dataType: 'json',
  279.                     beforeSend: function(xhr, settings) {
  280.                         // Buttonを無効にする
  281.                         $('.add-cart').prop('disabled', true);
  282.                     }
  283.                 }).done(function(data) {
  284.                     // レスポンス内のメッセージをalertで表示
  285.                     $.each(data.messages, function() {
  286.                         $('#ec-modal-header').html(this);
  287.                     });
  288.                     $('.ec-modal').show()
  289.                     // カートブロックを更新する
  290.                     $.ajax({
  291.                         url: "{{ url('block_cart') }}",
  292.                         type: 'GET',
  293.                         dataType: 'html'
  294.                     }).done(function(html) {
  295.                         $('.ec-headerRole__cart').html(html);
  296.                     });
  297.                 }).fail(function(data) {
  298.                     alert('{{ 'カートへの追加に失敗しました。'|trans }}');
  299.                 }).always(function(data) {
  300.                     // Buttonを有効にする
  301.                     $('.add-cart').prop('disabled', false);
  302.                 });
  303.             });
  304.             $("#is_print").change(function(){
  305.                 $(".group-input-qty").html('');
  306.                 if ($(this).val() == 1) {
  307.                     $('.form-is_repurchase').css('display', 'block');
  308.                     $('.form-is_repurchase input:first').prop('checked', true);
  309.                     $('.form-product-color').css('display', 'block');
  310.                     $(".group-input-qty").append(htmlQty(1, {{ Product.printer_unit | default(0) }}, {{ Product.order_unit | default(0) }}));
  311.                     // $("#quantity").attr('name', 'quantity').css('display', 'inline-block');
  312.                     // $("#quantity_raw").attr('name', 'quantity_raw').css('display', 'none');
  313.                 } else {
  314.                     $('.form-is_repurchase').css('display', 'none');
  315.                     $('.form-is_repurchase input').prop('checked', false);
  316.                     $('.form-product-color').css('display', 'none');
  317.                     $('.form-product-color select').val('');
  318.                     $(".group-input-qty").append(htmlQty(0, {{ Product.printer_unit | default(0) }}, {{ Product.order_unit | default(0) }}));
  319.                     // $("#quantity").attr('name', 'quantity_raw').css('display', 'none');
  320.                     // $("#quantity_raw").attr('name', 'quantity').css('display', 'inline-block');
  321.                 }
  322.             });
  323.         });
  324.         $('.ec-modal-wrap').on('click', function(e) {
  325.             // モーダル内の処理は外側にバブリングさせない
  326.             e.stopPropagation();
  327.         });
  328.         $('.ec-modal-overlay, .ec-modal, .ec-modal-close, .ec-inlineBtn--cancel').on('click', function() {
  329.             $('.ec-modal').hide()
  330.         });
  331.         var nIndexClass = 1;
  332.         $("#classcategory_id1 > option").each(function(k, v) {
  333.             if(this.value != "__unselected") {
  334.                 $(v).attr('key', nIndexClass);
  335.                 nIndexClass++;
  336.             }
  337.         });
  338.         var jsonQty = {{ Product.getAllStocks|json_encode|raw }}
  339.         var select_classcategory_id1 = 0;
  340.         $('#classcategory_id1').on('change', function(e) {
  341.             if(this.selectedIndex < 1) return false;
  342.             {% if form.classcategory_id1 is defined %}
  343.                 getClass(this.selectedIndex)
  344.                 {% if form.classcategory_id2 is defined %}
  345.                     select_classcategory_id1 = this.selectedIndex;
  346.                 {% endif %}
  347.             {% endif %}
  348.         });
  349.         $('#classcategory_id2').on('change', function(e) {
  350.             if(this.selectedIndex < 1) return false;
  351.             getClass(select_classcategory_id1, this.selectedIndex)
  352.         });
  353.         function getClass(class1, class2) {
  354.             var qty = '';
  355.             {% if form.classcategory_id1 is defined %}
  356.                qty = jsonQty[class1];
  357.                 {% if form.classcategory_id2 is defined %}
  358.                     qty = jsonQty[class1][class2];
  359.                 {% endif %}
  360.             {% endif %}
  361.             if(qty == "0") {
  362.                 $('.ec-productRole__code span.product-stock-quantity-default').html("<b>なし</b>");
  363.             } else if(!!qty === false){
  364.                 $('.ec-productRole__code span.product-stock-quantity-default').html("<b>あり</b>");
  365.             }else {
  366.                 qty = parseInt(qty);
  367.                 $('.ec-productRole__code span.product-stock-quantity-default').html("<b>"+qty+"</b>");
  368.             }
  369.         }
  370.         // $('#is_print').change(function() {
  371.         //     window.location.href = window.location.href+'?is_print='+$(this).val();
  372.         // });
  373.     </script>
  374.     <script type="application/ld+json">
  375.     {
  376.         "@context": "https://schema.org/",
  377.         "@type": "Product",
  378.         "name": "{{ Product.name }}",
  379.         "image": [
  380.             {% for img in Product.ProductImage %}
  381.                 "{{ app.request.schemeAndHttpHost }}{{ asset(img, 'save_image') }}"{% if not loop.last %},{% endif %}
  382.             {% else %}
  383.                 "{{ app.request.schemeAndHttpHost }}{{ asset(''|no_image_product, 'save_image') }}"
  384.             {% endfor %}
  385.         ],
  386.         "description": "{{ Product.description_list | default(Product.description_detail) | replace({'\n': '', '\r': ''}) | slice(0,300) }}",
  387.         {% if Product.code_min %}
  388.         "sku": "{{ Product.code_min }}",
  389.         {% endif %}
  390.         "offers": {
  391.             "@type": "Offer",
  392.             "url": "{{ url('product_detail', {'id': Product.id}) }}",
  393.             "priceCurrency": "{{ eccube_config.currency }}",
  394.             "price": {{ Product.getPrice02IncTaxMin ? Product.getPrice02IncTaxMin : 0}},
  395.             "availability": "{{ Product.stock_find ? "InStock" : "OutOfStock" }}"
  396.         }
  397.     }
  398.     </script>
  399.     {# --- この商品がカテゴリID=124か判定 --- #}
  400. {% set is_calendar = false %}
  401. {% for pc in Product.ProductCategories %}
  402.   {% if pc.Category.id == 124 %}
  403.     {% set is_calendar = true %}
  404.   {% endif %}
  405. {% endfor %}
  406. {% if is_calendar %}
  407. <script>
  408. document.addEventListener('DOMContentLoaded', function () {
  409.   // 規格1(数量パターン)
  410.   const select1 = document.getElementById('classcategory_id1')
  411.               || document.querySelector('select[name="classcategory_id1"]');
  412.   if (!select1) return;
  413.   // 規格2(優先: #is_print、なければ classcategory_id2)
  414.   const isPrint = document.getElementById('is_print'); // 0=しない, 1=する
  415.   const select2 = isPrint || document.getElementById('classcategory_id2')
  416.                || document.querySelector('select[name="classcategory_id2"]');
  417.   // 再版区分(表示/非表示を切り替え)
  418.   const repurchaseBlock = document.querySelector('.form-is_repurchase');
  419.   // ネームプリント切替連動(カレンダー)
  420. const PRINT_ON_VALUES   = new Set(['291', '292']);  // ネームプリントあり
  421. const PRINT_OFF_VALUES  = new Set(['289', '290']);  // ネームプリントなし
  422. const PRINT_BOTH_VALUES = new Set(['288']);         // 両方選択可
  423.   // classcategory_id2 用に元<option>保存(#is_printが無い場合に使う)
  424.   let original2 = null;
  425.   if (select2 && select2 !== isPrint) {
  426.     original2 = Array.from(select2.options).map(o => ({ value: o.value, text: (o.text||'').trim() }));
  427.   }
  428.   function applyRepurchaseVisibility() {
  429.     if (!repurchaseBlock) return;
  430.     if (isPrint) {
  431.       repurchaseBlock.style.display = (isPrint.value === '1') ? '' : 'none';
  432.     }
  433.   }
  434.   function setIsPrintMode(mode) {
  435.     // #is_print がある(今回の構成)場合
  436.     if (isPrint) {
  437.       isPrint.innerHTML = '';
  438.       if (mode === 'do') {
  439.         isPrint.add(new Option('ネームプリントする', '1'));
  440.         isPrint.value = '1';
  441.       } else if (mode === 'dont') {
  442.         isPrint.add(new Option('ネームプリントしない', '0'));
  443.         isPrint.value = '0';
  444.       } else if (mode === 'both') {
  445.         // ★ 両方出す
  446.         isPrint.add(new Option('ネームプリントしない', '0'));
  447.         isPrint.add(new Option('ネームプリントする', '1'));
  448.         // 初期は「しない」に寄せる(必要なら '1' に変更可)
  449.         isPrint.value = '0';
  450.       }
  451.       // 他のハンドラより後に発火させる
  452.       setTimeout(() => {
  453.         isPrint.dispatchEvent(new Event('change', { bubbles: true }));
  454.         applyRepurchaseVisibility();
  455.       }, 0);
  456.       return;
  457.     }
  458.     // #is_print が無く、classcategory_id2 を使っている場合(参考)
  459.     if (select2 && original2) {
  460.       let list = [];
  461.       if (mode === 'do')      list = original2.filter(o => o.text.includes('する'));
  462.       else if (mode === 'dont') list = original2.filter(o => o.text.includes('しない'));
  463.       else /* both */         list = original2.slice();
  464.       select2.innerHTML = '';
  465.       list.forEach(o => select2.add(new Option(o.text, o.value)));
  466.       select2.selectedIndex = list.length ? 0 : -1;
  467.       setTimeout(() => {
  468.         select2.dispatchEvent(new Event('change', { bubbles: true }));
  469.       }, 0);
  470.       if (repurchaseBlock) {
  471.         repurchaseBlock.style.display = (mode === 'do') ? '' : 'none';
  472.       }
  473.     }
  474.   }
  475.   function onFirstChanged() {
  476.     const v1 = select1.value;
  477.     if (!v1 || v1 === '__unselected') {
  478.       if (repurchaseBlock) repurchaseBlock.style.display = 'none';
  479.       return;
  480.     }
  481.     if (PRINT_BOTH_VALUES.has(v1)) {
  482.       setIsPrintMode('both');  // ★ 100冊以上~ は両方
  483.     } else if (PRINT_ON_VALUES.has(v1)) {
  484.       setIsPrintMode('do');
  485.     } else if (PRINT_OFF_VALUES.has(v1)) {
  486.       setIsPrintMode('dont');
  487.     } else {
  488.       setIsPrintMode('dont');
  489.     }
  490.   }
  491.   if (isPrint) {
  492.     isPrint.addEventListener('change', applyRepurchaseVisibility);
  493.   }
  494. function updateIsPrintOptions(mode) {
  495.   const isPrint = document.getElementById('is_print');
  496.   const repurchaseBlock = document.querySelector('.form-is_repurchase');
  497.   if (!isPrint) return;
  498.   // 一旦 option 全削除
  499.   isPrint.innerHTML = '';
  500.   if (mode === 'do') {
  501.     isPrint.add(new Option('ネームプリントする', '1'));
  502.     isPrint.value = '1';
  503.   } else if (mode === 'dont') {
  504.     isPrint.add(new Option('ネームプリントしない', '0'));
  505.     isPrint.value = '0';
  506.   } else {
  507.     isPrint.add(new Option('ネームプリントしない', '0'));
  508.     isPrint.add(new Option('ネームプリントする', '1'));
  509.     isPrint.value = '0';
  510.   }
  511.   // ネームプリント「する」なら再版選択を表示
  512.   if (repurchaseBlock) {
  513.     repurchaseBlock.style.display = (isPrint.value === '1') ? '' : 'none';
  514.   }
  515.   // イベント発火で数量 UI 切り替えなど反映
  516.   isPrint.dispatchEvent(new Event('change', { bubbles: true }));
  517. }
  518.   select1.addEventListener('change', onFirstChanged);
  519.   onFirstChanged(); // 初期反映
  520. });
  521. </script>
  522. <script>
  523. document.addEventListener('DOMContentLoaded', function () {
  524.   const el1 = document.getElementById('classcategory_id1') || document.querySelector('select[name="classcategory_id1"]');
  525.   const el2 = document.getElementById('classcategory_id2') || document.querySelector('select[name="classcategory_id2"]');
  526.   const out = document.getElementById('delivery-duration-text');
  527.   // 発送目安マップ
  528.   const deliveryMap = {};
  529.   {% for pc in Product.ProductClasses %}
  530.     {% set c1 = pc.ClassCategory1 ? pc.ClassCategory1.id : '' %}
  531.     {% set c2 = pc.ClassCategory2 ? pc.ClassCategory2.id : '' %}
  532.     {% set label = pc.DeliveryDuration ? pc.DeliveryDuration.name : '' %}
  533.     {% if pc.ClassCategory2 %}
  534.       deliveryMap['{{ c1 }}'] = deliveryMap['{{ c1 }}'] || {};
  535.       deliveryMap['{{ c1 }}']['{{ c2 }}'] = '{{ label|e('js') }}';
  536.     {% else %}
  537.       deliveryMap['{{ c1 }}'] = '{{ label|e('js') }}';
  538.     {% endif %}
  539.   {% endfor %}
  540.   function updateDelivery() {
  541.     if (!el1 || !out) return;
  542.     const v1 = el1.value;
  543.     if (!v1 || v1 === '__unselected') {
  544.       out.textContent = '';
  545.       return;
  546.     }
  547.     let label = '';
  548.     if (el2 && el2.value && el2.value !== '__unselected') {
  549.       if (deliveryMap[v1] && typeof deliveryMap[v1] === 'object') {
  550.         label = deliveryMap[v1][el2.value] || '';
  551.       }
  552.     } else {
  553.       label = (typeof deliveryMap[v1] === 'string') ? deliveryMap[v1] : '';
  554.     }
  555.     out.textContent = label || '';
  556.   }
  557.   if (el1) el1.addEventListener('change', updateDelivery);
  558.   if (el2) el2.addEventListener('change', updateDelivery);
  559.   updateDelivery(); // 初期表示
  560. });
  561. </script>
  562. <script>
  563. document.addEventListener('DOMContentLoaded', function () {
  564.   const form = document.querySelector('#form1');
  565.   const select1 = document.getElementById('classcategory_id1');
  566.   const qtySelector = 'select[name="quantity"], input[name="quantity"]';
  567.   function getQtyEl() {
  568.     return document.querySelector(qtySelector);
  569.   }
  570.   function getFixedQty(v1) {
  571.     if (v1 === '291' || v1 === '289') return '50';
  572.     if (v1 === '292' || v1 === '290') return '90';
  573.     return null;
  574.   }
  575.   function enforceQuantity(showAlert = false) {
  576.     const selected = select1?.value;
  577.     const qtyEl = getQtyEl();
  578.     const fixed = getFixedQty(selected);
  579.     if (!selected || !qtyEl || !fixed) return;
  580.     if (qtyEl.value !== fixed) {
  581.       if (showAlert) alert(`この規格では数量は${fixed}個に固定されています。`);
  582.       qtyEl.value = fixed;
  583.     }
  584.   }
  585.   // 数量変更時に自動補正
  586.   document.addEventListener('change', function (e) {
  587.     if (e.target.matches(qtySelector) || e.target.id === 'classcategory_id1') {
  588.       enforceQuantity(false);
  589.     }
  590.   });
  591.   // フォーム送信時にも補正とアラート
  592.   if (form) {
  593.     form.addEventListener('submit', function () {
  594.       enforceQuantity(true);
  595.     });
  596.   }
  597. });
  598. </script>
  599. <script>
  600. document.addEventListener('DOMContentLoaded', function () {
  601.   const select1 = document.getElementById('classcategory_id1');
  602.   const form = document.querySelector('#form1');
  603.   function getQtyEl() {
  604.     return document.querySelector('select[name="quantity"], input[name="quantity"]');
  605.   }
  606.   function getFixedQty(v1) {
  607.     if (v1 === '291' || v1 === '289') return '50';
  608.     if (v1 === '292' || v1 === '290') return '90';
  609.     return null;
  610.   }
  611.   function enforceQuantity(showAlert = false) {
  612.     const selected = select1?.value;
  613.     const qtyEl = getQtyEl();
  614.     const fixed = getFixedQty(selected);
  615.     if (!selected || !qtyEl || !fixed) return;
  616.     if (qtyEl.value !== fixed) {
  617.       if (showAlert) alert(`この規格では数量は ${fixed} 個に固定されています。`);
  618.       qtyEl.value = fixed;
  619.     }
  620.   }
  621.   function attachQtyWatcher() {
  622.     const qtyEl = getQtyEl();
  623.     if (!qtyEl) return;
  624.     qtyEl.removeEventListener('change', qtyEl._handler || (() => {}));
  625.     qtyEl._handler = () => enforceQuantity(false);
  626.     qtyEl.addEventListener('change', qtyEl._handler);
  627.   }
  628.   // セレクト1が変わるたびに数量制御イベント再アタッチ
  629.   select1?.addEventListener('change', () => {
  630.     setTimeout(() => {
  631.       enforceQuantity(false);
  632.       attachQtyWatcher();
  633.     }, 100);
  634.   });
  635.   // is_print変更時もセレクトが入れ替わるため再アタッチ
  636.   const isPrint = document.getElementById('is_print');
  637.   isPrint?.addEventListener('change', () => {
  638.     setTimeout(() => {
  639.       enforceQuantity(false);
  640.       attachQtyWatcher();
  641.     }, 100);
  642.   });
  643.   // 最初の数量セレクトにも適用
  644.   setTimeout(() => {
  645.     enforceQuantity(false);
  646.     attachQtyWatcher();
  647.   }, 300);
  648.   // フォーム送信時チェック
  649.   form?.addEventListener('submit', function (e) {
  650.     enforceQuantity(true);
  651.   });
  652. });
  653. </script>
  654. {% endif %}//カレンダー終わり
  655. {% endblock %}
  656. {% block main %}
  657.     <div class="ec-productRole">
  658.         <div class="ec-grid2">
  659.             <div class="ec-grid2__cell">
  660.                 <div class="ec-sliderItemRole">
  661.                     {# 関連カテゴリ #}
  662.                     {% if Product.ProductCategories is not empty %}
  663.                         <div class="ec-productRole__category">
  664.                             <div>{{ '関連カテゴリ'|trans }}</div>
  665.                             {% for ProductCategory in Product.ProductCategories %}
  666.                                 <ul>
  667.                                     <li>
  668.                                         {% for Category in ProductCategory.Category.path %}
  669.                                             <a href="{{ url('product_list') }}?category_id={{ Category.id }}">{{ Category.name }}</a> {%- if loop.last == false %}
  670.                                             <span>></span>{% endif -%}
  671.                                         {% endfor %}
  672.                                     </li>
  673.                                 </ul>
  674.                             {% endfor %}
  675.                         </div>
  676.                     {% endif %}
  677.                 </div>
  678.                 <div class="ec-sliderItemRole">
  679.                     <div class="item_visual">
  680.                         {% for ProductImage in Product.ProductImage %}
  681.                             <div class="slide-item"><img src="{{ asset(ProductImage, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" width="550" height="550"{% if loop.index > 1 %} loading="lazy"{% endif %}></div>
  682.                         {% else %}
  683.                             <div class="slide-item"><img src="{{ asset(''|no_image_product, 'save_image') }}" alt="{{ loop.first ? Product.name : '' }}" width="550" height="550"></div>
  684.                         {% endfor %}
  685.                     </div>
  686.                     <div class="item_nav">
  687.                         {% for ProductImage in Product.ProductImage %}
  688.                             <div class="slideThumb" data-index="{{ loop.index0 }}"><img src="{{ asset(ProductImage, 'save_image') }}" alt="" width="133" height="133" loading="lazy"></div>
  689.                         {% endfor %}
  690.                     </div>
  691.                 </div>
  692.                 {% set sampleTagIds = [8, 12] %}
  693. {% set hasSampleTag = false %}
  694. {% for ProductTag in Product.ProductTag %}
  695.     {% if ProductTag.Tag.id in sampleTagIds %}
  696.         {% set hasSampleTag = true %}
  697.     {% endif %}
  698. {% endfor %}
  699. {% if hasSampleTag %}
  700.                 <div class="ec-sliderItemRole">
  701.                     <p style="text-align: center;">
  702.                         <a class="ec-blockBtn--cancel" href="/contact?product_id={{ Product.id }}" target="_blank">サンプル請求</a>
  703.                     </p>
  704.                 </div>
  705.                 <div class="ec-sliderItemRole">
  706.                     <p><b>【サンプルについて】</b><br>
  707.                     <b><サンプル無料マークの商品について></b><br>
  708.                     こちらのの商品は3点まで組み合わせ自由、無料で発送いたします。<br>
  709.                     <b><サンプルレンタルOKマークの商品について></b><br>
  710.                     こちらの商品は3点まで組み合わせ自由、貸出いたします。(※発送料は弊社負担ですが、ご返却送料はお客様ご負担となります。※サンプル到着後2週間以内にご返却ください。)</p><br>
  711.                     <p>※在庫状況により、ご要望に添えない場合がございます。ご了承ください。</p>
  712.                 </div>
  713. {% endif %}
  714.             </div>
  715.             <div class="ec-grid2__cell">
  716.                 <div class="ec-productRole__profile">
  717.                     {# 商品名 #}
  718.                     <div class="ec-productRole__title">
  719.                         <h2 class="ec-headingTitle">{{ Product.name }}</h2>
  720.                     </div>
  721.                     {# タグ #}
  722.                     <ul class="ec-productRole__tags">
  723.                         {% for Tag in Product.Tags %}
  724.                             <li class="ec-productRole__tag tag_{{ Tag.id }}">{{ Tag }}</li>
  725.                         {% endfor %}
  726.                     </ul>
  727.                     {# 通常価格 #}
  728.                     {#{% if Product.hasProductClass -%}
  729.                         <div class="ec-productRole__priceRegular">
  730.                             {% if Product.getPrice01Min is not null and Product.getPrice01IncTaxMin == Product.getPrice01IncTaxMax %}
  731.                                 <span class="ec-productRole__priceRegularPrice">{{ '通常価格'|trans }}:<span class="price01-default">{{ Product.getPrice01IncTaxMin|price }}</span></span>
  732.                                 <span class="ec-productRole__priceRegularTax">{{ '税込'|trans }}</span>
  733.                             {% elseif Product.getPrice01Min is not null and Product.getPrice01Max is not null %}
  734.                                 <span class="ec-productRole__priceRegularPrice">{{ '通常価格'|trans }}:<span class="price01-default">{{ Product.getPrice01IncTaxMin|price }}~ {{ Product.getPrice01IncTaxMax|price }}</span></span>
  735.                                 <span class="ec-productRole__priceRegularTax">{{ '税込'|trans }}</span>
  736.                             {% endif %}
  737.                         </div>
  738.                     {% else %}
  739.                         {% if Product.getPrice01Max is not null %}
  740.                             <span class="ec-productRole__priceRegularPrice">{{ '通常価格'|trans }}:{{ Product.getPrice01IncTaxMin|price }}</span>
  741.                             <span class="ec-productRole__priceRegularTax">{{ '税込'|trans }}</span>
  742.                         {% endif %}
  743.                     {% endif %}#}
  744.                     {# 販売価格 #}
  745.                     <div class="ec-productRole__price">
  746.                         {% if Product.hasProductClass -%}
  747.                             {% if Product.getPrice02IncTaxMin == Product.getPrice02IncTaxMax %}
  748.                                 <div class="ec-price">
  749.                                     <span class="ec-price__price price02-default">{{ Product.getPrice02IncTaxMin|price }}</span>
  750.                                     <span class="ec-price__tax">{{ '税込'|trans }}</span>
  751.                                     <span class="ec-price__price price_no_tax price_no_tax_first">{{ Product.getPrice02Min|price }}</span>
  752.                                     <span class="ec-price__tax price_no_tax">{{ '税抜'|trans }}</span>
  753.                                 </div>
  754.                             {% else %}
  755.                                 <div class="ec-price">
  756.                                     <span class="ec-price__price price02-default">{{ Product.getPrice02IncTaxMin|price }} ~ {{ Product.getPrice02IncTaxMax|price }}</span>
  757.                                     <span class="ec-price__tax">{{ '税込'|trans }}</span><br>
  758.                                     <span class="ec-price__price price_no_tax">{{ Product.getPrice02Min|price }} ~ {{ Product.getPrice02Max|price }}</span>
  759.                                     <span class="ec-price__tax price_no_tax">{{ '税抜'|trans }}</span>
  760.                                 </div>
  761.                             {% endif %}
  762.                         {% else %}
  763.                             <div class="ec-price">
  764.                                 <span class="ec-price__price">{{ Product.getPrice02IncTaxMin|price }}</span>
  765.                                 <span class="ec-price__tax">{{ '税込'|trans }}</span>
  766.                                 <span class="ec-price__price price_no_tax price_no_tax_first">{{ Product.getPrice02Min|price }}</span>
  767.                                 <span class="ec-price__tax price_no_tax">{{ '税抜'|trans }}</span>
  768.                             </div>
  769.                         {% endif %}
  770.                     </div>
  771.                     {# 商品コード #}
  772.                     {% if Product.code_min is not empty %}
  773.                         <div class="ec-productRole__code">
  774.                             {{ '商品コード'|trans }}: <span class="product-code-default">{{ Product.code_min }}{% if Product.code_min != Product.code_max %} ~ {{ Product.code_max }}{% endif %}</span>
  775.                         </div>
  776.                     {% endif %}
  777.                     {% if not is_calendar %}
  778.                         <div class="ec-productRole__code">
  779.                               {{ '在庫'|trans }}: <span class="product-stock-quantity-default"></span>
  780.                         </div>
  781.                     {% endif %}
  782.                     {% if is_calendar %}
  783.                     <div class="ec-productRole__code">
  784.                         発送日目安:<span id="delivery-duration-text"></span>
  785.                     </div>
  786.                     {% endif %}
  787.                     <br>
  788.                     <div class="ec-productRole__description">{{ Product.description_detail|raw|nl2br }}
  789.                     </div>
  790.                     <form action="{{ url('product_add_cart', {id:Product.id}) }}" method="post" id="form1" name="form1">
  791.                         {% if Product.stock_find %}
  792.                             <div class="ec-productRole__actions">
  793.                                 {% if form.classcategory_id1 is defined %}
  794.                                     <div class="ec-select">
  795.                                         {{ form_row(form.classcategory_id1, {'label':false}) }}
  796.                                         {{ form_errors(form.classcategory_id1) }}
  797.                                     </div>
  798.                                     {% if form.classcategory_id2 is defined %}
  799.                                         <div class="ec-select">
  800.                                             {{ form_row(form.classcategory_id2, {'label':false}) }}
  801.                                             {{ form_errors(form.classcategory_id2) }}
  802.                                         </div>
  803.                                     {% endif %}
  804.                                 {% endif %}
  805.                                 {% if Product.product_print_type == false %}
  806.                                     {% set colors = Product.product_print_color|split(',') %}
  807.                                     {% if Product.product_print_type == false and colors|length > 0 %}
  808.                                         <div class="row mb-2">
  809.                                             <div class="col-4">
  810.                                                 <div class="ec-select">
  811.                                                     {{ form_widget(form.is_print, {'attr': {'style':'min-width:unset', 'id': 'is_print'}}) }}
  812.                                                     {{ form_errors(form.is_print) }}
  813.                                                 </div>
  814.                                             </div>
  815.                                             <div class="col-6 form-is_repurchase" style="display: none">
  816.                                                 <div class="form-check mt-2">
  817.                                                     {{ form_widget(form.is_repurchase,  {'label_attr': {'class': 'radio-inline'}}) }}
  818.                                                     {{ form_errors(form.is_repurchase) }}
  819.                                                 </div>
  820.                                             </div>
  821.                                         </div>
  822.                                         <div class="row form-product-color" style="display: none">
  823.                                             <div class="col-5">
  824.                                                 <div class="ec-select">
  825.                                                     <div class="mb-3">
  826.                                                         {{ form_widget(form.product_print_color, {'attr': {'style':'min-width:unset'}}) }}
  827.                                                         {{ form_errors(form.product_print_color) }}
  828.                                                     </div>
  829.                                                 </div>
  830.                                             </div>
  831.                                         </div>
  832.                                     {% endif %}
  833.                                 {% endif %}
  834.                                 <div class="row">
  835.                                     <div class="col-3">
  836.                                         <div class="{% if Product.order_unit %}ec-select{% else %}ec-numberInput{% endif %} mt-2 group-input-qty">
  837.                                             {{ form_widget(form.quantity, {'attr': {'style':'min-width:unset;display: none;', 'placeholder': '数量'}}) }}
  838. {#                                            {{ form_widget(form.quantity_raw, {'attr': {'style':'min-width: unset;display: none', 'placeholder': '数量'}}) }}#}
  839.                                             {{ form_errors(form.quantity) }}
  840.                                         </div>
  841.                                     </div>
  842.                                     <div class="col-8">
  843.                                         <button type="submit" class="ec-blockBtn--action add-cart">
  844.                                             {{ 'カートに入れる'|trans }}
  845.                                         </button>
  846.                                     </div>
  847.                                 </div>
  848.                             </div>
  849.                         {% else %}
  850.                             <div class="ec-productRole__btn">
  851.                                 <button type="button" class="ec-blockBtn--action" disabled="disabled">
  852.                                     {{ 'ただいま品切れ中です。'|trans }}
  853.                                 </button>
  854.                             </div>
  855.                         {% endif %}
  856.                         {{ form_rest(form) }}
  857.                     </form>
  858.                     <div class="ec-modal">
  859.                         <div class="ec-modal-overlay">
  860.                             <div class="ec-modal-wrap">
  861.                                 <span class="ec-modal-close"><span class="ec-icon"><img src="{{ asset('assets/icon/cross-dark.svg') }}" alt=""/></span></span>
  862.                                 <div id="ec-modal-header" class="text-center">{{ 'カートに追加しました。'|trans }}</div>
  863.                                 <div class="ec-modal-box">
  864.                                     <div class="ec-role">
  865.                                         <span class="ec-inlineBtn--cancel">{{ 'お買い物を続ける'|trans }}</span>
  866.                                         <a href="{{ url('cart') }}" class="ec-inlineBtn--action">{{ 'カートへ進む'|trans }}</a>
  867.                                     </div>
  868.                                 </div>
  869.                             </div>
  870.                         </div>
  871.                     </div>
  872.                     <div class="ec-productRole__SNS">
  873.                         <a href="https://twitter.com/share" class="twitter-share-button" data-via="lemanze" data-lang="ja" data-count="none">ツイート</a>
  874.                         <script>
  875.                             !function(d,s,id){
  876.                                 var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';
  877.                                 if(!d.getElementById(id)){
  878.                                     js=d.createElement(s);
  879.                                     js.id=id;
  880.                                     js.src=p+'://platform.twitter.com/widgets.js';
  881.                                     fjs.parentNode.insertBefore(js,fjs);
  882.                                 }
  883.                             }(document, 'script', 'twitter-wjs');
  884.                         </script>
  885.                         <div class="line-it-button" data-lang="ja" data-type="share-a" data-env="REAL" data-url="{{ url('product_detail', {'id': Product.id}) }}" data-color="default" data-size="small" data-count="true" data-ver="3" style="display: none;"></div>
  886.                         <script src="https://www.line-website.com/social-plugins/js/thirdparty/loader.min.js" async="async" defer="defer"></script>
  887.                     </div>
  888.                     {% if BaseInfo.option_favorite_product %}
  889.                         <form action="{{ url('product_add_favorite', {id:Product.id}) }}" method="post">
  890.                             <div class="ec-productRole__btn">
  891.                                 {% if is_favorite == false %}
  892.                                     <button type="submit" id="favorite" class="ec-blockBtn--cancel">
  893.                                         {{ 'お気に入りに追加'|trans }}
  894.                                     </button>
  895.                                 {% else %}
  896.                                     <button type="submit" id="favorite" class="ec-blockBtn--cancel"
  897.                                             disabled="disabled">{{ 'お気に入りに追加済です。'|trans }}
  898.                                     </button>
  899.                                 {% endif %}
  900.                             </div>
  901.                         </form>
  902.                     {% endif %}
  903.                     {% if Product.freearea %}
  904.                          <div class="ec-productRole__description">
  905.                              {{ include(template_from_string(Product.freearea|raw|nl2br)) }}
  906.                         </div>
  907.                     {% endif %}
  908.                 </div>
  909.             </div>
  910.         </div>
  911.     </div>
  912. {% endblock %}