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 => `${this._generateLabel(network.pages, 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 "" + `` + /**/`${icons}` + /**/`${label}` + /**/cells.map(it => `${it}`) + ``; }); return `${rows}`; } /** * Renders the the table describing the interlanguage network. * * @param id {String} the ID to assign to the table element * @param network {InterlangNetwork} the network of pages to render * @return {String} the generated table */ render(id, network) { return "" + `` + /**/this._generateTableHead(network) + /**/this._generateTableBody(network) + `
`; } }