コンテンツにスキップ

「MediaWiki:MobileUI.js」の版間の差分

提供: Vikipedia
細編集の要約なし
タグ: モバイル編集 モバイルウェブ編集
細編集の要約なし
タグ: モバイル編集 モバイルウェブ編集
 
(同じ利用者による、間の25版が非表示)
2行目: 2行目:


/**
/**
  * @typedef {import("jquery")} _JQueryStatic
  * @typedef MediaWiki
* @prop {readonly Map<string, never>} config
*/
 
/**
* @typedef WGNamespaceIds
* @prop {number} user
* @prop {number} user_talk
* @prop {number} special
*/
 
/**
* @typedef {{ [id: string]: string; }} WGFormattedNamespaces
  */
  */


27行目: 39行目:
  * @prop {string | string[]} [class]
  * @prop {string | string[]} [class]
  * @prop {MenuDataItem[]} items
  * @prop {MenuDataItem[]} items
*/
/**
* @typedef UserMenuItem
* @prop {string} text
* @prop {string} title
* @prop {readonly string[]} iconClass
  */
  */


32行目: 51行目:
  * @typedef MobileUI
  * @typedef MobileUI
  * @prop {MenuDataGroup[]} leftGroups
  * @prop {MenuDataGroup[]} leftGroups
  * @prop {MenuDataGroup} leftShortURLGroup
  * @prop {readonly UserMenuItem[]} userMenuItems
* @prop {MenuDataGroup[]} rightGroups
  */
  */


class MobileUIBuilder {
class MobileUIBuilder {
     /**
     /**
     * @param {any} mw
     * @param {MediaWiki} mw
    * @param {JQueryStatic} $
     */
     */
     constructor(mw, $) {
     constructor(mw) {
         this.mw = mw;
         // this.mw = mw;
        this.$ = $;


         /**
         /**
49行目: 65行目:
         */
         */
         this.wgServer = mw.config.get("wgServer");
         this.wgServer = mw.config.get("wgServer");
        /**
        * @type {string}
        */
        this.wgScript = mw.config.get("wgScript");


         /**
         /**
54行目: 75行目:
         */
         */
         this.wgScriptPath = mw.config.get("wgScriptPath");
         this.wgScriptPath = mw.config.get("wgScriptPath");
        /**
        * @type {string}
        */
        this.wgPageName = mw.config.get("wgPageName");


         /**
         /**
59行目: 85行目:
         */
         */
         this.wgArticleId = mw.config.get("wgArticleId");
         this.wgArticleId = mw.config.get("wgArticleId");
        /**
        * @type {boolean}
        */
        this.wgIsArticle = mw.config.get("wgIsArticle");
        /**
        * @type {boolean}
        */
        this.wgIsMainPage = mw.config.get("wgIsMainPage");
        /**
        * @type {number}
        */
        this.wgUserId = mw.config.get("wgUserId");
        /**
        * @type {string}
        */
        this.wgUserName = mw.config.get("wgUserName");
        /**
        * @type {WGNamespaceIds}
        */
        this.wgNamespaceIds = Object.assign({}, mw.config.get("wgNamespaceIds"));
        /**
        * @type {WGFormattedNamespaces}
        */
        this.wgFormattedNamespaces = Object.assign({}, mw.config.get("wgFormattedNamespaces"));
     }
     }


68行目: 124行目:
         switch (component.id) {
         switch (component.id) {
             case "mobile-ui-page-short-url":
             case "mobile-ui-page-short-url":
                 return (
                 return [
                     this.wgServer
                     this.wgServer,
                     + this.wgScriptPath
                     this.wgScriptPath,
                     + "?curid="
                     "/?curid=",
                     + String(this.wgArticleId)
                     String(this.wgArticleId),
                 );
                ].join("");
 
            case "mobile-ui-page-special-user":
                return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.user],
                    ":",
                    this.wgUserName,
                ].join("");
 
            case "mobile-ui-page-special-contributions":
                 return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.special],
                    ":",
                    "Contributions",
                    "/",
                    this.wgUserName,
                ].join("");
 
            case "mobile-ui-page-user-talk":
                return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.user_talk],
                    ":",
                    this.wgUserName,
                ].join("");
 
            case "mobile-ui-page-special-logout":
                return [
                    this.wgScript,
                    "?title=",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.special],
                    ":",
                    "Logout",
                    "&returnto=",
                    this.wgPageName,
                ].join("");


             default:
             default:
                 return component.title
                 return component.title
                     ? (this.wgScriptPath + "/" + component.title)
                     ? (this.wgScriptPath + "/" + component.title)
                     : component.href;
                     : component.href || "";
         }
         }
     }
     }
84行目: 180行目:
     /**
     /**
     * @param {MenuDataItemComponent} component
     * @param {MenuDataItemComponent} component
     * @returns {JQuery<HTMLElement>}
     * @returns {HTMLAnchorElement}
     */
     */
     menuDataItemComponent(component) {
     menuDataItemComponent(component) {
         const href = this.hrefForMenuDataItemComponent(component);
         const href = this.hrefForMenuDataItemComponent(component);


         return this
         const element = window.document.createElement("a");
            .$("<a>")
        element.id = component.id || "";
            .text(component.text || href)
        element.classList.add(...[].concat(component.class || []));
            .attr("href", href)
        element.setAttribute("data-event-name", component.dataEventName || "");
            .attr("id", component.id)
 
            .addClass(component.class)
        if (href) {
            .attr("data-event-name", component.dataEventName);
            element.href = encodeURI(href);
        }
 
        const textElement = window.document.createElement("span");
        textElement.textContent = component.text || href;
        element.append(textElement);
 
        return element;
     }
     }


     /**
     /**
     * @param {MenuDataItem} item
     * @param {MenuDataItem} item
     * @returns {JQuery<HTMLElement>}
     * @returns {HTMLLIElement}
     */
     */
     menuDataItem(item) {
     menuDataItem(item) {
         /**
         /**
         * @type {JQuery<HTMLElement>[]}
         * @type {HTMLAnchorElement[]}
         */
         */
         const children = [];
         const children = [];
112行目: 215行目:
         }
         }


         return this
         const element = window.document.createElement("li");
            .$("<li>")
        element.id = item.id || "";
            .attr("id", item.id)
        element.classList.add(...[].concat(item.class || []));
            .addClass(item.class)
        element.append(...children);
            .append(children);
 
        return element;
     }
     }


     /**
     /**
     * @param {MenuDataGroup} group
     * @param {MenuDataGroup} group
     * @returns {JQuery<HTMLElement>}
     * @returns {HTMLUListElement}
     */
     */
     menuDataGroup(group) {
     menuDataGroup(group) {
         /**
         /**
         * @type {JQuery<HTMLElement>[]}
         * @type {HTMLLIElement[]}
         */
         */
         const children = [];
         const children = [];
133行目: 237行目:
         }
         }


         return this
         const element = window.document.createElement("ul");
            .$("<ul>")
        element.id = group.id || "";
            .attr("id", group.id)
        element.classList.add(...[].concat(group.class || []));
            .attr("data-article-id", this.wgArticleId)
        element.append(...children);
            .addClass(group.class)
 
            .append(children);
        return element;
     }
     }


     /**
     /**
     * @param {MenuDataGroup[]} groups
     * @param {MenuDataGroup[]} groups
     * @returns {JQuery<HTMLElement>[]}
     * @returns {HTMLUListElement[]}
     */
     */
     menuDataGroups(groups) {
     menuDataGroups(groups) {
         /**
         /**
         * @type {JQuery<HTMLElement>[]}
         * @type {HTMLUListElement[]}
         */
         */
         const children = [];
         const children = [];
159行目: 263行目:


     /**
     /**
     * @param {Node} node
     * @returns {HTMLElement}
     * @returns {HTMLElement | null}
    */
    static get leftMenuContainerElement() {
        return window.document
            .getElementById("mw-mf-page-left");
    }
 
    /**
    * @returns {HTMLDivElement | null}
    */
    static get leftMenuElement() {
        return /** @type {HTMLDivElement} */ (
            MobileUIBuilder
                .leftMenuContainerElement
                .getElementsByClassName(MobileUIBuilder.menuClass)
                .item(0)
        );
    }
 
    /**
     * @returns {HTMLUListElement | null}
    */
    static get userMenuElement() {
        return window
            .document
            .querySelector(".minerva-user-menu > ul.toggle-list__list");
    }
 
    /**
    * @returns {this}
    */
    setNotArticleClass() {
        if (this.wgIsArticle && !this.wgIsMainPage) return this;
 
        MobileUIBuilder
            .leftMenuContainerElement
            .classList
            .add("mobile-ui-not-article");
 
        return this;
    }
 
    /**
    * @param {UserMenuItem} item
    * @returns {HTMLLIElement}
     */
     */
     elementIfTarget(node) {
     createUserMenuItemElement(item) {
         const element = /** @type {HTMLElement} */ (node);
         const li = window.document.createElement("li");
         const { classList } = element;
        li.classList.add("toggle-list-item");
 
        const a = window.document.createElement("a");
        a.classList.add("toggle-list-item__anchor");
        a.href = [this.wgScriptPath, item.title].join("/");
        li.append(a);
 
         const outerSpan = window.document.createElement("span");
        outerSpan.classList.add("toggle-list-item__icon", "mw-ui-icon", "mw-ui-icon-before", ...item.iconClass);
        a.append(outerSpan);
 
        const spacingSpan = window.document.createElement("span");
        spacingSpan.textContent = " ";
        outerSpan.append(spacingSpan);


         if (!classList) return null;
         const innerSpan = window.document.createElement("span");
         if (!classList.contains("menu")) return null;
         innerSpan.classList.add("toggle-list-item__label");
         if (classList.contains("menu-right")) return null;
         innerSpan.textContent = item.text;
        outerSpan.append(innerSpan);


         return element;
         return li;
     }
     }


     /**
     /**
     * @returns {HTMLElement}
    * @param {readonly UserMenuItem[]} items
     * @returns {this}
     */
     */
     get navElement() {
     extendUserMenu(items) {
         return window.document.getElementById("mw-mf-page-left");
         const menu = MobileUIBuilder.userMenuElement;
 
        if (menu) {
            const lastItem = menu.lastElementChild;
            if (lastItem) {
                for (const item of items) {
                    const li = this.createUserMenuItemElement(item);
                    menu.insertBefore(li, lastItem);
                }
            }
        }
 
        return this;
     }
     }


     /**
     /**
     * @returns {HTMLElement | null}
     * @returns {this}
     */
     */
     get menuElement() {
     prebuild() {
         return /** @type {HTMLElement} */ (
         return this
             this.navElement.getElementsByClassName("menu").item(0)
             .setNotArticleClass()
        );
            ;
     }
     }


     /**
     /**
     * @param {MobileUI} mobileUI
     * @param {MobileUI} mobileUI
     * @param {HTMLElement} menuElement
     * @param {HTMLDivElement} leftMenu
    * @returns {this}
     */
     */
     modify(mobileUI, menuElement) {
     modify(mobileUI, leftMenu) {
         const leftGroups = this.menuDataGroups(mobileUI.leftGroups);
         const firstGroup = leftMenu.getElementsByTagName("ul").item(0);
         const rightGroups = [];
         if (firstGroup) {
            firstGroup.classList.add(MobileUIBuilder.hiddenClass);
        }


         const jqMenu = this.$(menuElement);
         const customLeftGroups = this.menuDataGroups(mobileUI.leftGroups);
         jqMenu.find("ul").first().remove();
         leftMenu.prepend(...customLeftGroups);
        jqMenu.prepend(leftGroups);


         this
         return this;
            .$("<div>")
            .addClass(["menu", "menu-right"])
            .append(rightGroups)
            .insertAfter(jqMenu);
     }
     }


     /**
     /**
     * @param {MobileUI} mobileUI
     * @param {MobileUI} mobileUI
    * @returns {this}
     */
     */
     build(mobileUI) {
     build(mobileUI) {
         const element = this.menuElement;
         if (mobileUI.userMenuItems) {
            this.extendUserMenu(mobileUI.userMenuItems);
        }


         if (element) {
        const leftMenuContainer = window.document.getElementById("mw-mf-page-left");
             this.modify(mobileUI, element);
 
             return;
        const leftMenu = /** @type {HTMLDivElement} */ (
            leftMenuContainer.getElementsByClassName(MobileUIBuilder.menuClass).item(0)
        );
 
         if (MobileUIBuilder.leftMenuElement) {
             this.modify(mobileUI, leftMenu);
             return this;
         }
         }


222行目: 403行目:
             for (const record of mutations) {
             for (const record of mutations) {
                 for (const node of record.addedNodes) {
                 for (const node of record.addedNodes) {
                     const element = this.elementIfTarget(node);
                     const maybeLeftMenu = /** @type {HTMLDivElement} */ (node);
                     if (element) {
                    const { classList } = maybeLeftMenu;
                     if (classList && classList.contains(MobileUIBuilder.menuClass)) {
                         observer.disconnect();
                         observer.disconnect();
                         this.modify(mobileUI, element);
                         this.modify(mobileUI, maybeLeftMenu);
                         return;
                         return;
                     }
                     }
232行目: 414行目:
         });
         });


         observer.observe(this.navElement, { childList: true });
         observer.observe(leftMenuContainer, { childList: true });
 
        return this;
     }
     }
}
}
MobileUIBuilder.rightMenuButtonID = "mobile-ui-right-menu-button";
MobileUIBuilder.menuClass = "menu";
MobileUIBuilder.hiddenClass = "mobile-ui-hidden";
MobileUIBuilder.navigationEnabledClass = "navigation-enabled";

2020年5月3日 (日) 01:44時点における最新版

// @ts-check

/**
 * @typedef MediaWiki
 * @prop {readonly Map<string, never>} config
 */

/**
 * @typedef WGNamespaceIds
 * @prop {number} user
 * @prop {number} user_talk
 * @prop {number} special
 */

/**
 * @typedef {{ [id: string]: string; }} WGFormattedNamespaces
 */

/**
 * @typedef MenuDataItemComponent
 * @prop {string} [text]
 * @prop {string} [title]
 * @prop {string} [href]
 * @prop {string} [id]
 * @prop {string | string[]} [class]
 * @prop {string} [dataEventName]
 */

/**
 * @typedef MenuDataItem
 * @prop {string} [id]
 * @prop {string | string[]} [class]
 * @prop {MenuDataItemComponent[]} components
 */

/**
 * @typedef MenuDataGroup
 * @prop {string} [id]
 * @prop {string | string[]} [class]
 * @prop {MenuDataItem[]} items
 */

/**
 * @typedef UserMenuItem
 * @prop {string} text
 * @prop {string} title
 * @prop {readonly string[]} iconClass
 */

/**
 * @typedef MobileUI
 * @prop {MenuDataGroup[]} leftGroups
 * @prop {readonly UserMenuItem[]} userMenuItems
 */

class MobileUIBuilder {
    /**
     * @param {MediaWiki} mw
     */
    constructor(mw) {
        // this.mw = mw;

        /**
         * @type {string}
         */
        this.wgServer = mw.config.get("wgServer");

        /**
         * @type {string}
         */
        this.wgScript = mw.config.get("wgScript");

        /**
         * @type {string}
         */
        this.wgScriptPath = mw.config.get("wgScriptPath");

        /**
         * @type {string}
         */
        this.wgPageName = mw.config.get("wgPageName");

        /**
         * @type {number}
         */
        this.wgArticleId = mw.config.get("wgArticleId");

        /**
         * @type {boolean}
         */
        this.wgIsArticle = mw.config.get("wgIsArticle");

        /**
         * @type {boolean}
         */
        this.wgIsMainPage = mw.config.get("wgIsMainPage");

        /**
         * @type {number}
         */
        this.wgUserId = mw.config.get("wgUserId");

        /**
         * @type {string}
         */
        this.wgUserName = mw.config.get("wgUserName");

        /**
         * @type {WGNamespaceIds}
         */
        this.wgNamespaceIds = Object.assign({}, mw.config.get("wgNamespaceIds"));

        /**
         * @type {WGFormattedNamespaces}
         */
        this.wgFormattedNamespaces = Object.assign({}, mw.config.get("wgFormattedNamespaces"));
    }

    /**
     * @param {MenuDataItemComponent} component
     * @returns {string}
     */
    hrefForMenuDataItemComponent(component) {
        switch (component.id) {
            case "mobile-ui-page-short-url":
                return [
                    this.wgServer,
                    this.wgScriptPath,
                    "/?curid=",
                    String(this.wgArticleId),
                ].join("");

            case "mobile-ui-page-special-user":
                return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.user],
                    ":",
                    this.wgUserName,
                ].join("");

            case "mobile-ui-page-special-contributions":
                return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.special],
                    ":",
                    "Contributions",
                    "/",
                    this.wgUserName,
                ].join("");

            case "mobile-ui-page-user-talk":
                return [
                    this.wgScriptPath,
                    "/",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.user_talk],
                    ":",
                    this.wgUserName,
                ].join("");

            case "mobile-ui-page-special-logout":
                return [
                    this.wgScript,
                    "?title=",
                    this.wgFormattedNamespaces[this.wgNamespaceIds.special],
                    ":",
                    "Logout",
                    "&returnto=",
                    this.wgPageName,
                ].join("");

            default:
                return component.title
                    ? (this.wgScriptPath + "/" + component.title)
                    : component.href || "";
        }
    }

    /**
     * @param {MenuDataItemComponent} component
     * @returns {HTMLAnchorElement}
     */
    menuDataItemComponent(component) {
        const href = this.hrefForMenuDataItemComponent(component);

        const element = window.document.createElement("a");
        element.id = component.id || "";
        element.classList.add(...[].concat(component.class || []));
        element.setAttribute("data-event-name", component.dataEventName || "");

        if (href) {
            element.href = encodeURI(href);
        }

        const textElement = window.document.createElement("span");
        textElement.textContent = component.text || href;
        element.append(textElement);

        return element;
    }

    /**
     * @param {MenuDataItem} item
     * @returns {HTMLLIElement}
     */
    menuDataItem(item) {
        /**
         * @type {HTMLAnchorElement[]}
         */
        const children = [];

        for (const component of item.components) {
            children.push(this.menuDataItemComponent(component));
        }

        const element = window.document.createElement("li");
        element.id = item.id || "";
        element.classList.add(...[].concat(item.class || []));
        element.append(...children);

        return element;
    }

    /**
     * @param {MenuDataGroup} group
     * @returns {HTMLUListElement}
     */
    menuDataGroup(group) {
        /**
         * @type {HTMLLIElement[]}
         */
        const children = [];

        for (const item of group.items) {
            children.push(this.menuDataItem(item));
        }

        const element = window.document.createElement("ul");
        element.id = group.id || "";
        element.classList.add(...[].concat(group.class || []));
        element.append(...children);

        return element;
    }

    /**
     * @param {MenuDataGroup[]} groups
     * @returns {HTMLUListElement[]}
     */
    menuDataGroups(groups) {
        /**
         * @type {HTMLUListElement[]}
         */
        const children = [];

        for (const group of groups) {
            children.push(this.menuDataGroup(group));
        }

        return children;
    }

    /**
     * @returns {HTMLElement}
     */
    static get leftMenuContainerElement() {
        return window.document
            .getElementById("mw-mf-page-left");
    }

    /**
     * @returns {HTMLDivElement | null}
     */
    static get leftMenuElement() {
        return /** @type {HTMLDivElement} */ (
            MobileUIBuilder
                .leftMenuContainerElement
                .getElementsByClassName(MobileUIBuilder.menuClass)
                .item(0)
        );
    }

    /**
     * @returns {HTMLUListElement | null}
     */
    static get userMenuElement() {
        return window
            .document
            .querySelector(".minerva-user-menu > ul.toggle-list__list");
    }

    /**
     * @returns {this}
     */
    setNotArticleClass() {
        if (this.wgIsArticle && !this.wgIsMainPage) return this;

        MobileUIBuilder
            .leftMenuContainerElement
            .classList
            .add("mobile-ui-not-article");

        return this;
    }

    /**
     * @param {UserMenuItem} item
     * @returns {HTMLLIElement}
     */
    createUserMenuItemElement(item) {
        const li = window.document.createElement("li");
        li.classList.add("toggle-list-item");

        const a = window.document.createElement("a");
        a.classList.add("toggle-list-item__anchor");
        a.href = [this.wgScriptPath, item.title].join("/");
        li.append(a);

        const outerSpan = window.document.createElement("span");
        outerSpan.classList.add("toggle-list-item__icon", "mw-ui-icon", "mw-ui-icon-before", ...item.iconClass);
        a.append(outerSpan);

        const spacingSpan = window.document.createElement("span");
        spacingSpan.textContent = " ";
        outerSpan.append(spacingSpan);

        const innerSpan = window.document.createElement("span");
        innerSpan.classList.add("toggle-list-item__label");
        innerSpan.textContent = item.text;
        outerSpan.append(innerSpan);

        return li;
    }

    /**
     * @param {readonly UserMenuItem[]} items
     * @returns {this}
     */
    extendUserMenu(items) {
        const menu = MobileUIBuilder.userMenuElement;

        if (menu) {
            const lastItem = menu.lastElementChild;
            if (lastItem) {
                for (const item of items) {
                    const li = this.createUserMenuItemElement(item);
                    menu.insertBefore(li, lastItem);
                }
            }
        }

        return this;
    }

    /**
     * @returns {this}
     */
    prebuild() {
        return this
            .setNotArticleClass()
            ;
    }

    /**
     * @param {MobileUI} mobileUI
     * @param {HTMLDivElement} leftMenu
     * @returns {this}
     */
    modify(mobileUI, leftMenu) {
        const firstGroup = leftMenu.getElementsByTagName("ul").item(0);
        if (firstGroup) {
            firstGroup.classList.add(MobileUIBuilder.hiddenClass);
        }

        const customLeftGroups = this.menuDataGroups(mobileUI.leftGroups);
        leftMenu.prepend(...customLeftGroups);

        return this;
    }

    /**
     * @param {MobileUI} mobileUI
     * @returns {this}
     */
    build(mobileUI) {
        if (mobileUI.userMenuItems) {
            this.extendUserMenu(mobileUI.userMenuItems);
        }

        const leftMenuContainer = window.document.getElementById("mw-mf-page-left");

        const leftMenu = /** @type {HTMLDivElement} */ (
            leftMenuContainer.getElementsByClassName(MobileUIBuilder.menuClass).item(0)
        );

        if (MobileUIBuilder.leftMenuElement) {
            this.modify(mobileUI, leftMenu);
            return this;
        }

        const observer = new MutationObserver((mutations, observer) => {
            for (const record of mutations) {
                for (const node of record.addedNodes) {
                    const maybeLeftMenu = /** @type {HTMLDivElement} */ (node);
                    const { classList } = maybeLeftMenu;
                    if (classList && classList.contains(MobileUIBuilder.menuClass)) {
                        observer.disconnect();
                        this.modify(mobileUI, maybeLeftMenu);
                        return;
                    }
                }
            }
        });

        observer.observe(leftMenuContainer, { childList: true });

        return this;
    }
}

MobileUIBuilder.rightMenuButtonID = "mobile-ui-right-menu-button";
MobileUIBuilder.menuClass = "menu";
MobileUIBuilder.hiddenClass = "mobile-ui-hidden";
MobileUIBuilder.navigationEnabledClass = "navigation-enabled";
Cookieは私達のサービスを提供するのに役立ちます。このサービスを使用することにより、お客様はCookieの使用に同意するものとします。