window.ilc = window.ilc || {};
/**
* Upon clicking the icon at `target`, copies the string `[[${link}]]` to the clipboard and temporarily replaces the
* clipboard icon with a checkmark.
*
* @param target the icon that is clicked on
* @param link {String} the link to copy to the clipboard
*/
window.ilc.onClickCopy = function(target, link) {
// noinspection JSIgnoredPromiseFromCall
navigator.clipboard.writeText(`[[${link}]]`);
target.classList.replace("fa-clipboard", "fa-check");
setTimeout(() => target.classList.replace("fa-check", "fa-clipboard"), 1000);
}
/**
* An input that can be validated.
*
* @property input {HTMLElement} the input that is validatable
*/
export class ValidatableInput {
/**
* Constructs a new validatable input.
*
* @param input {HTMLInputElement} the input that is validatable
* @param isValid {function(string): string} returns an empty string if the given input string is valid, and a
* string explaining why it is is invalid otherwise
*/
constructor(input, isValid) {
this.input = input;
this._isValid = isValid;
}
/**
* Returns the value of the underlying input element.
*
* @return {String} the value of the underlying input element
*/
getValue() {
return this.input.value;
}
/**
* Sets the value of the underlying input element.
*
* @param value {string} the value to set
*/
setValue(value) {
this.input.value = value;
}
/**
* Validates the input.
*
* @return {String} an empty string if the input string is valid, and a string explaining why it is is invalid
* otherwise
*/
validate() {
const validity = this._isValid(this.input.value);
if (validity.length === 0) this.showSuccess();
else this.showError();
return validity;
}
/**
* Marks the input as neither valid nor invalid.
*/
showBlank() {
this.input.dataset["entered"] = "false";
this.input.setCustomValidity("");
}
/**
* Marks the input as invalid and moves focus to it.
*/
showError() {
this.input.dataset["entered"] = "true";
this.input.setCustomValidity("Incorrect");
this.input.focus();
}
/**
* Marks the input as valid.
*/
showSuccess() {
this.input.dataset["entered"] = "true";
this.input.setCustomValidity("");
}
}
/**
* Interacts with the DOM to delegate messages to the user.
*/
export class MessageHandler {
/**
* Constructs a new `MessageHandler`, inserting relevant new elements into the DOM to interact with.
*
* @param parent {HTMLElement} the element to insert elements into
* @param [id] {string} the id of the div containing the message
*/
constructor(parent, id) {
this._mainDiv = document.createElement("div");
this._mainDiv.style.display = "none";
this._mainDiv.classList.add("messageInner");
parent.appendChild(this._mainDiv);
this._loadingIcon = document.createElement("i");
this._loadingIcon.classList.add("fa", "fa-spinner", "fa-spin");
this._mainDiv.appendChild(this._loadingIcon);
this._spacing = document.createElement("span");
this._spacing.innerHTML = " ";
this._mainDiv.appendChild(this._spacing);
this._textSpan = document.createElement("span");
if (id !== undefined)
this._textSpan.id = id;
this._mainDiv.appendChild(this._textSpan);
this._currentLevel = undefined;
this._callback = undefined;
}
/**
* Handles the displaying of the given message.
*
* If no message is given, the current message and the loading icon are hidden. To display an empty message next to
* the loading icon, give an empty string.
*
* @param [level] {"complete"|"progress"|"warning"|"error"|"neutral"} the level of message to display, or
* `undefined` if the entire message handler should be hidden
* @param [message] {*} the message to display
* @returns {MessageHandler} this `MessageHandler`
*/
handle(level, message) {
this._displayLevel(level);
if (level === undefined) return this; // No need to handle the rest
if (this._callback !== undefined) this._callback(level, message);
this._textSpan.innerHTML = message;
return this;
}
/**
* Calls `#handle` without any arguments.
*
* @returns {MessageHandler} this `MessageHandler`
*/
clear() {
return this.handle();
}
/**
* Sets the callback to be executed whenever a message is handler by this handler.
*
* @param callback {function("complete"|"progress"|"warning"|"error"|"neutral", [*]): *} the function to execute
* whenever a message is handled
* @returns {MessageHandler} this `MessageHandler`
*/
setCallback(callback) {
this._callback = callback;
return this;
}
/**
* Changes the appearance of the message handler to that of the given level.
*
* @param level {"complete"|"progress"|"warning"|"error"|"neutral"} the level to change appearance to
* @private
*/
_displayLevel(level) {
if (level === this._currentLevel) return;
this._currentLevel = level;
this._mainDiv.style.display = null;
this._mainDiv.classList.remove("success", "warning", "error");
switch (level) {
case "complete":
this._mainDiv.classList.add("success");
this._toggleLoadingIcon(false);
break;
case "progress":
this._toggleLoadingIcon(true);
break;
case "warning":
this._mainDiv.classList.add("warning");
this._toggleLoadingIcon(false);
break;
case "error":
this._mainDiv.classList.add("error");
this._toggleLoadingIcon(false);
break;
case "neutral":
this._toggleLoadingIcon(false);
break;
default:
this._mainDiv.style.display = "none";
return this; // No further handling necessary
}
}
/**
* Turns the loading icon on or off.
*
* @param state {boolean} `true` if and only if the loading icon should be on
* @returns {MessageHandler} this `MessageHandler`
* @private
*/
_toggleLoadingIcon(state) {
this._loadingIcon.style.display = state ? null : "none";
this._spacing.style.display = state ? null : "none";
return this;
}
}
/**
* A network of interlanguage links.
*
* @property pages {Page[]} the pages in the network
*/
export class InterlangTable {
/**
* Generates an icon element with the given title and additional classes.
*
* @param icon {string} the name of the icon to display
* @param title {string} the title of the icon, used for the `title` attribute
* @param [classes] {string[]} the additional classes to apply to the icon
* @return {String} an icon element with the given title and additional classes
* @private
*/
_createIcon(icon, title, classes) {
return ``;
}
/**
* Returns an appropriate label for the given page.
*
* The label contains a link to the page and a few buttons to help the user interact with that page. The label's
* appearance and contents depend both on the properties of the page (e.g. whether it exists and whether it's a
* redirect page) and on the other pages in this network (e.g. whether it's the only page in its language).
*
* @param pages {Page[]} a list of all pages
* @param page {Page} the page to generate a label of
* @return {String} an appropriate label with icons for the given page
* @private
*/
_generateLabel(pages, page) {
const labelText = pages.some(it => it.link.lang === page.link.lang && !it.link.equals(page.link))
? page.link.toString()
: page.link.lang;
return "" +
`` +
/**/`${labelText}` +
/**/` ` +
/**/`` +
/**/` ` +
/**/`` +
``;
}
/**
* Generates the head of the table generated by `#toTable`.
*
* @param network {InterlangNetwork} the network to generate the head for
* @return {String} the head of the table generated by `#toTable`
* @private
*/
_generateTableHead(network) {
return "" +
`` +
/**/`` +
/****/` ` +
/**/`` +
/****/` Source ` +
/****/`Destination ` +
/**/`${network.pages.map(page => ` ` +
``;
}
/**
* Generates the body of the table generated by `#toTable`.
*
* @param network {InterlangNetwork} the network to generate the body for
* @return {String} the body of the table generated by `#toTable`
* @private
*/
_generateTableBody(network) {
const rows = network.pages.map(srcPage => {
const verdict = network.getPageVerdict(srcPage);
const icons = verdict.self
.map(state => {
switch (state) {
case "perfect":
return this._createIcon("check", "Perfect 🙂", ["success"]);
case "not-found":
return this._createIcon("search", "Article does not exist 😕", ["error"]);
case "wrongly-ordered":
return this._createIcon("sort-alpha-asc", "Links are in the wrong order 😕", ["warning"]);
case "doubly-linked":
return this._createIcon("clone", "Links to the same wiki multiple times 😕", ["warning"]);
case "self-linked":
return this._createIcon("rotate-left", "Links to its own wiki 😕", ["warning"]);
case "unlinked":
return this._createIcon("chain-broken", "Misses one or more links 😕", ["error"]);
}
})
.map(it => `${it} `);
const label = this._generateLabel(network.pages, srcPage);
const cells = network.pages.map(dstPage => {
const linkState = verdict.pages.find(it => it.page.link.equals(dstPage.link)).verdict;
switch (linkState) {
case "linked":
return this._createIcon("check", "Linked 🙂", ["success"]);
case "self-linked":
return this._createIcon("rotate-left", "Links to its own wiki 😕", ["warning"]);
case "unlinked":
return this._createIcon("times", "Link is missing 😕", ["error"]);
case "self-unlinked":
return ``;
case "redirected":
return this._createIcon("mail-forward", "Links to a redirect 😕", ["warning"]);
default:
throw new Error(`Invalid link state '${linkState}'`);
}
});
return "" +
`${this._generateLabel(network.pages, page)} `)}