注意: 保存後、変更を確認するにはブラウザーのキャッシュを消去する必要がある場合があります。

  • Firefox / Safari: Shift を押しながら 再読み込み をクリックするか、Ctrl-F5 または Ctrl-R を押してください (Mac では ⌘-R)
  • Google Chrome: Ctrl-Shift-R を押してください (Mac では ⌘-Shift-R)
  • Microsoft Edge: Ctrl を押しながら 最新の情報に更新 をクリックするか、Ctrl-F5 を押してください。
/* ========================================
 * 屋台比較
 *
 * ・最大4店舗
 * ・localStorageにはstall_idだけ保存
 * ・屋台名はCargo APIから取得
 * ・画面下部に固定比較トレイを表示
 * ======================================== */

mw.loader.using( [
    'mediawiki.storage',
    'mediawiki.api',
    'mediawiki.util'
] ).then( function () {

    'use strict';

    const STORAGE_KEY = 'matsuriWikiCompareStalls';
    const MAX_COMPARE = 4;

    const api = new mw.Api();

    /*
     * このページを表示している間だけ使う
     * 屋台名キャッシュ
     */
    const stallNameCache = {};


    /* =====================================
     * localStorage
     * ===================================== */

    function getCompareStalls() {

        const raw = mw.storage.get( STORAGE_KEY );

        if ( !raw ) {
            return [];
        }

        try {

            const ids = JSON.parse( raw );

            if ( !Array.isArray( ids ) ) {
                return [];
            }

            /*
             * 数字だけ許可
             * localStorage改変対策
             */
            return [ ...new Set(
                ids
                    .map( String )
                    .filter( function ( id ) {
                        return /^\d+$/.test( id );
                    } )
            ) ].slice( 0, MAX_COMPARE );

        } catch ( e ) {

            return [];

        }

    }


    function saveCompareStalls( ids ) {

        mw.storage.set(
            STORAGE_KEY,
            JSON.stringify( ids )
        );

    }


    /* =====================================
     * Cargoから屋台名取得
     * ===================================== */

    function fetchStallNames( ids ) {

        if ( ids.length === 0 ) {
            return Promise.resolve( {} );
        }

        /*
         * すでに取得済みのID
         */
        const result = {};

        const missingIds = [];

        ids.forEach( function ( id ) {

            if ( stallNameCache[ id ] ) {

                result[ id ] =
                    stallNameCache[ id ];

            } else {

                missingIds.push( id );

            }

        } );


        /*
         * 全部キャッシュ済み
         */
        if ( missingIds.length === 0 ) {
            return Promise.resolve( result );
        }


        /*
         * 数字だけにしてあるため
         * IN (1,2,3) として安全にCargoへ渡す
         */
        const where =
            'stall_id IN (' +
            missingIds.join( ',' ) +
            ')';


        return api.get( {

            action: 'cargoquery',

            tables: 'Stalls',

            fields:
                'stall_id=stall_id,' +
                'name=stall_name,' +
                '_pageName=page_name',

            where: where,

            limit: MAX_COMPARE,

            format: 'json'

        } ).then( function ( data ) {

            if (
                !data ||
                !Array.isArray( data.cargoquery )
            ) {
                return result;
            }


            data.cargoquery.forEach( function ( item ) {

                /*
                 * Cargo APIは通常
                 *
                 * {
                 *   title: {
                 *     stall_id: "...",
                 *     stall_name: "..."
                 *   }
                 * }
                 *
                 * の形式
                 */
                const row =
                    item.title || item;

                if (
                    !row ||
                    row.stall_id === undefined
                ) {
                    return;
                }

                const id =
                    String( row.stall_id );

                const info = {

                    id: id,

                    name:
                        row.stall_name ||
                        '屋台ID ' + id,

                    page:
                        row.page_name || ''

                };

                stallNameCache[ id ] = info;
                result[ id ] = info;

            } );


            return result;

        } ).catch( function () {

            /*
             * APIエラーでも
             * 比較機能そのものは壊さない
             */
            missingIds.forEach( function ( id ) {

                result[ id ] = {
                    id: id,
                    name: '屋台ID ' + id,
                    page: ''
                };

            } );

            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 )
        );

    }


    /* =====================================
     * 詳細ページ側button生成
     * ===================================== */

    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 stallId =
                    placeholder.id.substring(
                        prefix.length
                    );

                if (
                    !/^\d+$/.test( stallId )
                ) {
                    return;
                }

                const button =
                    document.createElement(
                        'button'
                    );

                button.type =
                    'button';

                button.className =
                    'stall-compare-button';

                button.dataset.stallId =
                    stallId;

                button.setAttribute(
                    'aria-pressed',
                    'false'
                );

                button.textContent =
                    '比較に追加';

                placeholder.appendChild(
                    button
                );

                placeholder.dataset.initialized =
                    '1';

            } );

    }


    /* =====================================
     * 詳細ページ側button状態
     * ===================================== */

    function updateCompareButtons() {

        const ids =
            getCompareStalls();

        document
            .querySelectorAll(
                '.stall-compare-button'
            )
            .forEach( function ( button ) {

                const stallId =
                    String(
                        button.dataset.stallId || ''
                    );

                const selected =
                    ids.includes( stallId );

                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 =
            getCompareStalls();


        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;

        }


        /*
         * 0件ならトレイ非表示
         */
        if ( ids.length === 0 ) {

            tray.classList.remove(
                'stall-compare-tray-visible'
            );

            if ( items ) {
                items.innerHTML = '';
            }

            return;
        }


        tray.classList.add(
            'stall-compare-tray-visible'
        );


        /*
         * 2件未満では比較ページを無効化
         */
        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 =
            '屋台情報を読み込み中…';


        fetchStallNames(
            ids
        ).then( function ( stallInfo ) {

            /*
             * 更新待ち中に選択内容が
             * 変わった場合も現在値を優先
             */
            const currentIds =
                getCompareStalls();

            items.innerHTML =
                '';


            currentIds.forEach(
                function ( id ) {

                    const info =
                        stallInfo[ id ] || {
                            id: id,
                            name:
                                '屋台ID ' + id,
                            page: ''
                        };


                    const item =
                        document.createElement(
                            'div'
                        );

                    item.className =
                        'stall-compare-tray-item';


                    /*
                     * 屋台名
                     */
                    let nameElement;

                    if ( info.page ) {

                        nameElement =
                            document.createElement(
                                'a'
                            );

                        nameElement.href =
                            mw.util.getUrl(
                                info.page
                            );

                    } else {

                        nameElement =
                            document.createElement(
                                'span'
                            );

                    }

                    nameElement.className =
                        'stall-compare-tray-item-name';

                    nameElement.textContent =
                        info.name;


                    /*
                     * × ボタン
                     */
                    const removeButton =
                        document.createElement(
                            'button'
                        );

                    removeButton.type =
                        'button';

                    removeButton.className =
                        'stall-compare-tray-remove';

                    removeButton.dataset.stallId =
                        id;

                    removeButton.setAttribute(
                        'aria-label',
                        info.name +
                        'を比較候補から外す'
                    );

                    removeButton.textContent =
                        '×';


                    item.appendChild(
                        nameElement
                    );

                    item.appendChild(
                        removeButton
                    );

                    items.appendChild(
                        item
                    );

                }
            );

        } );

    }


    /* =====================================
     * 詳細ページ比較button
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-button'
                );

            if ( !button ) {
                return;
            }

            event.preventDefault();

            const stallId =
                String(
                    button.dataset.stallId || ''
                );

            if (
                !/^\d+$/.test( stallId )
            ) {
                return;
            }


            const control =
                button.closest(
                    '.stall-compare-control'
                );


            let ids =
                getCompareStalls();


            const index =
                ids.indexOf(
                    stallId
                );


            /*
             * すでに選択済み
             * → 外す
             */
            if ( index !== -1 ) {

                ids.splice(
                    index,
                    1
                );

                saveCompareStalls(
                    ids
                );

                updateCompareButtons();
                updateCompareTray();

                if ( control ) {

                    showMessage(
                        control,
                        '比較候補から外しました。',
                        false
                    );

                }

                return;

            }


            /*
             * 4件上限
             */
            if (
                ids.length >=
                MAX_COMPARE
            ) {

                if ( control ) {

                    showMessage(
                        control,
                        '比較できる屋台は最大4店舗です。',
                        true
                    );

                }

                return;

            }


            ids.push(
                stallId
            );

            saveCompareStalls(
                ids
            );

            updateCompareButtons();
            updateCompareTray();


            if ( control ) {

                showMessage(
                    control,
                    '比較候補に追加しました(' +
                        ids.length +
                        '/4)。',
                    false
                );

            }

        }
    );


    /* =====================================
     * トレイ × button
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-tray-remove'
                );

            if ( !button ) {
                return;
            }

            event.preventDefault();

            const stallId =
                String(
                    button.dataset.stallId || ''
                );

            let ids =
                getCompareStalls();

            ids =
                ids.filter(
                    function ( id ) {
                        return id !== stallId;
                    }
                );

            saveCompareStalls(
                ids
            );

            updateCompareButtons();
            updateCompareTray();

        }
    );


    /* =====================================
     * 「すべて外す」
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const button =
                event.target.closest(
                    '.stall-compare-clear'
                );

            if ( !button ) {
                return;
            }

            event.preventDefault();

            saveCompareStalls(
                []
            );

            updateCompareButtons();
            updateCompareTray();

        }
    );


    /* =====================================
     * 1件時の「比較する」クリック防止
     * ===================================== */

    document.addEventListener(
        'click',
        function ( event ) {

            const link =
                event.target.closest(
                    '.stall-compare-open-disabled'
                );

            if ( !link ) {
                return;
            }

            event.preventDefault();

        }
    );


    /* =====================================
     * 初期化
     * ===================================== */

    function initStallCompare() {

        createCompareButtons();

        createCompareTray();

        updateCompareButtons();

        updateCompareTray();

    }


    initStallCompare();


    mw.hook(
        'wikipage.content'
    ).add(
        function () {

            initStallCompare();

        }
    );

} );