328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
import {html} from "htm/preact";
|
|
import {Component} from "preact";
|
|
import {Redirect} from "./MediaWiki";
|
|
|
|
|
|
/**
|
|
* 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 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 extends Component {
|
|
/**
|
|
* 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 {VNode} an appropriate label with icons for the given page
|
|
*/
|
|
_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;
|
|
|
|
const onClickCopy = (event) => {
|
|
navigator.clipboard.writeText(`[[${page.link}]]`);
|
|
event.target.classList.replace("fa-clipboard", "fa-check");
|
|
setTimeout(() => event.target.classList.replace("fa-check", "fa-clipboard"), 1000);
|
|
};
|
|
|
|
return html`
|
|
<span class="${page.exists ? "" : "redLink"}">
|
|
<a href="${page.url}" target="_blank" title="${page.link}">${labelText}</a>
|
|
<span> </span>
|
|
<a href="${page.url}?action=edit" target="_blank" title="Edit"><i class="fa fa-pencil"></i></a>
|
|
<span> </span>
|
|
<a title="Copy"><i class="fa fa-clipboard" onclick="${onClickCopy}"></i></a>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Generates the head of the table generated by `#toTable`.
|
|
*
|
|
* @param network {InterlangNetwork} the network to generate the head for
|
|
* @return {VNode} the head of the table generated by `#toTable`
|
|
*/
|
|
_generateTableHead(network) {
|
|
const topRow = html`
|
|
<tr>
|
|
<th class="sourceLabel" rowspan="2">Source</th>
|
|
<th colspan="${network.pages.length}">Destination</th>
|
|
</tr>
|
|
`;
|
|
const bottomRow = html`
|
|
<tr>
|
|
${network.pages.map(page => html`<th>${this._generateLabel(network.pages, page)}</th>`)}
|
|
</tr>
|
|
`;
|
|
|
|
return html`<thead>${topRow}${bottomRow}</thead>`;
|
|
}
|
|
|
|
/**
|
|
* Generates the body of the table generated by `#toTable`.
|
|
*
|
|
* @param network {InterlangNetwork} the network to generate the body for
|
|
* @return {HTMLElement} the body of the table generated by `#toTable`
|
|
*/
|
|
_generateTableBody(network) {
|
|
const rows = network.pages.map(srcPage => {
|
|
const label = html`<th class="sourceLabel">${this._generateLabel(network.pages, srcPage)}</th>`;
|
|
const cells = network.pages.map(dstPage => {
|
|
let type, icon, title;
|
|
const status = network.checkIfLinksTo(srcPage, dstPage);
|
|
switch (status) {
|
|
case "present":
|
|
[type, icon, title] = ["success", "check", "Present"];
|
|
break;
|
|
case "absent":
|
|
[type, icon, title] = ["error", "times", "Absent"];
|
|
break;
|
|
default:
|
|
[type, icon, title] = ["warning", "mail-forward", "Redirect"];
|
|
break;
|
|
}
|
|
if (srcPage.link.lang === dstPage.link.lang) {
|
|
if (status === "absent")
|
|
return html`<td></td>`;
|
|
|
|
[type, icon, title] = ["warning", "exclamation", "Self-link"];
|
|
}
|
|
|
|
return html`<td class="${type}" title="${title}"><i class="fa fa-${icon}"></i></td>`;
|
|
});
|
|
|
|
return html`<tr>${label}${cells}</tr>`;
|
|
});
|
|
|
|
return html`<tbody>${rows}</tbody>`;
|
|
}
|
|
|
|
/**
|
|
* Renders the the table describing the interlanguage network.
|
|
*
|
|
* @param props {Object} the element's rendering properties
|
|
* @param props.network {InterlangNetwork} the network of pages to render
|
|
* @return {VNode} the generated table
|
|
*/
|
|
render(props) {
|
|
return html`
|
|
<table id="${props.id}">
|
|
${this._generateTableHead(props.network)}
|
|
${this._generateTableBody(props.network)}
|
|
</table>
|
|
`;
|
|
}
|
|
}
|