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

編集の要約なし
編集の要約なし
タグ: モバイル編集 モバイルウェブ編集
編集の要約なし
タグ: モバイル編集 モバイルウェブ編集
 
(同じ利用者による、間の16版が非表示)
3行目: 3行目:
/**
/**
  * @typedef MediaWiki
  * @typedef MediaWiki
  * @prop {readonly Map<string, any>} config
  * @prop {readonly Map<string, never>} config
*/
 
/**
* @typedef WGNamespaceIds
* @prop {number} user
* @prop {number} user_talk
* @prop {number} special
*/
 
/**
* @typedef {{ [id: string]: string; }} WGFormattedNamespaces
  */
  */


28行目: 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
  */
  */


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


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


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


         /**
         /**
77行目: 105行目:
         */
         */
         this.wgUserName = mw.config.get("wgUserName");
         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"));
     }
     }


86行目: 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 || "";
         }
         }
     }
     }
108行目: 186行目:


         const element = window.document.createElement("a");
         const element = window.document.createElement("a");
        element.text = component.text || href;
        element.href = encodeURI(href);
         element.id = component.id || "";
         element.id = component.id || "";
         element.classList.add(...[].concat(component.class || []));
         element.classList.add(...[].concat(component.class || []));
         element.setAttribute("data-event-name", component.dataEventName || "");
         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;
         return element;
176行目: 260行目:


         return children;
         return children;
    }
    /**
    * @param {boolean} isVisible
    */
    static setRightMenuVisible(isVisible) {
        MobileUIBuilder
            .menuContainerElement
            .setAttribute("data-mobile-ui-right-menu-visible", String(Boolean(isVisible)));
     }
     }


190行目: 265行目:
     * @returns {HTMLElement}
     * @returns {HTMLElement}
     */
     */
     static get menuContainerElement() {
     static get leftMenuContainerElement() {
         return window.document
         return window.document
             .getElementById("mw-mf-page-left");
             .getElementById("mw-mf-page-left");
198行目: 273行目:
     * @returns {HTMLDivElement | null}
     * @returns {HTMLDivElement | null}
     */
     */
     static get menuElement() {
     static get leftMenuElement() {
         return /** @type {HTMLDivElement} */ (
         return /** @type {HTMLDivElement} */ (
             MobileUIBuilder
             MobileUIBuilder
                 .menuContainerElement
                 .leftMenuContainerElement
                 .getElementsByClassName(MobileUIBuilder.menuClass)
                 .getElementsByClassName(MobileUIBuilder.menuClass)
                 .item(0)
                 .item(0)
208行目: 283行目:


     /**
     /**
     * @returns {HTMLBodyElement | null}
     * @returns {HTMLUListElement | null}
     */
     */
     static get authenticatedBody() {
     static get userMenuElement() {
         return /** @type {HTMLBodyElement} */ (
         return window
             window.document
             .document
                .getElementsByClassName("is-authenticated")
            .querySelector(".minerva-user-menu > ul.toggle-list__list");
                .item(0)
        );
     }
     }


     /**
     /**
     * @returns {HTMLFormElement}
     * @returns {this}
     */
     */
     static get headerElement() {
     setNotArticleClass() {
         return /** @type {HTMLFormElement} */ (
         if (this.wgIsArticle && !this.wgIsMainPage) return this;
             window.document
 
                .getElementsByClassName("header")
        MobileUIBuilder
                .item(0)
            .leftMenuContainerElement
        );
             .classList
    }
            .add("mobile-ui-not-article");


    /**
         return this;
    * @returns {HTMLAnchorElement | null}
    */
    static get userButton() {
         return /** @type {HTMLAnchorElement} */ (
            MobileUIBuilder
                .headerElement
                .getElementsByClassName("user-button")
                .item(0)
        );
     }
     }


     /**
     /**
     * @returns {HTMLButtonElement | null}
    * @param {UserMenuItem} item
     * @returns {HTMLLIElement}
     */
     */
     static get rightMenuButtonElement() {
     createUserMenuItemElement(item) {
         return /** @type {HTMLButtonElement} */ (
         const li = window.document.createElement("li");
            window.document
         li.classList.add("toggle-list-item");
                .getElementById(MobileUIBuilder.rightMenuButtonID)
         );
    }


    setNotArticleClass() {
        const a = window.document.createElement("a");
         if (this.wgIsArticle && !this.wgIsMainPage) return;
         a.classList.add("toggle-list-item__anchor");
        a.href = [this.wgScriptPath, item.title].join("/");
        li.append(a);


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


    assembleRightMenuButton() {
         const spacingSpan = window.document.createElement("span");
         if (!this.wgUserId) return;
         spacingSpan.textContent = " ";
         if (!MobileUIBuilder.authenticatedBody) return;
         outerSpan.append(spacingSpan);
         if (MobileUIBuilder.rightMenuButtonElement) return;


         const userButton = MobileUIBuilder.userButton;
         const innerSpan = window.document.createElement("span");
         if (userButton && !userButton.text.trim()) {
         innerSpan.classList.add("toggle-list-item__label");
            userButton.parentElement.classList.add(MobileUIBuilder.hiddenClass);
        innerSpan.textContent = item.text;
        }
        outerSpan.append(innerSpan);


         const button = window.document.createElement("button");
         return li;
        button.id = MobileUIBuilder.rightMenuButtonID;
    }
        button.classList.add(
            "mw-ui-icon",
            "mw-ui-icon-element",
            "mobile-ui-icon-puzzle",
        );
        button.addEventListener("click", function (event) {
            event.preventDefault();


            const body = MobileUIBuilder.authenticatedBody;
    /**
            if (!body) return;
    * @param {readonly UserMenuItem[]} items
            if (body.classList.contains(MobileUIBuilder.navigationEnabledClass)) return;
    * @returns {this}
    */
    extendUserMenu(items) {
        const menu = MobileUIBuilder.userMenuElement;


             MobileUIBuilder.setRightMenuVisible(true);
        if (menu) {
            body.classList.add(
             const lastItem = menu.lastElementChild;
                MobileUIBuilder.navigationEnabledClass,
            if (lastItem) {
                 MobileUIBuilder.secondaryNavigationEnabledClass,
                for (const item of items) {
             );
                    const li = this.createUserMenuItemElement(item);
         });
                    menu.insertBefore(li, lastItem);
                 }
             }
         }


         const buttonWrapper = window.document.createElement("div");
         return this;
        buttonWrapper.append(button);
        MobileUIBuilder.headerElement.append(buttonWrapper);
     }
     }


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


     /**
     /**
     * @param {MobileUI} mobileUI
     * @param {MobileUI} mobileUI
    * @param {HTMLDivElement} leftMenu
    * @returns {this}
     */
     */
     modify(mobileUI) {
     modify(mobileUI, leftMenu) {
        const leftMenu = MobileUIBuilder.menuElement;
        if (leftMenu.classList.contains(MobileUIBuilder.leftMenuClass)) return;
 
        MobileUIBuilder.setRightMenuVisible(false);
        const menuContainer = MobileUIBuilder.menuContainerElement;
        menuContainer.addEventListener("transitionend", function () {
            const body = MobileUIBuilder.authenticatedBody;
            if (!body || body.classList.contains(MobileUIBuilder.secondaryNavigationEnabledClass)) return;
 
            MobileUIBuilder.setRightMenuVisible(false);
        });
 
         const firstGroup = leftMenu.getElementsByTagName("ul").item(0);
         const firstGroup = leftMenu.getElementsByTagName("ul").item(0);
         if (firstGroup) {
         if (firstGroup) {
325行目: 377行目:
         leftMenu.prepend(...customLeftGroups);
         leftMenu.prepend(...customLeftGroups);


         const rightMenu = window.document.createElement("div");
         return this;
        rightMenu.classList.add(...leftMenu.classList, MobileUIBuilder.rightMenuClass);
 
        const rightGroups = this.menuDataGroups(mobileUI.rightGroups);
        rightMenu.append(...rightGroups);
        menuContainer.append(rightMenu);
 
        leftMenu.classList.add(MobileUIBuilder.leftMenuClass);
     }
     }


     /**
     /**
     * @param {MobileUI} mobileUI
     * @param {MobileUI} mobileUI
    * @returns {this}
     */
     */
     build(mobileUI) {
     build(mobileUI) {
         if (MobileUIBuilder.menuElement) {
         if (mobileUI.userMenuItems) {
             this.modify(mobileUI);
            this.extendUserMenu(mobileUI.userMenuItems);
             return;
        }
 
        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;
         }
         }


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


         observer.observe(MobileUIBuilder.menuContainerElement, { childList: true });
         observer.observe(leftMenuContainer, { childList: true });
 
        return this;
     }
     }
}
}
363行目: 422行目:
MobileUIBuilder.rightMenuButtonID = "mobile-ui-right-menu-button";
MobileUIBuilder.rightMenuButtonID = "mobile-ui-right-menu-button";
MobileUIBuilder.menuClass = "menu";
MobileUIBuilder.menuClass = "menu";
MobileUIBuilder.leftMenuClass = "mobile-ui-left-menu";
MobileUIBuilder.rightMenuClass = "mobile-ui-right-menu";
MobileUIBuilder.hiddenClass = "mobile-ui-hidden";
MobileUIBuilder.hiddenClass = "mobile-ui-hidden";
MobileUIBuilder.navigationEnabledClass = "navigation-enabled";
MobileUIBuilder.navigationEnabledClass = "navigation-enabled";
MobileUIBuilder.secondaryNavigationEnabledClass = "secondary-navigation-enabled";