Utilities library to help build pages.
此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.greasyfork.org/scripts/580181/1838058/Page%20builder%20utils%20library.js
// ==UserScript==
// @name Page builder utils library
// @namespace PageBuilderLib
// @description Utilities library to help build pages.
// @author kekekeKaj
// @version 2.0.0.3
// @grant none
// ==/UserScript==
// IMPORTANT: update version number when publishing!!!
function getPageBuilderLibInfo() {
return 'Page Builder Lib v2.0.0.3';
}
// --- GENERIC BUILDERS ---
/**
* @param {string} tagName The element tag.
* @param {string} id Id for the element. Element will not have ID if not present.
* @param {boolean} isInline Boolean flag indicating whether element should be displayed inline.
* @returns {Element} The built element.
*/
function buildElement(tagName, id = null, isInline = false) {
const element = document.createElement(tagName);
if (id != null) {
element.id = id;
}
if (isInline) {
setInlineBlockDisplayStyle(element);
} else {
setBlockDisplayStyle(element);
}
return element;
}
function buildDivWithChildren(id, children) {
const divElement = buildElement('div', id);
children.forEach(child => divElement.appendChild(child));
return divElement;
}
function buildBtn(id, labelTxt, clickFunc) {
const buttonElement = buildElement('button', id, true);
buttonElement.setAttribute('class', 'inputButton');
buttonElement.innerText = labelTxt;
if (clickFunc != null) {
buttonElement.addEventListener("click", clickFunc);
}
return buttonElement;
}
function buildTextArea(id, width, height, defaultText = '') {
let textAreaElement = buildElement('textarea', id);
textAreaElement.cols = width;
textAreaElement.rows = height;
textAreaElement.className = 'textarea';
textAreaElement.innerText = defaultText;
return textAreaElement;
}
function setBlockDisplayStyle(element) {
element.style.display = 'block';
element.style.marginTop = getPageBuilderConsts().stdMargin;
}
function setInlineBlockDisplayStyle(element) {
element.style.display = 'inline-block';
element.style.marginRight = getPageBuilderConsts().stdMargin;
}
/**
* Builds a selector element.
*
* @param {string} id The ID to assign to the selector.
* @param {string} defaultOptionText The default option text, e.g. "Select a melon"
* @param {string[]} optionTexts The list of options texts.
* @param callFunction the callback function for when the selection changes.
*
* @returns the selector element.
*/
function buildSelectorElement(id, defaultOptionText, optionTexts, callFunction) {
let selectElement = buildElement('select', id, false);
selectElement.setAttribute('class', 'inputtext');
if (defaultOptionText != null) {
const defaultOption = document.createElement('option');
defaultOption.setAttribute('selected', true);
defaultOption.setAttribute('disabled', true);
defaultOption.innerText = defaultOptionText;
selectElement.appendChild(defaultOption);
}
for (let optionText of optionTexts) {
const optionElement = document.createElement('option')
optionElement.setAttribute('value', optionText);
optionElement.innerHTML = optionText;
selectElement.appendChild(optionElement);
}
if (callFunction != null) {
selectElement.addEventListener('change', callFunction);
}
return selectElement;
}
function buildLabel(labelTxt, isInline = false, isBold = true, id = null) {
const labelElement = buildElement('label', id, isInline);
if (isBold) {
labelElement.style.fontWeight = 'bold';
}
labelElement.innerText = labelTxt;
return labelElement;
}
function buildLabeledInputElement(inputId, labelText, inputWidth, isInline = true) {
const labelElement = buildLabel(labelText, isInline);
labelElement.style.width = '75px';
const inputElement = buildElement('input', inputId, isInline);
inputElement.setAttribute('size', `${inputWidth}`);
return buildDivWithChildren(null, [ labelElement, inputElement ]);
}
function buildSpinnerElement() {
const spinnerElement = document.createElement('img');
spinnerElement.height = '10';
spinnerElement.width = '10';
spinnerElement.src = '/images/xmlhttp-loader.gif';
return spinnerElement;
}
function buildAnchorElement(url, text) {
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.innerText = text;
return anchorElement;
}
function buildTickedIconElement() {
const tickedIconElement = document.createElement('span');
tickedIconElement.append(document.createTextNode('✅'));
return tickedIconElement;
}
function buildStatusLabel(id, labelTxt = '') {
const statusLabel = buildLabel(labelTxt, false, true, id);
statusLabel.style.color = 'red';
return statusLabel;
}
/**
* Builds a set of label and value elements.
*
* @param {string} labelText the text for the label.
* @param {string} fieldId The ID to assign to the field (for accessing the value).
* @returns the containing element.
*/
function buildLabelAndFieldElement(labelText, fieldId, isInline = true) {
const valueTag = isInline ? 'label' : 'p'
const valueLabel = buildElement(valueTag, fieldId, isInline);
let labelAndValueDiv = buildDivWithChildren(
null, [ buildLabel(labelText, true), valueLabel ]);
return labelAndValueDiv;
}
function buildLabelAndValueDisplayElement(labelText, value, isInline = true, isFormatted = false) {
const valueTag = isInline ? 'label' : 'p'
const valueLabel = buildElement(valueTag, null, isInline);
if (isFormatted) {
valueLabel.innerHTML = value;
} else {
valueLabel.innerText = value;
}
let labelAndValueDiv = buildDivWithChildren(
null, [ buildLabel(labelText, true), valueLabel ]);
return labelAndValueDiv;
}
function buildMsgPreviewPanelFromSpecs(specs) {
const msgPreviewPanel = buildDivWithChildren(
null,
[
buildLabelAndFieldElement('To', specs.previewRecipientSpec.id),
buildLabelAndFieldElement('Subject', specs.previewMsgSubjectSpec.id),
buildLabelAndFieldElement('Message', specs.previewMsgBodySpec.id, false)
]);
msgPreviewPanel.style.display = 'none';
return msgPreviewPanel;
}
function buildRecipientOptions(reportedUser, reporter) {
const constants = getPageBuilderConsts();
return [
reportedUser != null && reportedUser.trim().length > 0 ?
`${constants.reportedUserPrefix} ${reportedUser}` : '!UNKNOWN VICTIM!',
reporter != null && reporter.trim().length > 0 ?
`${constants.reporterPrefix} ${reporter}` : '!UNKNOWN REPORTER!'
];
}
// --- BUILDER THAT USES SPECIFICATIONS ---
function buildLabeledInputFromSpecs(specs) {
return buildLabeledInputElement(specs.id, specs.labelTxt, 50);
}
function buildSelectorFromSpec(spec) {
return buildSelectorElement(
spec.id, spec.defaultTxt, spec.optionTxts, spec.callFunc);
}
function buildTextAreaFromBlueprint(blueprint) {
return buildTextArea(
blueprint.id, blueprint.width, blueprint.height, blueprint.defaultTxt);
}
function buildButtonFromSpec(spec) {
return buildBtn(spec.id, spec.labelTxt, spec.callFunc);
}
function buildMsgPanelFromSpecs(specs) {
const templateSelector = buildSelectorFromSpec(specs.msgTemplateSelectorSpec);
const recipientSelector = buildSelectorFromSpec(specs.msgRecipientSelectorSpec);
const subjectPanel = buildLabeledInputFromSpecs(specs.msgSubjectSpec);
const msgBodyLabel = buildLabel(specs.msgBodySpec.labelTxt, false);
const msgBodyInput = buildTextAreaFromBlueprint(specs.msgBodySpec);
const actionElements = [];
specs.msgActionButtonsSpecs.forEach(btnSpec => {
actionElements[actionElements.length] = buildButtonFromSpec(btnSpec)
});
const actionPanel = buildDivWithChildren(null, actionElements);
const statusLabel = buildStatusLabel(specs.statusDisplaySpec.id, specs.statusDisplaySpec.labelTxt);
const msgPreviewPanel = buildMsgPreviewPanelFromSpecs(specs);
const msgPanel = buildDivWithChildren(
specs.id,
[
templateSelector,
recipientSelector,
subjectPanel,
msgBodyLabel,
msgBodyInput,
actionPanel,
statusLabel,
msgPreviewPanel
]
)
return msgPanel;
}
// --- SPECIFICATION RELATED FUNCTIONS ---
function draftMsgPanelSpecs() {
return {
msgPanelId: 'msgPanel',
msgTemplateSelectorSpec: draftSelectorSpec(
'msgTemplateSelector', 'Select a template', [], null),
msgRecipientSelectorSpec: draftSelectorSpec('msgRecipient', 'Select a recipient', [], null),
msgSubjectSpec: draftGenericControlSpec('msgSubject', 'Subject'),
msgBodySpec: draftTextAreaSpec('msgBody', 'Message'),
msgActionButtonsSpecs: [
draftGenericControlSpec('previewBtn', 'Preview'),
draftGenericControlSpec('sendBtn', 'Send!') ],
statusDisplaySpec: draftGenericControlSpec('sendMsgStatus', '', null),
previewRecipientSpec: draftGenericControlSpec('previewRecipient', 'Recipient:'),
previewMsgSubjectSpec: draftGenericControlSpec('previewMsgSubject', 'Subject:'),
previewMsgBodySpec: draftGenericControlSpec('previewMsgBody', 'Message body:'),
sendPreviewedMsgButtonSpec: draftGenericControlSpec('sendPreviewedMessage', 'Send!')
};
}
function draftGenericControlSpec(idStr, labelText = null, callFunction = null) {
return {
id: idStr,
labelTxt: labelText,
callFunc: callFunction
};
}
function draftSelectorSpec(idStr, defaultOptionText, optionTexts, callFunction) {
return {
id: idStr,
defaultTxt: defaultOptionText,
optionTxts: optionTexts,
callFunc: callFunction
};
}
function draftTextAreaSpec(idStr, labelText, defaultText = '', boxWidth = 100, boxHeight = 20) {
return {
id: idStr,
labelTxt: labelText,
defaultTxt: defaultText,
width: boxWidth,
height: boxHeight
}
}
// ----- EVENT RELATED PROCESSING -----
function msgTemplateSelected(specs, msgTemplateMap, reportType) {
const msgSummary = document.getElementById(specs.msgTemplateSelectorSpec.id).value;
const msgBody = msgTemplateMap.get(msgSummary);
const constants = getPageBuilderConsts();
const action = extractActionType(msgSummary);
const recipientPrefix = identifyRecipient(constants.reportedUserPrefix, constants.reporterPrefix, action);
selectRecipient(specs.msgRecipientSelectorSpec.id, recipientPrefix);
document.getElementById(specs.msgSubjectSpec.id).value =
buildSubjectTemplate(action, reportType);
document.getElementById(specs.msgBodySpec.id).value = msgBody;
}
function msgActionBtnClicked(specs, csrfToken, appendix, modSettings, isSendAction = false) {
const msgDetails = buildMsgDetails(specs, appendix, modSettings);
const statusDisplay = document.getElementById(specs.statusDisplaySpec.id);
try {
validateMsgDetails(msgDetails, specs.statusDisplaySpec.id);
statusDisplay.innerText = '';
} catch(errorMsg) {
statusDisplay.innerText = errorMsg;
return;
}
const msgBody = buildMsgBody(msgDetails);
if (isSendAction) {
sendMsg(
msgDetails.recipient, msgDetails.subject, msgBody,
specs.statusDisplaySpec.id, csrfToken);
} else {
previewMsg(msgDetails.recipient, msgDetails.subject, msgBody, specs);
}
}
function buildMsgDetails(specs, appendix, modSettings) {
const msgDetails = {
recipient: getRecipientNameFromSelector(specs.msgRecipientSelectorSpec.id),
moderator: modSettings.modName,
moderatorTitle: modSettings.modTitle,
subject: document.getElementById(specs.msgSubjectSpec.id).value,
msg: document.getElementById(specs.msgBodySpec.id).value,
appendix: appendix
}
return msgDetails;
}
function getRecipientNameFromSelector(selectorId) {
const recipientSelectorVal = document.getElementById(selectorId).value;
const tokens = recipientSelectorVal.split(" ");
if (tokens.length != 2) {
console.warn('Recipient name is of unusual format: ' + recipientSelectorVal);
return null;
}
return tokens[1];
}
function validateMsgDetails(msgDetails) {
if (msgDetails.recipient == null || msgDetails.recipient.trim().length < 1) {
throw 'Unknown recipient - account may have been deleted.';
}
if (msgDetails.subject == null || msgDetails.subject.trim().length < 1) {
throw 'subject is empty';
}
if (msgDetails.msg == null || msgDetails.msg.trim().length < 1) {
throw 'message is empty';
}
}
function buildMsgBody(msgDetails) {
return `Hello ${msgDetails.recipient},
${msgDetails.msg}
${msgDetails.moderator}
${msgDetails.moderatorTitle}
${msgDetails.appendix}
`;
}
function previewMsg(recipient, subject, msgBody, msgPanelSpecs) {
document.getElementById(msgPanelSpecs.previewRecipientSpec.id).innerText = recipient;
document.getElementById(msgPanelSpecs.previewMsgSubjectSpec.id).innerText = subject;
document.getElementById(msgPanelSpecs.previewMsgBodySpec.id).innerText = msgBody;
const previewPanel = document.getElementById(msgPanelSpecs.previewRecipientSpec.id).parentElement.parentElement;
previewPanel.style.display = '';
}
/**
* Function for sending a messager to a user.
*
* @param {*} msgDetails the object containing the message details
*/
function sendMsg(recipient, subject, msgBody, statusDisplayId, csrfToken) {
const spinner = buildSpinnerElement();
document.getElementById(statusDisplayId).innerHTML =
'Sending PM... ' + spinner.outerHTML;
const data = buildSendMessageData(subject, msgBody, csrfToken);
postMsgReq(
recipient,
data,
(responseTxt) => updateNotice(getSendMsgResultFromResponseTxt(responseTxt), statusDisplayId),
(error) => {
console.error(error);
updateNotice(appendInboxLink('Send probably failed. Please try again.'), statusDisplayId);
});
}
function postMsgReq(recipient, data, successCallback, errorCallback) {
fetch(
'https://myanimelist.net/mymessages.php?go=send&toname=' + recipient,
{
method: "POST",
body: new URLSearchParams(data),
})
.then(response => {
if (response.ok) {
return response.text();
}
throw new Error('Request failed for URL: ' + response.url);
})
.then(successCallback)
.catch(errorCallback);
}
function updateNotice(formattedMsg, statusDisplayId) {
document.getElementById(statusDisplayId).innerHTML = formattedMsg;
}
function getSendMsgResultFromResponseTxt(responseText) {
if (/Successfully sent/.test(responseText)) {
return appendInboxLink('Sent!');
}
if (/You may only have 75/.test(responseText)) {
return appendInboxLink('Inbox is full!');
}
if (/You may only have 100/.test(responseText)) {
return appendInboxLink('Sent Box is full!');
}
return appendInboxLink('Send probably failed. Please try again.');
}
function appendInboxLink(message) {
return `${message} (<a href="https://myanimelist.net/mymessages.php?go=sent">Check Sent</a>)`;
}
function buildSendMessageData(msgSubject, msgBody, csrfToken) {
return {
subject: msgSubject,
message: msgBody,
sendmessage: 'Sending...',
action_type: 'sendmessage',
ignore_recaptcha: 1,
csrf_token: csrfToken
}
}
/**
* Function for figuring the action type from the stock message description text.
* @param {String} actionStr
* @returns a string representing the action type, or an empty string if it can't figure it out.
*/
function extractActionType(actionStr) {
const constants = getPageBuilderConsts();
const actionStrPrefixes = [
constants.removalAction,
constants.editAction,
constants.rejectAction,
constants.sendAction,
constants.previewAction,
constants.featureAction
];
for (const prefix of actionStrPrefixes) {
if (actionStr.toLowerCase().startsWith(prefix)) {
return prefix;
}
}
return "";
}
function identifyRecipient(reportedUserIdentifier, reporterIdentifier, action) {
const constants = getPageBuilderConsts();
if ([constants.editAction, constants.removalAction, constants.featureAction].includes(action)) {
return reportedUserIdentifier;
}
if (action === constants.rejectAction) {
return reporterIdentifier;
}
return null;
}
function selectRecipient(selectorId, optionTextPrefix) {
const selector = document.getElementById(selectorId);
for (let child of selector.children) {
if (child.tagName.toLowerCase() === 'option' &&
child.innerText.startsWith(optionTextPrefix)) {
selector.value = child.value;
return;
}
}
console.warn('Cannot find recipient');
}
function buildSubjectTemplate(actionTypeStr, contentType) {
if (actionTypeStr == null || contentType == null) {
console.warn('Cannot build subject template');
return ''
}
const constants = getPageBuilderConsts();
switch(actionTypeStr) {
case constants.rejectAction: return "Report rejected";
case constants.editAction: return `${contentType} edited`;
case constants.removalAction: return `${contentType} removed`;
case constants.featureAction: return `Your ${contentType} has been featured!`;
default:
console.warn('Cannot build subject template');
return '';
}
}
function buildNormalHeader(headerTxt) {
const headerElement = buildElement('div');
headerElement.className = 'normal_header';
headerElement.style.textAlign = 'left';
headerElement.innerText = headerTxt;
return headerElement;
}
// TODO: CHECK IF ALL NEEDED
function getPageBuilderConsts() {
return {
removalAction: "remove",
editAction: "edit",
rejectAction: "reject",
sendAction: "send",
featureAction: "feature",
previewAction: "preview",
stackLink: 'stack',
profileLink: 'profile',
unknownLink: 'unknown',
msgSentResult: 'sent',
unknownResult: 'unknown',
inboxFullResult: 'inboxFull',
sentBoxFullResult: 'sentBoxFull',
emptyFieldsResult: 'emptyFields',
reportedUserPrefix: '[victim]',
reporterPrefix: '[reporter]',
stackType: 'stack',
recType: 'rec',
reviewType: 'review',
stdMargin: '5px'
}
}