編集の要約なし |
編集の要約なし |
||
| 1行目: | 1行目: | ||
/* ======================================== | /* ======================================== | ||
* 屋台比較 | * 屋台比較 | ||
* placement_id 正式版 | |||
* | * | ||
* | * 最大4出店 | ||
* | * | ||
* | * localStorage: | ||
* | * matsuriWikiComparePlacements | ||
* | |||
* 1 placement = | |||
* 1 festival + 1 year + 1 venue + 1 stall | |||
* ======================================== */ | * ======================================== */ | ||
| 16行目: | 20行目: | ||
'use strict'; | 'use strict'; | ||
const STORAGE_KEY = ' | const STORAGE_KEY = | ||
'matsuriWikiComparePlacements'; | |||
const MAX_COMPARE = 4; | const MAX_COMPARE = 4; | ||
const api = new mw.Api(); | const api = | ||
new mw.Api(); | |||
/* | /* | ||
* | * ページ表示中だけ使うキャッシュ | ||
*/ | */ | ||
const | const placementInfoCache = {}; | ||
| 32行目: | 38行目: | ||
* ===================================== */ | * ===================================== */ | ||
function | function getComparePlacements() { | ||
const raw = mw.storage.get( STORAGE_KEY ); | const raw = | ||
mw.storage.get( | |||
STORAGE_KEY | |||
); | |||
if ( !raw ) { | if ( !raw ) { | ||
| 42行目: | 51行目: | ||
try { | try { | ||
const ids = JSON.parse( raw ); | const ids = | ||
JSON.parse( raw ); | |||
if ( !Array.isArray( ids ) ) { | if ( !Array.isArray( ids ) ) { | ||
| 48行目: | 58行目: | ||
} | } | ||
return [ ...new Set( | return [ ...new Set( | ||
ids | ids | ||
.map( String ) | .map( String ) | ||
.filter( function ( id ) { | .filter( | ||
function ( id ) { | |||
return /^\d+$/.test( id ); | |||
) ].slice( 0, MAX_COMPARE ); | } | ||
) | |||
) ].slice( | |||
0, | |||
MAX_COMPARE | |||
); | |||
} catch ( e ) { | } catch ( e ) { | ||
| 69行目: | 80行目: | ||
function | function saveComparePlacements( ids ) { | ||
mw.storage.set( | mw.storage.set( | ||
| 80行目: | 91行目: | ||
/* ===================================== | /* ===================================== | ||
* | * Cargo共通処理 | ||
* ===================================== */ | |||
function cargoQuery( | |||
table, | |||
fields, | |||
where, | |||
limit | |||
) { | |||
const params = { | |||
action: 'cargoquery', | |||
tables: table, | |||
fields: fields, | |||
limit: limit || 100, | |||
format: 'json' | |||
}; | |||
if ( where ) { | |||
params.where = where; | |||
} | |||
return api.get( | |||
params | |||
).then( | |||
function ( data ) { | |||
if ( | |||
!data || | |||
!Array.isArray( | |||
data.cargoquery | |||
) | |||
) { | |||
return []; | |||
} | |||
return data.cargoquery.map( | |||
function ( item ) { | |||
return item.title || item; | |||
} | |||
); | |||
} | |||
); | |||
} | |||
function makeInClause( ids ) { | |||
return ids | |||
.map( String ) | |||
.filter( | |||
function ( id ) { | |||
return /^\d+$/.test( id ); | |||
} | |||
) | |||
.join( ',' ); | |||
} | |||
function uniqueIds( values ) { | |||
return [ | |||
...new Set( | |||
values | |||
.map( String ) | |||
.filter( | |||
function ( id ) { | |||
return /^\d+$/.test( id ); | |||
} | |||
) | |||
) | |||
]; | |||
} | |||
function mapBy( rows, key ) { | |||
const result = {}; | |||
rows.forEach( | |||
function ( row ) { | |||
if ( | |||
row[ key ] === undefined || | |||
row[ key ] === null | |||
) { | |||
return; | |||
} | |||
result[ | |||
String( | |||
row[ key ] | |||
) | |||
] = row; | |||
} | |||
); | |||
return result; | |||
} | |||
/* ===================================== | |||
* Placement情報取得 | |||
* ===================================== */ | * ===================================== */ | ||
function | function fetchPlacementInfo( ids ) { | ||
if ( ids.length === 0 ) { | if ( ids.length === 0 ) { | ||
| 89行目: | 214行目: | ||
} | } | ||
const result = {}; | const result = {}; | ||
const missingIds = []; | const missingIds = []; | ||
ids.forEach( function ( id ) { | ids.forEach( | ||
function ( id ) { | |||
if ( | |||
placementInfoCache[ id ] | |||
) { | |||
result[ id ] = | |||
placementInfoCache[ id ]; | |||
} else { | |||
missingIds.push( id ); | |||
} | |||
} | } | ||
); | |||
if ( missingIds.length === 0 ) { | |||
return Promise.resolve( | |||
result | |||
); | |||
} | } | ||
return cargoQuery( | |||
'FestivalStallPlacements', | |||
'placement_id=placement_id,' + | |||
' | 'stall_id=stall_id,' + | ||
missingIds | 'festival_id=festival_id,' + | ||
')'; | 'venue_id=venue_id,' + | ||
'year=year,' + | |||
'location_note=location_note', | |||
'placement_id IN (' + | |||
makeInClause( | |||
missingIds | |||
) + | |||
')', | |||
100 | |||
).then( | |||
function ( placements ) { | |||
const stallIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.stall_id; | |||
} | |||
) | |||
); | |||
const festivalIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.festival_id; | |||
} | |||
) | |||
); | |||
const venueIds = | |||
uniqueIds( | |||
placements.map( | |||
function ( row ) { | |||
return row.venue_id; | |||
} | |||
) | |||
); | |||
return Promise.all( [ | |||
stallIds.length | |||
? cargoQuery( | |||
'Stalls', | |||
'stall_id=stall_id,' + | |||
'name=stall_name,' + | |||
'_pageName=page_name', | |||
'stall_id IN (' + | |||
makeInClause( | |||
stallIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( [] ), | |||
festivalIds.length | |||
? cargoQuery( | |||
'Festivals', | |||
'festival_id=festival_id,' + | |||
'name=festival_name,' + | |||
'_pageName=page_name', | |||
'festival_id IN (' + | |||
makeInClause( | |||
festivalIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( [] ), | |||
venueIds.length | |||
? cargoQuery( | |||
'Venues', | |||
'venue_id=venue_id,' + | |||
'name=venue_name,' + | |||
'_pageName=page_name', | |||
'venue_id IN (' + | |||
makeInClause( | |||
venueIds | |||
) + | |||
')', | |||
100 | |||
) | |||
: Promise.resolve( [] ) | |||
] ).then( | |||
function ( related ) { | |||
return { | |||
placements: | |||
placements, | |||
stalls: | |||
related[ 0 ], | |||
festivals: | |||
related[ 1 ], | |||
venues: | |||
related[ 2 ] | |||
}; | |||
} | |||
); | |||
} | } | ||
).then( | |||
function ( data ) { | |||
const stallMap = | |||
mapBy( | |||
data.stalls, | |||
'stall_id' | |||
); | |||
const festivalMap = | |||
mapBy( | |||
data.festivals, | |||
'festival_id' | |||
); | |||
const venueMap = | |||
mapBy( | |||
data.venues, | |||
'venue_id' | |||
); | |||
data.placements.forEach( | |||
function ( placement ) { | |||
const placementId = | |||
String( | |||
placement.placement_id | |||
); | |||
const info = { | |||
placement: | |||
placement, | |||
stall: | |||
stallMap[ | |||
String( | |||
placement.stall_id | |||
) | |||
] || null, | |||
festival: | |||
festivalMap[ | |||
String( | |||
placement.festival_id | |||
) | |||
] || null, | |||
venue: | |||
venueMap[ | |||
String( | |||
placement.venue_id | |||
) | |||
] || null | |||
}; | |||
placementInfoCache[ | |||
placementId | |||
] = info; | |||
result[ | |||
placementId | |||
] = info; | |||
} | |||
); | |||
return result; | |||
} | |||
); | |||
} | } | ||
| 229行目: | 463行目: | ||
/* ===================================== | /* ===================================== | ||
* | * メッセージ | ||
* ===================================== */ | * ===================================== */ | ||
| 259行目: | 493行目: | ||
/* ===================================== | /* ===================================== | ||
* | * Placement比較ボタン生成 | ||
* ===================================== */ | * ===================================== */ | ||
| 268行目: | 502行目: | ||
'.stall-compare-placeholder' | '.stall-compare-placeholder' | ||
) | ) | ||
.forEach( function ( placeholder ) { | .forEach( | ||
function ( placeholder ) { | |||
if ( | |||
placeholder.dataset.initialized === | |||
'1' | |||
) { | |||
return; | return; | ||
} | |||
const prefix = | |||
'stall-compare-placeholder-'; | |||
if ( | |||
!placeholder.id.startsWith( | |||
prefix | |||
) | |||
) { | |||
return; | |||
} | |||
const placementId = | |||
placeholder.id.substring( | |||
prefix.length | |||
); | |||
if ( | |||
!/^\d+$/.test( | |||
placementId | |||
) || | |||
placementId === '0' | |||
) { | |||
return; | |||
} | |||
const button = | |||
document.createElement( | |||
'button' | |||
); | |||
button.type = | |||
'button'; | |||
'button' | |||
button.className = | |||
'stall-compare-button'; | |||
button.dataset.placementId = | |||
placementId; | |||
button.setAttribute( | |||
'aria-pressed', | |||
'false' | |||
); | |||
button.textContent = | |||
'比較に追加'; | |||
placeholder.appendChild( | |||
button | |||
); | |||
placeholder.dataset.initialized = | |||
'1'; | |||
} | |||
); | |||
} | } | ||
| 334行目: | 579行目: | ||
/* ===================================== | /* ===================================== | ||
* | * ボタン状態更新 | ||
* ===================================== */ | * ===================================== */ | ||
| 340行目: | 585行目: | ||
const ids = | const ids = | ||
getComparePlacements(); | |||
document | document | ||
| 346行目: | 592行目: | ||
'.stall-compare-button' | '.stall-compare-button' | ||
) | ) | ||
.forEach( function ( button ) { | .forEach( | ||
function ( button ) { | |||
const placementId = | |||
String( | |||
button.dataset | |||
.placementId || '' | |||
); | |||
const selected = | |||
ids.includes( | |||
placementId | |||
); | |||
if ( selected ) { | |||
button.textContent = | |||
'比較から外す'; | |||
button.classList.add( | |||
'stall-compare-button-selected' | |||
); | |||
button.setAttribute( | |||
'aria-pressed', | |||
'true' | |||
); | |||
} else { | |||
button.textContent = | |||
'比較に追加'; | |||
button.classList.remove( | |||
'stall-compare-button-selected' | |||
); | |||
button.setAttribute( | |||
'aria-pressed', | |||
'false' | |||
); | |||
} | |||
} | } | ||
); | |||
} | } | ||
| 392行目: | 645行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 固定比較トレイ生成 | ||
* ===================================== */ | * ===================================== */ | ||
| 404行目: | 657行目: | ||
return; | return; | ||
} | } | ||
const tray = | const tray = | ||
document.createElement( 'div' ); | document.createElement( | ||
'div' | |||
); | |||
tray.id = | tray.id = | ||
| 420行目: | 676行目: | ||
const header = | const header = | ||
document.createElement( 'div' ); | document.createElement( | ||
'div' | |||
); | |||
header.className = | header.className = | ||
| 431行目: | 686行目: | ||
const title = | const title = | ||
document.createElement( 'strong' ); | document.createElement( | ||
'strong' | |||
); | |||
title.className = | title.className = | ||
| 441行目: | 698行目: | ||
const count = | const count = | ||
document.createElement( 'span' ); | document.createElement( | ||
'span' | |||
); | |||
count.className = | count.className = | ||
| 447行目: | 706行目: | ||
header.appendChild( title ); | header.appendChild( | ||
header.appendChild( count ); | title | ||
); | |||
header.appendChild( | |||
count | |||
); | |||
const items = | const items = | ||
document.createElement( 'div' ); | document.createElement( | ||
'div' | |||
); | |||
items.className = | items.className = | ||
| 461行目: | 724行目: | ||
const actions = | const actions = | ||
document.createElement( 'div' ); | document.createElement( | ||
'div' | |||
); | |||
actions.className = | actions.className = | ||
| 472行目: | 734行目: | ||
const clearButton = | const clearButton = | ||
document.createElement( 'button' ); | document.createElement( | ||
'button' | |||
); | |||
clearButton.type = | clearButton.type = | ||
| 485行目: | 749行目: | ||
const compareLink = | const compareLink = | ||
document.createElement( 'a' ); | document.createElement( | ||
'a' | |||
); | |||
compareLink.className = | compareLink.className = | ||
| 491行目: | 757行目: | ||
compareLink.href = | compareLink.href = | ||
mw.util.getUrl( '屋台比較' ); | mw.util.getUrl( | ||
'屋台比較' | |||
); | |||
compareLink.textContent = | compareLink.textContent = | ||
| 527行目: | 795行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 比較トレイ更新 | ||
* ===================================== */ | * ===================================== */ | ||
| 533行目: | 801行目: | ||
createCompareTray(); | createCompareTray(); | ||
const tray = | const tray = | ||
| 545行目: | 814行目: | ||
const ids = | const ids = | ||
getComparePlacements(); | |||
| 574行目: | 843行目: | ||
if ( ids.length === 0 ) { | if ( ids.length === 0 ) { | ||
| 588行目: | 854行目: | ||
return; | return; | ||
} | } | ||
| 596行目: | 863行目: | ||
if ( compareLink ) { | if ( compareLink ) { | ||
| 633行目: | 897行目: | ||
items.textContent = | items.textContent = | ||
'屋台情報を読み込み中…'; | '屋台情報を読み込み中…'; | ||
fetchPlacementInfo( | |||
ids | ids | ||
).then( function ( | ).then( | ||
function ( placementInfo ) { | |||
const currentIds = | |||
getComparePlacements(); | |||
items.innerHTML = | |||
''; | |||
currentIds.forEach( | |||
function ( placementId ) { | |||
const info = | |||
placementInfo[ | |||
placementId | |||
]; | |||
if ( !info ) { | |||
return; | |||
} | |||
const item = | |||
document.createElement( | |||
'div' | |||
); | |||
item.className = | |||
'stall-compare-tray-item'; | |||
const text = | |||
document.createElement( | document.createElement( | ||
' | 'div' | ||
); | ); | ||
const name = | |||
info. | document.createElement( | ||
info.stall && | |||
info.stall.page_name | |||
? 'a' | |||
: 'span' | |||
); | ); | ||
if ( | |||
info.stall && | |||
info.stall.page_name | |||
) { | |||
name.href = | |||
mw.util.getUrl( | |||
info.stall.page_name | |||
); | |||
} | |||
name.className = | |||
'stall-compare-tray-item-name'; | |||
name.textContent = | |||
info.stall | |||
? info.stall.stall_name | |||
: '屋台'; | |||
const context = | |||
document.createElement( | document.createElement( | ||
' | 'div' | ||
); | ); | ||
context.className = | |||
'stall-compare-tray-item-context'; | |||
const parts = []; | |||
if ( | |||
info.placement && | |||
info.placement.year | |||
) { | |||
parts.push( | |||
info.placement.year + | |||
'年' | |||
); | |||
} | |||
if ( | |||
info.festival && | |||
info.festival.festival_name | |||
) { | |||
parts.push( | |||
info.festival | |||
.festival_name | |||
); | |||
} | |||
if ( | |||
info.venue && | |||
info.venue.venue_name | |||
) { | |||
parts.push( | |||
info.venue | |||
.venue_name | |||
); | |||
} | |||
context.textContent = | |||
parts.join( | |||
' / ' | |||
); | |||
text.appendChild( | |||
name | |||
); | |||
text.appendChild( | |||
context | |||
); | ); | ||
const removeButton = | |||
document.createElement( | |||
'button' | |||
); | |||
removeButton.type = | |||
'button'; | |||
removeButton.className = | |||
'stall-compare-tray-remove'; | |||
removeButton.dataset.placementId = | |||
placementId; | |||
removeButton.setAttribute( | |||
'aria-label', | |||
( | |||
info.stall | |||
? info.stall.stall_name | |||
: '屋台' | |||
) + | |||
'を比較候補から外す' | |||
); | |||
removeButton.textContent = | |||
'×'; | |||
item.appendChild( | |||
text | |||
); | |||
item.appendChild( | |||
removeButton | |||
); | |||
items.appendChild( | |||
item | |||
); | ); | ||
} | |||
); | |||
} | |||
).catch( | |||
function ( error ) { | |||
console.error( | |||
'比較トレイ取得エラー:', | |||
); | error | ||
); | |||
items.textContent = | |||
'比較候補を読み込めませんでした。'; | |||
} | |||
); | |||
} | } | ||
| 757行目: | 1,101行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 詳細ページ | ||
* 「比較に追加」 | |||
* ===================================== */ | * ===================================== */ | ||
| 772行目: | 1,117行目: | ||
return; | return; | ||
} | } | ||
event.preventDefault(); | event.preventDefault(); | ||
const | |||
const placementId = | |||
String( | String( | ||
button.dataset. | button.dataset | ||
.placementId || '' | |||
); | ); | ||
if ( | if ( | ||
!/^\d+$/.test( | !/^\d+$/.test( | ||
placementId | |||
) | |||
) { | ) { | ||
return; | return; | ||
| 794行目: | 1,145行目: | ||
let ids = | let ids = | ||
getComparePlacements(); | |||
const index = | const index = | ||
ids.indexOf( | ids.indexOf( | ||
placementId | |||
); | ); | ||
/* | /* | ||
* | * すでに選択中 | ||
*/ | */ | ||
if ( index !== -1 ) { | if ( index !== -1 ) { | ||
| 814行目: | 1,164行目: | ||
); | ); | ||
saveComparePlacements( | |||
ids | ids | ||
); | ); | ||
| 820行目: | 1,170行目: | ||
updateCompareButtons(); | updateCompareButtons(); | ||
updateCompareTray(); | updateCompareTray(); | ||
if ( control ) { | if ( control ) { | ||
| 837行目: | 1,188行目: | ||
/* | /* | ||
* | * 最大4件 | ||
*/ | */ | ||
if ( | if ( | ||
| 848行目: | 1,199行目: | ||
showMessage( | showMessage( | ||
control, | control, | ||
' | '比較できる出店は最大4件です。', | ||
true | true | ||
); | ); | ||
| 860行目: | 1,211行目: | ||
ids.push( | ids.push( | ||
placementId | |||
); | ); | ||
saveComparePlacements( | |||
ids | ids | ||
); | ); | ||
updateCompareButtons(); | updateCompareButtons(); | ||
| 876行目: | 1,229行目: | ||
control, | control, | ||
'比較候補に追加しました(' + | '比較候補に追加しました(' + | ||
ids.length + | |||
'/4)。', | |||
false | false | ||
); | ); | ||
| 888行目: | 1,241行目: | ||
/* ===================================== | /* ===================================== | ||
* | * トレイから1件削除 | ||
* ===================================== */ | * ===================================== */ | ||
| 903行目: | 1,256行目: | ||
return; | return; | ||
} | } | ||
event.preventDefault(); | event.preventDefault(); | ||
const | |||
const placementId = | |||
String( | String( | ||
button.dataset. | button.dataset | ||
.placementId || '' | |||
); | ); | ||
let ids = | let ids = | ||
getComparePlacements(); | |||
ids = | ids = | ||
ids.filter( | ids.filter( | ||
function ( id ) { | function ( id ) { | ||
return id !== | return id !== placementId; | ||
} | } | ||
); | ); | ||
saveComparePlacements( | |||
ids | ids | ||
); | ); | ||
updateCompareButtons(); | updateCompareButtons(); | ||
| 933行目: | 1,293行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 全削除 | ||
* ===================================== */ | * ===================================== */ | ||
| 948行目: | 1,308行目: | ||
return; | return; | ||
} | } | ||
event.preventDefault(); | event.preventDefault(); | ||
saveComparePlacements( | |||
[] | [] | ||
); | ); | ||
updateCompareButtons(); | updateCompareButtons(); | ||
| 963行目: | 1,326行目: | ||
/* ===================================== | /* ===================================== | ||
* | * 1件時の比較リンク無効 | ||
* ===================================== */ | * ===================================== */ | ||
| 989行目: | 1,352行目: | ||
* ===================================== */ | * ===================================== */ | ||
function | function initPlacementCompare() { | ||
createCompareButtons(); | createCompareButtons(); | ||
| 1,002行目: | 1,365行目: | ||
initPlacementCompare(); | |||
| 1,010行目: | 1,373行目: | ||
function () { | function () { | ||
initPlacementCompare(); | |||
} | } | ||
2026年8月12日 (水) 01:32時点における版
/* ========================================
* 屋台比較
* placement_id 正式版
*
* 最大4出店
*
* localStorage:
* matsuriWikiComparePlacements
*
* 1 placement =
* 1 festival + 1 year + 1 venue + 1 stall
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const STORAGE_KEY =
'matsuriWikiComparePlacements';
const MAX_COMPARE = 4;
const api =
new mw.Api();
/*
* ページ表示中だけ使うキャッシュ
*/
const placementInfoCache = {};
/* =====================================
* localStorage
* ===================================== */
function getComparePlacements() {
const raw =
mw.storage.get(
STORAGE_KEY
);
if ( !raw ) {
return [];
}
try {
const ids =
JSON.parse( raw );
if ( !Array.isArray( ids ) ) {
return [];
}
return [ ...new Set(
ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
) ].slice(
0,
MAX_COMPARE
);
} catch ( e ) {
return [];
}
}
function saveComparePlacements( ids ) {
mw.storage.set(
STORAGE_KEY,
JSON.stringify( ids )
);
}
/* =====================================
* Cargo共通処理
* ===================================== */
function cargoQuery(
table,
fields,
where,
limit
) {
const params = {
action: 'cargoquery',
tables: table,
fields: fields,
limit: limit || 100,
format: 'json'
};
if ( where ) {
params.where = where;
}
return api.get(
params
).then(
function ( data ) {
if (
!data ||
!Array.isArray(
data.cargoquery
)
) {
return [];
}
return data.cargoquery.map(
function ( item ) {
return item.title || item;
}
);
}
);
}
function makeInClause( ids ) {
return ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
.join( ',' );
}
function uniqueIds( values ) {
return [
...new Set(
values
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test( id );
}
)
)
];
}
function mapBy( rows, key ) {
const result = {};
rows.forEach(
function ( row ) {
if (
row[ key ] === undefined ||
row[ key ] === null
) {
return;
}
result[
String(
row[ key ]
)
] = row;
}
);
return result;
}
/* =====================================
* Placement情報取得
* ===================================== */
function fetchPlacementInfo( ids ) {
if ( ids.length === 0 ) {
return Promise.resolve( {} );
}
const result = {};
const missingIds = [];
ids.forEach(
function ( id ) {
if (
placementInfoCache[ id ]
) {
result[ id ] =
placementInfoCache[ id ];
} else {
missingIds.push( id );
}
}
);
if ( missingIds.length === 0 ) {
return Promise.resolve(
result
);
}
return cargoQuery(
'FestivalStallPlacements',
'placement_id=placement_id,' +
'stall_id=stall_id,' +
'festival_id=festival_id,' +
'venue_id=venue_id,' +
'year=year,' +
'location_note=location_note',
'placement_id IN (' +
makeInClause(
missingIds
) +
')',
100
).then(
function ( placements ) {
const stallIds =
uniqueIds(
placements.map(
function ( row ) {
return row.stall_id;
}
)
);
const festivalIds =
uniqueIds(
placements.map(
function ( row ) {
return row.festival_id;
}
)
);
const venueIds =
uniqueIds(
placements.map(
function ( row ) {
return row.venue_id;
}
)
);
return Promise.all( [
stallIds.length
? cargoQuery(
'Stalls',
'stall_id=stall_id,' +
'name=stall_name,' +
'_pageName=page_name',
'stall_id IN (' +
makeInClause(
stallIds
) +
')',
100
)
: Promise.resolve( [] ),
festivalIds.length
? cargoQuery(
'Festivals',
'festival_id=festival_id,' +
'name=festival_name,' +
'_pageName=page_name',
'festival_id IN (' +
makeInClause(
festivalIds
) +
')',
100
)
: Promise.resolve( [] ),
venueIds.length
? cargoQuery(
'Venues',
'venue_id=venue_id,' +
'name=venue_name,' +
'_pageName=page_name',
'venue_id IN (' +
makeInClause(
venueIds
) +
')',
100
)
: Promise.resolve( [] )
] ).then(
function ( related ) {
return {
placements:
placements,
stalls:
related[ 0 ],
festivals:
related[ 1 ],
venues:
related[ 2 ]
};
}
);
}
).then(
function ( data ) {
const stallMap =
mapBy(
data.stalls,
'stall_id'
);
const festivalMap =
mapBy(
data.festivals,
'festival_id'
);
const venueMap =
mapBy(
data.venues,
'venue_id'
);
data.placements.forEach(
function ( placement ) {
const placementId =
String(
placement.placement_id
);
const info = {
placement:
placement,
stall:
stallMap[
String(
placement.stall_id
)
] || null,
festival:
festivalMap[
String(
placement.festival_id
)
] || null,
venue:
venueMap[
String(
placement.venue_id
)
] || null
};
placementInfoCache[
placementId
] = info;
result[
placementId
] = info;
}
);
return result;
}
);
}
/* =====================================
* メッセージ
* ===================================== */
function showMessage(
control,
message,
isError
) {
const element =
control.querySelector(
'.stall-compare-message'
);
if ( !element ) {
return;
}
element.textContent =
message;
element.classList.toggle(
'stall-compare-message-error',
Boolean( isError )
);
}
/* =====================================
* Placement比較ボタン生成
* ===================================== */
function createCompareButtons() {
document
.querySelectorAll(
'.stall-compare-placeholder'
)
.forEach(
function ( placeholder ) {
if (
placeholder.dataset.initialized ===
'1'
) {
return;
}
const prefix =
'stall-compare-placeholder-';
if (
!placeholder.id.startsWith(
prefix
)
) {
return;
}
const placementId =
placeholder.id.substring(
prefix.length
);
if (
!/^\d+$/.test(
placementId
) ||
placementId === '0'
) {
return;
}
const button =
document.createElement(
'button'
);
button.type =
'button';
button.className =
'stall-compare-button';
button.dataset.placementId =
placementId;
button.setAttribute(
'aria-pressed',
'false'
);
button.textContent =
'比較に追加';
placeholder.appendChild(
button
);
placeholder.dataset.initialized =
'1';
}
);
}
/* =====================================
* ボタン状態更新
* ===================================== */
function updateCompareButtons() {
const ids =
getComparePlacements();
document
.querySelectorAll(
'.stall-compare-button'
)
.forEach(
function ( button ) {
const placementId =
String(
button.dataset
.placementId || ''
);
const selected =
ids.includes(
placementId
);
if ( selected ) {
button.textContent =
'比較から外す';
button.classList.add(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'true'
);
} else {
button.textContent =
'比較に追加';
button.classList.remove(
'stall-compare-button-selected'
);
button.setAttribute(
'aria-pressed',
'false'
);
}
}
);
}
/* =====================================
* 固定比較トレイ生成
* ===================================== */
function createCompareTray() {
if (
document.getElementById(
'stall-compare-tray'
)
) {
return;
}
const tray =
document.createElement(
'div'
);
tray.id =
'stall-compare-tray';
tray.className =
'stall-compare-tray';
tray.setAttribute(
'aria-live',
'polite'
);
const header =
document.createElement(
'div'
);
header.className =
'stall-compare-tray-header';
const title =
document.createElement(
'strong'
);
title.className =
'stall-compare-tray-title';
title.textContent =
'比較候補';
const count =
document.createElement(
'span'
);
count.className =
'stall-compare-tray-count';
header.appendChild(
title
);
header.appendChild(
count
);
const items =
document.createElement(
'div'
);
items.className =
'stall-compare-tray-items';
const actions =
document.createElement(
'div'
);
actions.className =
'stall-compare-tray-actions';
const clearButton =
document.createElement(
'button'
);
clearButton.type =
'button';
clearButton.className =
'stall-compare-clear';
clearButton.textContent =
'すべて外す';
const compareLink =
document.createElement(
'a'
);
compareLink.className =
'stall-compare-open';
compareLink.href =
mw.util.getUrl(
'屋台比較'
);
compareLink.textContent =
'比較する';
actions.appendChild(
clearButton
);
actions.appendChild(
compareLink
);
tray.appendChild(
header
);
tray.appendChild(
items
);
tray.appendChild(
actions
);
document.body.appendChild(
tray
);
}
/* =====================================
* 比較トレイ更新
* ===================================== */
function updateCompareTray() {
createCompareTray();
const tray =
document.getElementById(
'stall-compare-tray'
);
if ( !tray ) {
return;
}
const ids =
getComparePlacements();
const count =
tray.querySelector(
'.stall-compare-tray-count'
);
const items =
tray.querySelector(
'.stall-compare-tray-items'
);
const compareLink =
tray.querySelector(
'.stall-compare-open'
);
if ( count ) {
count.textContent =
ids.length +
' / ' +
MAX_COMPARE;
}
if ( ids.length === 0 ) {
tray.classList.remove(
'stall-compare-tray-visible'
);
if ( items ) {
items.innerHTML = '';
}
return;
}
tray.classList.add(
'stall-compare-tray-visible'
);
if ( compareLink ) {
if ( ids.length >= 2 ) {
compareLink.classList.remove(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'false'
);
} else {
compareLink.classList.add(
'stall-compare-open-disabled'
);
compareLink.setAttribute(
'aria-disabled',
'true'
);
}
}
if ( !items ) {
return;
}
items.textContent =
'屋台情報を読み込み中…';
fetchPlacementInfo(
ids
).then(
function ( placementInfo ) {
const currentIds =
getComparePlacements();
items.innerHTML =
'';
currentIds.forEach(
function ( placementId ) {
const info =
placementInfo[
placementId
];
if ( !info ) {
return;
}
const item =
document.createElement(
'div'
);
item.className =
'stall-compare-tray-item';
const text =
document.createElement(
'div'
);
const name =
document.createElement(
info.stall &&
info.stall.page_name
? 'a'
: 'span'
);
if (
info.stall &&
info.stall.page_name
) {
name.href =
mw.util.getUrl(
info.stall.page_name
);
}
name.className =
'stall-compare-tray-item-name';
name.textContent =
info.stall
? info.stall.stall_name
: '屋台';
const context =
document.createElement(
'div'
);
context.className =
'stall-compare-tray-item-context';
const parts = [];
if (
info.placement &&
info.placement.year
) {
parts.push(
info.placement.year +
'年'
);
}
if (
info.festival &&
info.festival.festival_name
) {
parts.push(
info.festival
.festival_name
);
}
if (
info.venue &&
info.venue.venue_name
) {
parts.push(
info.venue
.venue_name
);
}
context.textContent =
parts.join(
' / '
);
text.appendChild(
name
);
text.appendChild(
context
);
const removeButton =
document.createElement(
'button'
);
removeButton.type =
'button';
removeButton.className =
'stall-compare-tray-remove';
removeButton.dataset.placementId =
placementId;
removeButton.setAttribute(
'aria-label',
(
info.stall
? info.stall.stall_name
: '屋台'
) +
'を比較候補から外す'
);
removeButton.textContent =
'×';
item.appendChild(
text
);
item.appendChild(
removeButton
);
items.appendChild(
item
);
}
);
}
).catch(
function ( error ) {
console.error(
'比較トレイ取得エラー:',
error
);
items.textContent =
'比較候補を読み込めませんでした。';
}
);
}
/* =====================================
* 詳細ページ
* 「比較に追加」
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-button'
);
if ( !button ) {
return;
}
event.preventDefault();
const placementId =
String(
button.dataset
.placementId || ''
);
if (
!/^\d+$/.test(
placementId
)
) {
return;
}
const control =
button.closest(
'.stall-compare-control'
);
let ids =
getComparePlacements();
const index =
ids.indexOf(
placementId
);
/*
* すでに選択中
*/
if ( index !== -1 ) {
ids.splice(
index,
1
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補から外しました。',
false
);
}
return;
}
/*
* 最大4件
*/
if (
ids.length >=
MAX_COMPARE
) {
if ( control ) {
showMessage(
control,
'比較できる出店は最大4件です。',
true
);
}
return;
}
ids.push(
placementId
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
if ( control ) {
showMessage(
control,
'比較候補に追加しました(' +
ids.length +
'/4)。',
false
);
}
}
);
/* =====================================
* トレイから1件削除
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-tray-remove'
);
if ( !button ) {
return;
}
event.preventDefault();
const placementId =
String(
button.dataset
.placementId || ''
);
let ids =
getComparePlacements();
ids =
ids.filter(
function ( id ) {
return id !== placementId;
}
);
saveComparePlacements(
ids
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 全削除
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const button =
event.target.closest(
'.stall-compare-clear'
);
if ( !button ) {
return;
}
event.preventDefault();
saveComparePlacements(
[]
);
updateCompareButtons();
updateCompareTray();
}
);
/* =====================================
* 1件時の比較リンク無効
* ===================================== */
document.addEventListener(
'click',
function ( event ) {
const link =
event.target.closest(
'.stall-compare-open-disabled'
);
if ( !link ) {
return;
}
event.preventDefault();
}
);
/* =====================================
* 初期化
* ===================================== */
function initPlacementCompare() {
createCompareButtons();
createCompareTray();
updateCompareButtons();
updateCompareTray();
}
initPlacementCompare();
mw.hook(
'wikipage.content'
).add(
function () {
initPlacementCompare();
}
);
} );
/* ========================================
* 屋台比較ページ
*
* localStorage:
* matsuriWikiCompareStalls
*
* 2~4店舗をCargoから取得して
* 横並び比較表を自動生成
* ======================================== */
mw.loader.using( [
'mediawiki.storage',
'mediawiki.api',
'mediawiki.util'
] ).then( function () {
'use strict';
const compareRoot =
document.getElementById(
'stall-compare-page'
);
/*
* 屋台比較ページ以外では
* 何もしない
*/
if ( !compareRoot ) {
return;
}
const STORAGE_KEY =
'matsuriWikiCompareStalls';
const MIN_COMPARE = 2;
const MAX_COMPARE = 4;
const api =
new mw.Api();
/* =====================================
* 比較年
*
* ?year=2026 があれば使用
* 無ければ現在年
* ===================================== */
function getCompareYear() {
const params =
new URLSearchParams(
window.location.search
);
const yearParam =
params.get( 'year' );
if (
yearParam &&
/^\d{4}$/.test( yearParam )
) {
const year =
Number( yearParam );
if (
year >= 2000 &&
year <= 2100
) {
return year;
}
}
return new Date().getFullYear();
}
const compareYear =
getCompareYear();
/* =====================================
* localStorageからstall_id取得
* ===================================== */
function getCompareStallIds() {
const raw =
mw.storage.get(
STORAGE_KEY
);
if ( !raw ) {
return [];
}
try {
const ids =
JSON.parse( raw );
if ( !Array.isArray( ids ) ) {
return [];
}
return [ ...new Set(
ids
.map( String )
.filter(
function ( id ) {
return /^\d+$/.test(
id
);
}
)
) ].slice(
0,
MAX_COMPARE
);
} catch ( e ) {
return [];
}
}
/* =====================================
* Cargo API共通処理
* ===================================== */
function cargoQuery(
table,
fields,
where,
limit
) {
const params = {
action: 'cargoquery',
tables: table,
fields: fields,
limit: limit || 100,
format: 'json'
};
if ( where ) {
params.where = where;
}
return api.get(
params
).then(
function ( data ) {
if (
!data ||
!Array.isArray(
data.cargoquery
)
) {
return [];
}
return data.cargoquery.map(
function ( item ) {
return (
item.title ||
item
);
}
);
}
);
}
/* =====================================
* ID配列をCargo IN句にする
* ===================================== */
function makeInClause( ids ) {
return ids
.filter(
function ( id ) {
return /^\d+$/.test(
String( id )
);
}
)
.join( ',' );
}
/* =====================================
* 重複削除
* ===================================== */
function uniqueIds( values ) {
return [
...new Set(
values
.map( String )
.filter(
function ( value ) {
return (
value &&
/^\d+$/.test(
value
)
);
}
)
)
];
}
/* =====================================
* Object Map作成
* ===================================== */
function mapBy(
rows,
key
) {
const result = {};
rows.forEach(
function ( row ) {
if (
row[ key ] ===
undefined
) {
return;
}
result[
String(
row[ key ]
)
] = row;
}
);
return result;
}
/* =====================================
* 表示用関数
* ===================================== */
function textOrDash( value ) {
if (
value === undefined ||
value === null ||
value === ''
) {
return '―';
}
return String( value );
}
function cleanNumber( value ) {
const number =
Number( value );
if (
!Number.isFinite(
number
)
) {
return '';
}
if (
Number.isInteger(
number
)
) {
return String(
number
);
}
return String(
Math.round(
number * 100
) / 100
);
}
function formatHours(
placement
) {
if ( !placement ) {
return '―';
}
const open =
placement.opening_time || '';
const close =
placement.closing_time || '';
if (
open &&
close
) {
return (
open +
'~' +
close
);
}
if ( open ) {
return open + '~';
}
if ( close ) {
return '~' + close;
}
if (
placement.hours_note
) {
return placement.hours_note;
}
return '未確認';
}
function formatPositionStatus(
status
) {
switch ( status ) {
case 'exact':
return '正確な位置';
case 'approximate':
return 'おおよその位置';
case 'test':
return 'テスト位置';
default:
return '位置未確認';
}
}
function formatVerification(
status
) {
switch ( status ) {
case 'verified':
return '確認済み';
case 'partially_verified':
return '一部確認済み';
case 'outdated':
return '情報が古い';
default:
return '未確認';
}
}
function formatAvailability(
status
) {
switch ( status ) {
case 'available':
return '販売あり';
case 'unavailable':
return '販売なし';
default:
return '未確認';
}
}
/* =====================================
* 1単位あたり価格
* ===================================== */
function getUnitPrice(
offering
) {
if ( !offering ) {
return '―';
}
const price =
Number(
offering.price
);
const quantity =
Number(
offering.serving_quantity
);
if (
!Number.isFinite(
price
) ||
!Number.isFinite(
quantity
) ||
quantity <= 0
) {
return '―';
}
const unitPrice =
Math.round(
(
price /
quantity
) * 100
) / 100;
const unit =
offering.serving_unit ||
'単位';
return (
unitPrice +
'円/' +
unit
);
}
/* =====================================
* DOM helper
* ===================================== */
function createTextCell(
tagName,
text
) {
const cell =
document.createElement(
tagName
);
cell.textContent =
text;
return cell;
}
/* =====================================
* Menu表示
* ===================================== */
function createMenuList(
menuData
) {
const container =
document.createElement(
'div'
);
container.className =
'stall-compare-menu-list';
if (
!menuData ||
menuData.length === 0
) {
container.textContent =
'メニュー未登録';
return container;
}
menuData.forEach(
function ( item ) {
const menu =
document.createElement(
'div'
);
menu.className =
'stall-compare-menu-item';
const name =
document.createElement(
'strong'
);
name.className =
'stall-compare-menu-name';
name.textContent =
item.menuName ||
'商品';
const price =
document.createElement(
'div'
);
price.textContent =
item.price
? item.price + '円'
: '価格未確認';
const serving =
document.createElement(
'div'
);
if (
item.servingQuantity
) {
serving.textContent =
'内容量:' +
item.servingQuantity +
(
item.servingUnit ||
''
);
} else {
serving.textContent =
'内容量:未確認';
}
const perUnit =
document.createElement(
'div'
);
perUnit.textContent =
'1単位あたり:' +
item.unitPrice;
const availability =
document.createElement(
'div'
);
availability.textContent =
'販売状況:' +
item.availability;
menu.appendChild(
name
);
menu.appendChild(
price
);
menu.appendChild(
serving
);
menu.appendChild(
perUnit
);
menu.appendChild(
availability
);
container.appendChild(
menu
);
}
);
return container;
}
/* =====================================
* 比較表生成
* ===================================== */
function renderComparison(
compareData
) {
compareRoot.innerHTML = '';
/*
* 年表示
*/
const heading =
document.createElement(
'h2'
);
heading.textContent =
compareYear +
'年の屋台比較';
compareRoot.appendChild(
heading
);
/*
* 横スクロール用
*/
const wrapper =
document.createElement(
'div'
);
wrapper.className =
'stall-compare-table-wrapper';
const table =
document.createElement(
'table'
);
table.className =
'stall-compare-table';
/* ==============================
* ヘッダー
* ============================== */
const thead =
document.createElement(
'thead'
);
const headerRow =
document.createElement(
'tr'
);
headerRow.appendChild(
createTextCell(
'th',
'比較項目'
)
);
compareData.forEach(
function ( data ) {
const th =
document.createElement(
'th'
);
if (
data.stall &&
data.stall.page_name
) {
const link =
document.createElement(
'a'
);
link.href =
mw.util.getUrl(
data.stall.page_name
);
link.textContent =
data.stall.stall_name ||
'屋台';
th.appendChild(
link
);
} else {
th.textContent =
data.stall
? data.stall.stall_name
: '屋台';
}
headerRow.appendChild(
th
);
}
);
thead.appendChild(
headerRow
);
table.appendChild(
thead
);
const tbody =
document.createElement(
'tbody'
);
/* ==============================
* 普通の文字列行
* ============================== */
function addRow(
label,
getter
) {
const tr =
document.createElement(
'tr'
);
const labelCell =
createTextCell(
'th',
label
);
labelCell.scope =
'row';
tr.appendChild(
labelCell
);
compareData.forEach(
function ( data ) {
const td =
createTextCell(
'td',
textOrDash(
getter(
data
)
)
);
tr.appendChild(
td
);
}
);
tbody.appendChild(
tr
);
}
addRow(
'祭り',
function ( data ) {
return data.festival
? data.festival.festival_name
: '―';
}
);
addRow(
'会場',
function ( data ) {
return data.venue
? data.venue.venue_name
: '―';
}
);
addRow(
'地域',
function ( data ) {
return data.area
? data.area.area_name
: '―';
}
);
addRow(
'カテゴリ',
function ( data ) {
return data.stall
? data.stall.category
: '―';
}
);
addRow(
'出店場所',
function ( data ) {
return data.placement
? data.placement.location_note
: '―';
}
);
addRow(
'営業時間',
function ( data ) {
return formatHours(
data.placement
);
}
);
addRow(
'位置情報',
function ( data ) {
return data.placement
? formatPositionStatus(
data.placement
.position_status
)
: '―';
}
);
addRow(
'確認状態',
function ( data ) {
return data.placement
? formatVerification(
data.placement
.verification_status
)
: '―';
}
);
/* ==============================
* Menu行
* ============================== */
const menuRow =
document.createElement(
'tr'
);
const menuLabel =
createTextCell(
'th',
'メニュー'
);
menuLabel.scope =
'row';
menuRow.appendChild(
menuLabel
);
compareData.forEach(
function ( data ) {
const td =
document.createElement(
'td'
);
td.appendChild(
createMenuList(
data.menus
)
);
menuRow.appendChild(
td
);
}
);
tbody.appendChild(
menuRow
);
table.appendChild(
tbody
);
wrapper.appendChild(
table
);
compareRoot.appendChild(
wrapper
);
/*
* 注意書き
*/
const note =
document.createElement(
'p'
);
note.className =
'stall-compare-note';
note.textContent =
'価格・営業時間・出店位置は開催年によって変わる場合があります。確認状態もあわせて参照してください。';
compareRoot.appendChild(
note
);
}
/* =====================================
* エラー表示
* ===================================== */
function renderMessage(
message
) {
compareRoot.innerHTML = '';
const p =
document.createElement(
'p'
);
p.className =
'stall-compare-page-message';
p.textContent =
message;
compareRoot.appendChild(
p
);
}
/* =====================================
* データ取得開始
* ===================================== */
const stallIds =
getCompareStallIds();
if (
stallIds.length <
MIN_COMPARE
) {
renderMessage(
'比較する屋台を2店舗以上選択してください。'
);
return;
}
/*
* STEP 1
*
* Stalls
* FestivalStallPlacements
*/
Promise.all( [
cargoQuery(
'Stalls',
'stall_id=stall_id,' +
'name=stall_name,' +
'category=category,' +
'_pageName=page_name',
'stall_id IN (' +
makeInClause(
stallIds
) +
')',
MAX_COMPARE
),
cargoQuery(
'FestivalStallPlacements',
'placement_id=placement_id,' +
'stall_id=stall_id,' +
'festival_id=festival_id,' +
'venue_id=venue_id,' +
'year=year,' +
'location_note=location_note,' +
'opening_time=opening_time,' +
'closing_time=closing_time,' +
'hours_note=hours_note,' +
'position_status=position_status,' +
'verification_status=verification_status,' +
'sort_order=sort_order',
'stall_id IN (' +
makeInClause(
stallIds
) +
') AND year=' +
compareYear,
100
)
] ).then(
function ( firstResults ) {
const stalls =
firstResults[ 0 ];
const placements =
firstResults[ 1 ];
/*
* 同じstallに複数Placementがある場合
* sort_orderの小さいものを
* 今回の比較対象にする
*
* 後でfestival_idまで比較条件に
* 含める設計へ拡張可能
*/
const placementByStall = {};
placements
.slice()
.sort(
function ( a, b ) {
return (
Number(
a.sort_order || 0
) -
Number(
b.sort_order || 0
)
);
}
)
.forEach(
function (
placement
) {
const id =
String(
placement.stall_id
);
if (
!placementByStall[
id
]
) {
placementByStall[
id
] =
placement;
}
}
);
const chosenPlacements =
stallIds
.map(
function ( id ) {
return (
placementByStall[
id
] ||
null
);
}
)
.filter(
Boolean
);
const festivalIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.festival_id;
}
)
);
const venueIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.venue_id;
}
)
);
const placementIds =
uniqueIds(
chosenPlacements.map(
function ( row ) {
return row.placement_id;
}
)
);
/*
* STEP 2
*
* Festivals
* Venues
* Offerings
*/
return Promise.all( [
festivalIds.length
? cargoQuery(
'Festivals',
'festival_id=festival_id,' +
'name=festival_name,' +
'_pageName=page_name',
'festival_id IN (' +
makeInClause(
festivalIds
) +
')',
100
)
: Promise.resolve(
[]
),
venueIds.length
? cargoQuery(
'Venues',
'venue_id=venue_id,' +
'name=venue_name,' +
'area_id=area_id,' +
'_pageName=page_name',
'venue_id IN (' +
makeInClause(
venueIds
) +
')',
100
)
: Promise.resolve(
[]
),
placementIds.length
? cargoQuery(
'FestivalStallMenuOfferings',
'offering_id=offering_id,' +
'placement_id=placement_id,' +
'menu_item_id=menu_item_id,' +
'price=price,' +
'serving_quantity=serving_quantity,' +
'serving_unit=serving_unit,' +
'serving_note=serving_note,' +
'availability=availability,' +
'verification_status=verification_status,' +
'sort_order=sort_order',
'placement_id IN (' +
makeInClause(
placementIds
) +
')',
100
)
: Promise.resolve(
[]
)
] ).then(
function (
secondResults
) {
return {
stalls: stalls,
placementByStall:
placementByStall,
festivals:
secondResults[ 0 ],
venues:
secondResults[ 1 ],
offerings:
secondResults[ 2 ]
};
}
);
}
).then(
function ( data ) {
const areaIds =
uniqueIds(
data.venues.map(
function ( row ) {
return row.area_id;
}
)
);
const menuItemIds =
uniqueIds(
data.offerings.map(
function ( row ) {
return row.menu_item_id;
}
)
);
/*
* STEP 3
*
* Areas
* StallMenuItems
*/
return Promise.all( [
areaIds.length
? cargoQuery(
'Areas',
'area_id=area_id,' +
'name=area_name,' +
'_pageName=page_name',
'area_id IN (' +
makeInClause(
areaIds
) +
')',
100
)
: Promise.resolve(
[]
),
menuItemIds.length
? cargoQuery(
'StallMenuItems',
'menu_item_id=menu_item_id,' +
'stall_id=stall_id,' +
'name=menu_name,' +
'item_category=item_category,' +
'sort_order=sort_order',
'menu_item_id IN (' +
makeInClause(
menuItemIds
) +
')',
100
)
: Promise.resolve(
[]
)
] ).then(
function (
thirdResults
) {
data.areas =
thirdResults[ 0 ];
data.menuItems =
thirdResults[ 1 ];
return data;
}
);
}
).then(
function ( data ) {
/* ============================
* Map化
* ============================ */
const stallMap =
mapBy(
data.stalls,
'stall_id'
);
const festivalMap =
mapBy(
data.festivals,
'festival_id'
);
const venueMap =
mapBy(
data.venues,
'venue_id'
);
const areaMap =
mapBy(
data.areas,
'area_id'
);
const menuMap =
mapBy(
data.menuItems,
'menu_item_id'
);
/* ============================
* OfferingをPlacementごとに
* ============================ */
const offeringsByPlacement =
{};
data.offerings
.slice()
.sort(
function ( a, b ) {
return (
Number(
a.sort_order || 0
) -
Number(
b.sort_order || 0
)
);
}
)
.forEach(
function (
offering
) {
const placementId =
String(
offering
.placement_id
);
if (
!offeringsByPlacement[
placementId
]
) {
offeringsByPlacement[
placementId
] = [];
}
offeringsByPlacement[
placementId
].push(
offering
);
}
);
/* ============================
* 比較データを
* localStorage順に生成
* ============================ */
const compareData =
stallIds.map(
function ( stallId ) {
const stall =
stallMap[
stallId
] || null;
const placement =
data
.placementByStall[
stallId
] || null;
let festival = null;
let venue = null;
let area = null;
let menus = [];
if ( placement ) {
festival =
festivalMap[
String(
placement
.festival_id
)
] || null;
venue =
venueMap[
String(
placement
.venue_id
)
] || null;
if (
venue &&
venue.area_id
) {
area =
areaMap[
String(
venue
.area_id
)
] || null;
}
const offerings =
offeringsByPlacement[
String(
placement
.placement_id
)
] || [];
menus =
offerings.map(
function (
offering
) {
const menu =
menuMap[
String(
offering
.menu_item_id
)
] || {};
return {
menuName:
menu.menu_name ||
'商品',
category:
menu.item_category ||
'',
price:
cleanNumber(
offering.price
),
servingQuantity:
cleanNumber(
offering
.serving_quantity
),
servingUnit:
offering
.serving_unit ||
'',
servingNote:
offering
.serving_note ||
'',
unitPrice:
getUnitPrice(
offering
),
availability:
formatAvailability(
offering
.availability
),
verification:
formatVerification(
offering
.verification_status
)
};
}
);
}
return {
stallId: stallId,
stall: stall,
placement:
placement,
festival:
festival,
venue:
venue,
area:
area,
menus:
menus
};
}
);
renderComparison(
compareData
);
}
).catch(
function ( error ) {
console.error(
'屋台比較データ取得エラー:',
error
);
renderMessage(
'比較データの取得中にエラーが発生しました。'
);
}
);
} );