(function ($, window, document) { 'use strict'; function ensureStyles() { if (document.getElementById('ekyc-slot-uploader-style')) { return; } var css = '' + '.ekyc-slot-grid{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;}' + '.ekyc-slot-wrap{width:66px;position:relative;}' + '.ekyc-plus-slot{width:66px;height:66px;border:1px solid #cfd3d7;background:#f7f7f7;color:#9aa0a6;font-size:34px;line-height:62px;text-align:center;cursor:pointer;border-radius:2px;padding:0;}' + '.ekyc-plus-slot.is-uploaded{color:transparent;}' + '.ekyc-plus-slot.has-thumb{background-size:cover;background-position:center;background-repeat:no-repeat;}' + '.ekyc-slot-name{margin-top:2px;font-size:10px;line-height:1.2;color:#666;word-break:break-all;}' + '.ekyc-slot-delete{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border:none;border-radius:50%;background:rgba(0,0,0,.65);color:#fff;font-size:12px;line-height:18px;text-align:center;cursor:pointer;padding:0;}'; var style = document.createElement('style'); style.id = 'ekyc-slot-uploader-style'; style.type = 'text/css'; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } function ensurePreviewModal(modalSelector, imageSelector) { if ($(modalSelector).length > 0) { return; } var modalId = modalSelector.replace('#', ''); var imgId = imageSelector.replace('#', ''); var html = '' + ''; $('body').append(html); } function escHtml(s) { return String(s).replace(/[&<>"']/g, function (m) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]; }); } window.EkycSlotUploader = { init: function (opts) { ensureStyles(); var options = $.extend({ maxCount: 10, initialSlots: 4, existingResults: [], previewModalSelector: '#ekycPreviewModal', previewImageSelector: '#ekycPreviewImage', enableDelete: true, errorModalSelector: '#modal_box', errorMessageSelector: '#file_error_message' }, opts || {}); var $root = $(options.containerSelector); if ($root.length === 0) { return null; } ensurePreviewModal(options.previewModalSelector, options.previewImageSelector); var uploadedResults = Array.isArray(options.existingResults) ? options.existingResults.slice(0, options.maxCount) : []; function showError(msg) { if (options.errorMessageSelector) { $(options.errorMessageSelector).html(msg); } if (options.errorModalSelector) { $(options.errorModalSelector).modal('show'); } } function isAllowedImage(file) { if (!file) { return false; } var t = (file.type || '').toLowerCase(); if (t.indexOf('image/') === 0) { return true; } var n = (file.name || '').toLowerCase(); return /\.(jpg|jpeg|png|gif|webp)$/i.test(n); } function syncHiddenInputs() { var $inputContainer = $(options.hiddenSelector); $inputContainer.empty(); uploadedResults.forEach(function (r) { $inputContainer.append(''); }); } function syncSummary() { if (options.countSelector) { $(options.countSelector).text(uploadedResults.length); } if (options.listSelector) { var names = uploadedResults.map(function (r) { return escHtml(r.originalName || r.name || (r.key ? String(r.key).split('/').pop() : 'uploaded')); }); $(options.listSelector).html(names.map(function (n) { return '
  • ' + n + '
  • '; }).join('')); } if (options.summaryBoxSelector) { if (uploadedResults.length > 0) { $(options.summaryBoxSelector).show(); } else { $(options.summaryBoxSelector).hide(); } } } function openPreview(src) { if (!src) { return; } $(options.previewImageSelector).attr('src', src); $(options.previewModalSelector).modal('show'); } function makeSlot() { if ($root.find('.ekyc-slot-wrap').length >= options.maxCount) { return; } var $wrap = $('
    '); var $button = $(''); var $name = $('
    '); var $input = $(''); $button.on('click', function () { if (!$button.hasClass('is-uploaded')) { $input.trigger('click'); } }); $input.on('change', async function () { var file = this.files && this.files[0]; if (!file) { return; } if (!isAllowedImage(file)) { this.value = ''; showError('画像ファイル(jpg / jpeg / png / gif / webp)のみアップロードできます。'); return; } this.value = ''; $button.prop('disabled', true).text('...'); if (typeof options.onStart === 'function') { options.onStart(); } try { var urlRes = await fetch(options.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: file.name, type: file.type || 'application/octet-stream' }), credentials: 'same-origin' }); if (!urlRes.ok) { throw new Error('failed_to_get_upload_url'); } var payload = await urlRes.json(); var putRes = await fetch(payload.url, { method: 'PUT', headers: { 'Content-Type': file.type || payload.type || 'application/octet-stream' }, body: file }); if (!putRes.ok) { throw new Error('failed_to_upload_file'); } uploadedResults.push({ key: payload.key, originalName: file.name, name: file.name, preview: URL.createObjectURL(file) }); renderAll(); } catch (e) { $button.removeClass('is-uploaded has-thumb').text('+').prop('disabled', false).css('background-image', ''); $name.text(''); showError('書類のアップロードに失敗しました。時間をおいて再試行してください。'); } finally { if (!$button.hasClass('is-uploaded')) { $button.prop('disabled', false); } if (typeof options.onDone === 'function') { options.onDone(uploadedResults); } } }); $wrap.append($button).append($name).append($input); $root.append($wrap); } async function deleteByIndex(index) { var item = uploadedResults[index]; if (!item || !item.key || !options.deleteApiUrl || options.enableDelete === false) { return; } try { var res = await fetch(options.deleteApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: item.key }), credentials: 'same-origin' }); if (!res.ok) { throw new Error('delete_failed'); } uploadedResults.splice(index, 1); renderAll(); } catch (e) { showError('書類の削除に失敗しました。時間をおいて再試行してください。'); } } function makeUploadedSlot(item, index) { if ($root.find('.ekyc-slot-wrap').length >= options.maxCount) { return; } var preview = item && item.preview ? item.preview : ''; var name = item && (item.originalName || item.name || (item.key ? String(item.key).split('/').pop() : 'uploaded')); var $wrap = $('
    '); var $button = $(''); var $name = $('
    '); $button.css('background-image', preview ? ('url("' + preview + '")') : ''); $button.on('click', function (e) { e.preventDefault(); openPreview(preview); }); if (options.enableDelete !== false && options.deleteApiUrl) { var $delete = $(''); $delete.on('click', function (e) { e.preventDefault(); e.stopPropagation(); deleteByIndex(index); }); $wrap.append($delete); } $name.text(name || ''); $wrap.append($button).append($name); $root.append($wrap); } function renderAll() { $root.empty().addClass('ekyc-slot-grid'); uploadedResults.forEach(function (item, idx) { makeUploadedSlot(item, idx); }); syncHiddenInputs(); syncSummary(); var target = Math.min(options.maxCount, Math.max(options.initialSlots, uploadedResults.length + (uploadedResults.length < options.maxCount ? 1 : 0))); for (var i = $root.find('.ekyc-slot-wrap').length; i < target; i++) { makeSlot(); } } renderAll(); return { getResults: function () { return uploadedResults.slice(); }, render: renderAll }; } }; })(jQuery, window, document);