/** * 一覧カード用: 代表画像の選定・種別判定・並び替え(外観優先・間取りは最後)。 * Node: require(.cjs) / ブラウザ: ListCardImages グローバル * * 詳細ページのメイン画像ロジックとは独立(一覧カード専用)。 */ (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.ListCardImages = factory(); } })(typeof globalThis !== 'undefined' ? globalThis : this, function () { 'use strict'; function norm(v) { return String(v == null ? '' : v).trim(); } /** デバッグ対象 ID(コンソールに row / 画像候補の内訳を出す) */ var LIST_CARD_IMAGE_DEBUG_IDS = { 'e0053ac3-86a0-4b7a-95f1-e639b2fb5c35': true, }; function rowIdForDebug(row) { return norm(row && (row.id || row.propertyId || row.property_id)); } function shouldDebugListCardImage(row) { var id = rowIdForDebug(row); return id !== '' && LIST_CARD_IMAGE_DEBUG_IDS[id] === true; } function rowField(row, camel, snake) { if (!row || typeof row !== 'object') return undefined; if (row[camel] != null && row[camel] !== '') return row[camel]; if (snake && row[snake] != null && row[snake] !== '') return row[snake]; return undefined; } /** 文字列 / {src,url,...} / 配列の先頭有効 URL */ function resolveListImageUrl(val) { if (val == null || val === '') return ''; if (typeof val === 'string') { var s = norm(val); if (!s) return ''; if (typeof PropertyPublicImageUrl !== 'undefined' && PropertyPublicImageUrl.sanitize) { return PropertyPublicImageUrl.sanitize(s); } return s; } if (typeof val === 'number') return ''; if (Array.isArray(val)) { var i; for (i = 0; i < val.length; i++) { var u = resolveListImageUrl(val[i]); if (u) return u; } return ''; } if (typeof val === 'object') { return getImageSrcFromImg(val); } return ''; } function countArrayField(row, camel, snake) { var v = rowField(row, camel, snake); return Array.isArray(v) ? v.length : 0; } function facilitiesRawImages(row) { var out = []; var facilities = row && row.facilities; if (!Array.isArray(facilities)) return out; var fi; for (fi = 0; fi < facilities.length; fi++) { var item = facilities[fi]; if (!item || typeof item !== 'object' || item.__kind !== 'raw_data') continue; var raw = item.raw && typeof item.raw === 'object' ? item.raw : {}; out = out.concat(toArray(raw.images), toArray(raw['画像一覧'])); } return out; } function logListCardImageRowDebug(row, tag) { if (!shouldDebugListCardImage(row)) return; row = row || {}; var cells = listApiImageCellsFromRow(row); var urls = collectOrderedUrlsForRow(row); var rawData = row.raw_data && typeof row.raw_data === 'object' ? row.raw_data : null; var rawInner = rawData && rawData.raw && typeof rawData.raw === 'object' ? rawData.raw : null; console.info('[LIST_CARD_IMAGE_DEBUG] tag=' + norm(tag) + ' id=' + rowIdForDebug(row), { keys: Object.keys(row), mainImageUrl: row.mainImageUrl, main_image_url: row.main_image_url, cardImages: row.cardImages, card_images: row.card_images, imagesLite: row.imagesLite, images_lite: row.images_lite, images: row.images, raw_data_images: rawData && rawData.images, raw_data_inner_images: rawInner && rawInner['画像一覧'], facilitiesRawImagesLen: facilitiesRawImages(row).length, listApiImageCellsFromRow_length: cells.length, collectOrderedUrlsForRow_length: urls.length, sampleCells: cells.slice(0, 4), sampleUrls: urls.slice(0, 4), ListCardImages_loaded: typeof ListCardImages !== 'undefined', PropertyPublicImageUrl_loaded: typeof PropertyPublicImageUrl !== 'undefined', }); } function getImageSrcFromImg(img) { if (typeof img === 'string') { var s = norm(img); if (!s) return ''; if (typeof PropertyPublicImageUrl !== 'undefined' && PropertyPublicImageUrl.sanitize) { return PropertyPublicImageUrl.sanitize(s); } return s; } if (typeof PropertyPublicImageUrl !== 'undefined' && PropertyPublicImageUrl.fromImageCell) { return PropertyPublicImageUrl.fromImageCell(img); } if (!img || typeof img !== 'object') return ''; var fileName = norm(img.fileName); var fallback = /^https?:\/\//i.test(fileName) || fileName.indexOf('/') === 0 ? fileName : ''; var u = norm( img.publicPath || img.url || img.src || img.originalPath || img.path || img.publicUrl || img.thumbnailUrl || img.thumbnail || img.thumbUrl || img.imageUrl || img.imageURL || img.originalUrl || img.remoteUrl || img.pictureUrl || img.externalUrl || img.atbbUrl ); var raw = u || fallback; if (typeof PropertyPublicImageUrl !== 'undefined' && PropertyPublicImageUrl.sanitize) { return PropertyPublicImageUrl.sanitize(raw); } return raw; } function imageTypeText(img) { if (!img || typeof img !== 'object') return ''; return [ norm(img.type), norm(img.comment), norm(img.category), norm(img.caption), norm(img.label), norm(img.alt), norm(img.name), norm(img.kind), norm(img.title), norm(img.fileName), norm(img.publicPath), norm(img.originalPath), norm(img.path), norm(img.url), norm(img.src), ] .join(' ') .toLowerCase(); } /** 間取り系は外観・室内などの判定のあとにのみ使う(単なる "plan" には反応しない) */ function listImageHasFloorplanHint(t) { if (!t) return false; if (t.indexOf('間取') >= 0) return true; if (t.indexOf('madori') >= 0) return true; if (t.indexOf('floorplan') >= 0 || t.indexOf('floor_plan') >= 0 || t.indexOf('floor-plan') >= 0) return true; if (t.indexOf('平面図') >= 0) return true; if (t.indexOf('図面') >= 0) return true; if (/\blayout\b/.test(t)) return true; return false; } /** * exterior > building > facade > appearance > room > living > interior > common > other > floorplan * (間取りは最後。外観・建物系の手がかりがなければ floorplan) */ function classifyListImageKind(img) { var t = imageTypeText(img); if (!t) return 'other'; if ( t.indexOf('外観') >= 0 || t.indexOf('外装') >= 0 || t.indexOf('外部') >= 0 || t.indexOf('外観写真') >= 0 || t.indexOf('exterior') >= 0 || t.indexOf('gaikan') >= 0 ) { return 'exterior'; } if (t.indexOf('建物') >= 0 || t.indexOf('building') >= 0) return 'building'; if (t.indexOf('facade') >= 0 || t.indexOf('ファサード') >= 0) return 'facade'; if (/\bappearance\b/.test(t)) return 'appearance'; if (/\broom\b/.test(t) || t.indexOf('居室') >= 0 || t.indexOf('洋室') >= 0 || t.indexOf('寝室') >= 0) return 'room'; if (/\bliving\b/.test(t) || t.indexOf('リビング') >= 0 || /\bldk\b/.test(t)) return 'living'; if (t.indexOf('interior') >= 0 || t.indexOf('室内') >= 0 || t.indexOf('内観') >= 0) return 'interior'; if ( t.indexOf('共用') >= 0 || t.indexOf('エントランス') >= 0 || t.indexOf('ロビー') >= 0 || t.indexOf('entrance') >= 0 || t.indexOf('lobby') >= 0 || t.indexOf('共用部') >= 0 || /\bcommon\b/.test(t) || /\bmain\b/.test(t) || t.indexOf('メイン') >= 0 || t.indexOf('代表') >= 0 || /\bprimary\b/.test(t) ) { return 'common'; } if (listImageHasFloorplanHint(t)) return 'floorplan'; return 'other'; } function kindToRank(kind) { var order = [ 'exterior', 'building', 'facade', 'appearance', 'room', 'living', 'interior', 'common', 'other', 'floorplan', ]; var ix = order.indexOf(kind); return ix >= 0 ? ix : 50; } function collectCardImageItemsFromImageArray(imgs) { imgs = Array.isArray(imgs) ? imgs : []; var withSrc = []; for (var idx = 0; idx < imgs.length; idx++) { var img = imgs[idx]; if (img == null || img === '') continue; if (typeof img === 'string' || typeof img === 'number') { var srcStr = getImageSrcFromImg(img); if (!srcStr) continue; withSrc.push({ src: srcStr, kind: 'other', rank: kindToRank('other'), idx: idx, raw: { src: srcStr } }); continue; } if (typeof img !== 'object') continue; var src = getImageSrcFromImg(img); if (!src) continue; var kind = classifyListImageKind(img); var rank = kindToRank(kind); withSrc.push({ src: src, kind: kind, rank: rank, idx: idx, raw: img }); } var seen = Object.create(null); var dedup = []; for (var i = 0; i < withSrc.length; i++) { var x = withSrc[i]; if (seen[x.src]) continue; seen[x.src] = true; dedup.push(x); } dedup.sort(function (a, b) { if (a.rank !== b.rank) return a.rank - b.rank; return a.idx - b.idx; }); return dedup; } function toArray(v) { return Array.isArray(v) ? v : []; } function pushImageCell(buckets, cell) { if (!cell) return; if (typeof cell === 'string') { var s = norm(cell); if (!s) return; buckets.push({ src: s, url: s, publicPath: s }); return; } if (typeof cell === 'object') buckets.push(cell); } /** mainImageUrl → cardImages → imagesLite → images → facilities/raw_data 等 */ function listApiImageCellsFromRow(row) { var buckets = []; var main = resolveListImageUrl(rowField(row, 'mainImageUrl', 'main_image_url')); var imageKind = norm(rowField(row, 'imageKind', 'image_kind')) || 'exterior'; if (main) { buckets.push({ type: imageKind, kind: imageKind, src: main, url: main, publicPath: main, }); } var cardImages = rowField(row, 'cardImages', 'card_images'); if (Array.isArray(cardImages)) { cardImages.forEach(function (ci) { if (!ci) return; var cs = resolveListImageUrl(ci); if (!cs) return; var ck = typeof ci === 'object' ? norm(ci.kind || ci.type) || 'other' : 'other'; buckets.push({ type: ck, kind: ck, src: cs, url: cs, publicPath: cs, }); }); } var imagesLite = rowField(row, 'imagesLite', 'images_lite'); if (Array.isArray(imagesLite)) { imagesLite.forEach(function (lite) { if (!lite) return; var ls = resolveListImageUrl(lite); if (!ls) return; var lk = typeof lite === 'object' ? norm(lite.kind || lite.type) || 'other' : 'other'; buckets.push({ type: lk, kind: lk, category: typeof lite === 'object' ? norm(lite.category) : '', label: typeof lite === 'object' ? norm(lite.label) : '', comment: typeof lite === 'object' ? norm(lite.comment) : '', src: ls, url: ls, publicPath: ls, }); }); } toArray(row && row.images).forEach(function (img) { pushImageCell(buckets, img); }); facilitiesRawImages(row).forEach(function (img) { pushImageCell(buckets, img); }); var rawData = row && row.raw_data && typeof row.raw_data === 'object' ? row.raw_data : {}; var rawInner = rawData && rawData.raw && typeof rawData.raw === 'object' ? rawData.raw : {}; toArray(row && row.imageList).forEach(function (img) { pushImageCell(buckets, img); }); toArray(row && row.pictures).forEach(function (img) { pushImageCell(buckets, img); }); toArray(row && row.photos).forEach(function (img) { pushImageCell(buckets, img); }); toArray(row && row['画像一覧']).forEach(function (img) { pushImageCell(buckets, img); }); toArray(row && row.atbbImages).forEach(function (img) { pushImageCell(buckets, img); }); toArray(rawData.images).forEach(function (img) { pushImageCell(buckets, img); }); toArray(rawInner['画像一覧']).forEach(function (img) { pushImageCell(buckets, img); }); return buckets; } function collectCardImageItemsForRow(row) { return collectCardImageItemsFromImageArray(listApiImageCellsFromRow(row)); } function collectOrderedUrlsForRow(row) { return collectCardImageItemsForRow(row).map(function (it) { return norm(it && it.src); }).filter(Boolean); } /** API 行に mainImageUrl が無いとき images / facilities から一覧用フィールドを補う */ function enrichListRowForCard(row) { if (!row || typeof row !== 'object') return row; if (resolveListImageUrl(rowField(row, 'mainImageUrl', 'main_image_url'))) return row; var urls = collectOrderedUrlsForRow(row); if (!urls.length) return row; var kind = norm(rowField(row, 'imageKind', 'image_kind')) || 'exterior'; row.mainImageUrl = urls[0]; if (!row.imageKind) row.imageKind = kind; if (!row.main_image_url) row.main_image_url = urls[0]; var cardImages = rowField(row, 'cardImages', 'card_images'); if (!Array.isArray(cardImages) || !cardImages.length) { row.cardImages = urls.slice(0, 2).map(function (u, ix) { return { src: u, kind: ix === 0 ? kind : 'other' }; }); } var lite = rowField(row, 'imagesLite', 'images_lite'); if (!Array.isArray(lite) || !lite.length) { row.imagesLite = collectCardImageItemsForRow(row) .slice(0, 8) .map(function (it) { return { src: it.src, kind: it.kind, type: it.kind, }; }); } return row; } function urlKindOverlayFromRow(row) { var o = Object.create(null); collectCardImageItemsForRow(row).forEach(function (it) { var u = norm(it && it.src); if (!u) return; o[u] = norm(it && it.kind) || 'other'; }); return o; } function collectOrderedUrlsForBuildingGroup(group) { var ordered = []; var seen = Object.create(null); function pushUrl(u) { u = norm(u); if (!u || seen[u]) return; seen[u] = true; ordered.push(u); } var rep = group && group.representative; if (rep) collectOrderedUrlsForRow(rep).forEach(pushUrl); toArray(group && group.rooms).forEach(function (r) { collectOrderedUrlsForRow(r).forEach(pushUrl); }); return ordered; } function urlKindOverlayForBuildingGroup(group) { var o = Object.create(null); var rep = group && group.representative; if (rep) Object.assign(o, urlKindOverlayFromRow(rep)); toArray(group && group.rooms).forEach(function (r) { Object.assign(o, urlKindOverlayFromRow(r)); }); return o; } function pickListRepresentativeImage(items) { if (!items || !items.length) return { src: '', kind: 'other', index: 0 }; var bestIdx = 0; var i; for (i = 1; i < items.length; i++) { if (items[i].rank < items[bestIdx].rank) bestIdx = i; else if (items[i].rank === items[bestIdx].rank && (items[i].idx || 0) < (items[bestIdx].idx || 0)) bestIdx = i; } return { src: items[bestIdx].src, kind: items[bestIdx].kind, index: bestIdx }; } function cardListImageStateFromImages(imgs) { var images = collectCardImageItemsFromImageArray(imgs); var pick = pickListRepresentativeImage(images); if (pick.index > 0) { var chosen = images[pick.index]; images = images.slice(0, pick.index).concat(images.slice(pick.index + 1)); images.unshift(chosen); } return { images: images, initialIndex: 0 }; } function cardListImageState(row) { return cardListImageStateFromImages(listApiImageCellsFromRow(row)); } /** * merge 後の urls/kinds を、一覧代表の優先順に先頭へ回転(号室サムネ等には使わない)。 * 優先: exterior > building > facade > appearance > room > living > interior > common > other > floorplan */ function rotateSlidesByPreferredKind(urls, kinds) { urls = Array.isArray(urls) ? urls.slice() : []; kinds = Array.isArray(kinds) ? kinds.slice() : []; if (!urls.length || urls.length !== kinds.length || urls.length <= 1) { return { urls: urls, kinds: kinds }; } var preferred = [ 'exterior', 'building', 'facade', 'appearance', 'room', 'living', 'interior', 'common', 'other', 'floorplan', ]; var pick = -1; var pi; for (pi = 0; pi < preferred.length; pi++) { var target = preferred[pi]; var j; for (j = 0; j < kinds.length; j++) { if (kinds[j] === target) { pick = j; break; } } if (pick >= 0) break; } if (pick <= 0) return { urls: urls, kinds: kinds }; function rot(arr, k) { return arr.slice(k).concat(arr.slice(0, k)); } return { urls: rot(urls, pick), kinds: rot(kinds, pick) }; } function ensureCardPhotoSliderInner(thumb) { var inner = thumb.querySelector('.card-photo-slider-inner'); if (inner) return inner; inner = document.createElement('div'); inner.className = 'card-photo-slider-inner list-card-thumb-inner'; var nodes = Array.prototype.slice.call(thumb.childNodes); nodes.forEach(function (node) { if (node.nodeType === 1 || node.nodeType === 3) inner.appendChild(node); }); thumb.appendChild(inner); return inner; } function initCommonListCardImageNav(root) { var doc = root || document; var cards = doc.querySelectorAll('.property-card[data-card-template="common-list-card"]'); console.info('[common-card-init]', { cards: cards.length, withImages: doc.querySelectorAll('.property-card[data-images]').length, }); cards.forEach(function (card, cardIdx) { var thumb = card.querySelector('.list-card-thumb'); if (!thumb) return; var id = String(card.getAttribute('data-property-id') || card.id || cardIdx + 1); var images = []; try { images = JSON.parse(String(card.getAttribute('data-images') || thumb.getAttribute('data-images') || '[]')); } catch (_e) { images = []; } if (!Array.isArray(images)) images = []; images = images .map(function (u) { return String(u == null ? '' : u).trim(); }) .filter(Boolean); if (!images.length) { var firstSrc = thumb.querySelector('.list-card-image'); var src = firstSrc && firstSrc.getAttribute ? String(firstSrc.getAttribute('src') || '').trim() : ''; if (src) images = [src]; } var total = images.length; var rawInit = card.getAttribute('data-initial-image-index') || thumb.getAttribute('data-initial-image-index') || '0'; var imageKinds = []; try { imageKinds = JSON.parse(String(card.getAttribute('data-image-kinds') || '[]')); } catch (_ek) { imageKinds = []; } if (!Array.isArray(imageKinds) || imageKinds.length !== images.length) { imageKinds = images.map(function () { return 'photo'; }); } if (total >= 2 && imageKinds.length === images.length && typeof rotateSlidesByPreferredKind === 'function') { var rr = rotateSlidesByPreferredKind(images, imageKinds); images = rr.urls; imageKinds = rr.kinds; } var initialIndex = total >= 2 && imageKinds.length === images.length ? 0 : Math.max(0, Math.min(Number(rawInit) || 0, Math.max(0, total - 1))); card.setAttribute('data-images', JSON.stringify(images)); card.setAttribute('data-image-kinds', JSON.stringify(imageKinds)); card.setAttribute('data-initial-image-index', String(initialIndex)); var imgEl = thumb.querySelector('.list-card-image'); var navHost = thumb; if (total >= 2) { navHost = ensureCardPhotoSliderInner(thumb); thumb.classList.add('js-card-slider'); imgEl = navHost.querySelector('.list-card-image') || thumb.querySelector('.list-card-image'); } if (!imgEl && total > 0) { imgEl = document.createElement('img'); imgEl.className = 'list-card-image'; imgEl.alt = ''; imgEl.loading = 'lazy'; imgEl.decoding = 'async'; navHost.appendChild(imgEl); } if (!imgEl) return; var prevBtn = navHost.querySelector('.list-card-img-nav--prev'); var nextBtn = navHost.querySelector('.list-card-img-nav--next'); if (total >= 2) { if (!prevBtn) { prevBtn = document.createElement('button'); prevBtn.type = 'button'; prevBtn.className = 'card-photo-nav card-photo-nav--prev list-card-img-nav list-card-img-nav--prev'; prevBtn.setAttribute('aria-label', '前の画像'); prevBtn.textContent = '<'; navHost.insertBefore(prevBtn, imgEl); } if (!nextBtn) { nextBtn = document.createElement('button'); nextBtn.type = 'button'; nextBtn.className = 'card-photo-nav card-photo-nav--next list-card-img-nav list-card-img-nav--next'; nextBtn.setAttribute('aria-label', '次の画像'); nextBtn.textContent = '>'; navHost.appendChild(nextBtn); } } else { if (prevBtn) prevBtn.remove(); if (nextBtn) nextBtn.remove(); } var hasButtons = !!(prevBtn && nextBtn && total >= 2); var idx = initialIndex; function syncImage() { if (!images.length) return; imgEl.setAttribute('src', images[idx]); var slot = imageKinds[idx] === 'floorplan' ? 'floorplan' : 'photo'; imgEl.classList.toggle('is-floorplan', slot === 'floorplan'); imgEl.setAttribute('data-kind', slot === 'floorplan' ? 'floorplan' : 'photo'); thumb.classList.toggle('is-floorplan', slot === 'floorplan'); var slideImg = thumb.querySelector('[data-card-slide-img]'); if (slideImg && slideImg !== imgEl) { slideImg.setAttribute('src', images[idx]); slideImg.setAttribute('data-index', String(idx)); slideImg.classList.toggle('is-floorplan', slot === 'floorplan'); slideImg.setAttribute('data-kind', slot === 'floorplan' ? 'floorplan' : 'photo'); } if (slideImg) { slideImg.setAttribute('data-images', JSON.stringify(images)); slideImg.setAttribute('data-image-kinds', JSON.stringify(imageKinds)); slideImg.setAttribute('data-index', String(idx)); } else if (imgEl && imgEl.hasAttribute('data-card-slide-img')) { imgEl.setAttribute('data-images', JSON.stringify(images)); imgEl.setAttribute('data-image-kinds', JSON.stringify(imageKinds)); imgEl.setAttribute('data-index', String(idx)); } console.info('[LIST_IMAGE_NAV] id=' + id + ' index=' + idx); } syncImage(); if (!hasButtons) return; if (thumb.getAttribute('data-nav-bound') === '1') return; thumb.setAttribute('data-nav-bound', '1'); prevBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); idx = (idx - 1 + images.length) % images.length; syncImage(); }); nextBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); idx = (idx + 1) % images.length; syncImage(); }); }); } return { getImageSrcFromImg: getImageSrcFromImg, imageTypeText: imageTypeText, classifyListImageKind: classifyListImageKind, kindToRank: kindToRank, logListCardImageRowDebug: logListCardImageRowDebug, shouldDebugListCardImage: shouldDebugListCardImage, enrichListRowForCard: enrichListRowForCard, listApiImageCellsFromRow: listApiImageCellsFromRow, collectCardImageItemsForRow: collectCardImageItemsForRow, collectCardImageItemsFromImageArray: collectCardImageItemsFromImageArray, collectOrderedUrlsForRow: collectOrderedUrlsForRow, collectOrderedUrlsForBuildingGroup: collectOrderedUrlsForBuildingGroup, urlKindOverlayFromRow: urlKindOverlayFromRow, urlKindOverlayForBuildingGroup: urlKindOverlayForBuildingGroup, pickListRepresentativeImage: pickListRepresentativeImage, cardListImageState: cardListImageState, cardListImageStateFromImages: cardListImageStateFromImages, rotateSlidesByPreferredKind: rotateSlidesByPreferredKind, initCommonListCardImageNav: initCommonListCardImageNav, }; });