「MediaWiki:MobileUI.js」の版間の差分
表示
編集の要約なし タグ: モバイル編集 モバイルウェブ編集 |
細編集の要約なし タグ: モバイル編集 モバイルウェブ編集 |
||
| (同じ利用者による、間の28版が非表示) | |||
| 1行目: | 1行目: | ||
// @ts-check | // @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} | |||
*/ | */ | ||
const | 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} | |||
*/ | */ | ||
const | 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 | 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 | 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"; | |||
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";