code:
/**
 * circumvents SugarCube, allowing a plain HTML5 UI within it
 *
 * @param {function(HTMLElement): HTMLElement} passageFunction
 */
globalThis.html5passage = function(passageFunction) {
	$(document).one(":passagedisplay", (ev) => {
		const element = document.createElement("div");
		element.classList.add("passage");
		document.getElementById("passages").appendChild(element);
		passageFunction(element);
		$(document).off(":passagedisplay");
	});
};

/**
 * If you want to include a SugarCube passage in a JS function use this. The result must be printed using the <<print>> macro.
 * @param {string} passageTitle
 * @returns {string}
 */
globalThis.jsInclude = function(passageTitle) {
	if (Story.has(passageTitle)) {
		return Story.get(passageTitle).processText();
	} else {
		return `<span class="red">Error: Passage ${passageTitle} does not exist.</span>`;
	}
};

/**
 * Creates a HTML element with custom SugarCube attributes which works as a passage link
 *
 * The result works in the same way as the wiki markup in the SugarCube
 * @see https://www.motoslave.net/sugarcube/2/docs/#markup-html-attribute
 * @param {string} linkText link text
 * @param {string} passage the passage name to link to
 * @param {string} [setter=''] setter text (optional)
 * @param {string} [tooltip=''] tooltip text (optional)
 * @param {string} [elementType='a'] element type (optional) default is 'a'.
 * Could be any of 'a', 'audio', img', 'source', 'video'
 * @returns {string} element text
 *
 * @example
 * // equal to [[Go to town|Town]]
 * App.UI.passageLink("Go to town", "Town")
 */
App.UI.passageLink = function(linkText, passage, setter = '', tooltip = '', elementType = 'a') {
	let res = `<${elementType} data-passage="${passage}"`;
	if (setter) {
		res += ` data-setter="${App.Utils.escapeHtml(setter)}"`;
	}
	if (tooltip) {
		res += ` title="${tooltip}"`;
	}
	res += `>${linkText}</${elementType}>`;
	return res;
};

App.UI.link = function() {
	let counter = 0;

	// reset all handlers for each passage
	$(document).on(':passageinit', function() {
		State.temporary.linkHandlers = {};
		counter = 0;
	});

	return makeLink;

	/**
	 * Creates a markup for a SugarCube link which executes given function with given arguments
	 *
	 * @param {string} linkText link text
	 * @param {*} handler callable object
	 * @param {*} args arguments
	 * @param {string} [passage] the passage name to link to
	 * @returns {string} link in SC markup
	 */
	function makeLink(linkText, handler, args = [], passage = '', tooltip = '') {
		// pack handler and data
		State.temporary.linkHandlers[counter] = {
			f: handler,
			args: Array.isArray(args) ? args : [args]
		};

		// can't say _linkHandlers here because SC does not recognize its own notation in "..._varName"
		let SCHandlerText =
			`State.temporary.linkHandlers[${counter}].f(...State.temporary.linkHandlers[${counter}].args);`;
		++counter;

		if (passage) {
			return App.UI.passageLink(linkText, passage, SCHandlerText, tooltip);
		} else {
			if (tooltip) {
				throw "Tooltips are not supported by the <<link>> markup.";
			}
			// data-passage scheme does not work with empty passage name
			return `<<link "${linkText}">><<run ${SCHandlerText}>><</link>>`;
		}
	}
}();

/**
 * Replaces contents of the element, identified by the given selector, with wiki'ed new content
 *
 * The function is an analogue to the SugarCube <<replace>> macro (and is a simplified version of it)
 * @param {string} selector
 * @param {string} newContent
 */
App.UI.replace = function(selector, newContent) {
	let ins = jQuery(document.createDocumentFragment());
	ins.wiki(newContent);
	const target = $(selector);
	target.empty();
	target.append(ins);
};

/**
 * A simple macro which allows to create wrapping html elements with dynamic IDs.
 *
 * idea blatantly robbed from the spanMacroJS.tw but expanded to a more generic case, allowing
,
 * <button> or whatever you want elements, default is for the div though.
 * In addition, you can pass an object in as the first argument instead of an id, and each of the
 * object's attributes will become attributes of the generate tag.
 *
 * @example
 * htag('test', "red") //
test

 * htag('test', {class: red}); //
test

 * htag('test', {class: red, id: green}); //
test

 * @param {string} text
 * @param {string|object} attributes
 * @param {string} [tag='div'] (optional)
 * @returns {string}
 */
App.UI.htag = function(text, attributes, tag = 'div') {
	const payload = text.replace(/(^\n+|\n+$)/, "");

	if (typeof attributes === "object") {
		attributes = $.map(attributes, (val, key) => `${key}="${val}"`).join(" ");
	} else {
		attributes = `id="${attributes.trim()}"`;
	}

	return `<${tag} ${attributes}>${payload}</${tag}>`;
};

App.UI.tabBar = function() {
	return {
		openTab: openTab,
		tabButton: tabButton,
		tabButtonDOM: tabButtonDOM,
		makeTab: makeTab,
		makeTabDOM: makeTabDOM,
		handlePreSelectedTab: handlePreSelectedTab,
		tabChoiceVarName: tabChoiceVarName
	};

	function openTab(evt, tabName) {
		/* var passage = passage().trim().replace(/ /g,"+");*/
		const tabcontent = document.getElementsByClassName("tab-content");
		for (let i = 0; i < tabcontent.length; i++) {
			tabcontent[i].style.display = "none";
		}
		const tablinks = document.getElementsByClassName("tab-links");
		for (let i = 0; i < tablinks.length; i++) {
			tablinks[i].className = tablinks[i].className.replace(" active", "");
		}
		V.tabChoice[tabChoiceVarName()] = tabName; /* The regex strips spaces and " ' " from passage names, making "Servants' Quarters" into "ServantsQuarters" and allowing it to be used as a label in this object. */
		document.getElementById(tabName).style.display = "block";
		evt.currentTarget.className += " active";
	}

	/**
	 * @param {string} name
	 * @param {string} text
	 * @returns {string}
	 */
	function tabButton(name, text) {
		return `<button class="tab-links" onclick="App.UI.tabBar.openTab(event, '${name}')" id="tab ${name}">${text}</button>`;
	}

	/**
	 * @param {string} name
	 * @param {string} text
	 * @param {boolean} [plainLink]
	 * @returns {HTMLButtonElement|HTMLAnchorElement}
	 */
	function tabButtonDOM(name, text, plainLink = false) {
		if (plainLink) {
			const link = document.createElement("a");
			link.classList.add("tab-links", "pure");
			link.id = `tab ${name}`;
			link.textContent = text;
			link.addEventListener('click', event => {
				openTab(event, name);
			});
			return link;
		} else {
			const button = document.createElement("button");
			button.classList.add("tab-links");
			button.id = `tab ${name}`;
			button.textContent = text;
			button.addEventListener('click', event => {
				openTab(event, name);
			});
			return button;
		}
	}

	/**
	 * @param {string} name
	 * @param {string} content
	 * @returns {string}
	 */
	function makeTab(name, content) {
		return `<div id="${name}" class="tab-content"><div class="content">${content}</div></div>`;
	}

	/**
	 * @param {string} name
	 * @param {Node} content
	 * @returns {HTMLDivElement}
	 */
	function makeTabDOM(name, content) {
		const outerDiv = document.createElement("div");
		outerDiv.id = name;
		outerDiv.classList.add("tab-content");
		const innerDiv = document.createElement("div");
		innerDiv.classList.add("content");
		innerDiv.append(content);
		outerDiv.append(innerDiv);
		return outerDiv;
	}

	function handlePreSelectedTab(defaultTab = "assign", immidiate = false) {
		let selectedTab = V.tabChoice[tabChoiceVarName()];
		if (!selectedTab) {
			selectedTab = defaultTab;
		}

		function selectTab() {
			let tabBtn = document.getElementById(`tab ${selectedTab}`);
			if (!tabBtn) {
				tabBtn = document.getElementsByClassName('tab-links').item(0);
			}
			if (tabBtn) {
				tabBtn.click();
			}
		}
		if (immidiate) {
			selectTab();
		} else {
			$(document).one(':passageend', selectTab);
		}
	}

	function tabChoiceVarName() {
		return passage().trim().replace(/ |'/g, '');
	}
}();

/**
 * Creates a span for an link with tooltip containing the reasons why it is disabled
 * @param {string} link
 * @param {string[]} reasons
 * @returns {string}
 */
App.UI.disabledLink = function(link, reasons) {
	const tooltips = reasons.length === 1 ?
		`<span class="tooltip">${reasons}</span>` :
		`<div class="tooltip"><ul>${reasons.map(e => `
${e}
`).join('')}</ul></div>`;
	return `<span class="textWithTooltip">${link}${tooltips}</span>`;
};

/** handler function for slaveDescriptionDialog. do not call directly. */
App.UI._showDescriptionDialog = function(slave) {
	Dialog.setup(SlaveFullName(slave));
	const image = V.seeImages ? App.UI.DOM.makeElement("div", App.Art.SlaveArtElement(slave, 2, 0), ["imageRef", "medImg"]) : '';
	Dialog.append(image).append(App.Desc.longSlave(slave, {eventDescription: 1}));
	Dialog.open();
};

/**
 * Generates a link which shows a slave description dialog for a specified slave.
 * Do not call from within another dialog.
 * @param {App.Entity.SlaveState} slave
 * @returns {string} link (in SC markup)
 */
App.UI.slaveDescriptionDialog = function(slave) {
	return App.UI.link(SlaveFullName(slave), App.UI._showDescriptionDialog, [slave]);
};

/**
 * Generates a link which shows a slave description dialog for a specified slave.
 * Do not call from within another dialog.
 * @param {App.Entity.SlaveState} slave
 * @returns {HTMLElement} link
 */
App.UI.DOM.slaveDescriptionDialog = function(slave) {
	return App.UI.DOM.link(SlaveFullName(slave), App.UI._showDescriptionDialog, [slave]);
};

/**
 * Reloads the passage and stays at the same height.
 */
App.UI.reload = function() {
	const position = window.pageYOffset;
	Engine.play(passage());
	window.scrollTo(0, position);
};

/**
 * Renders passage into a document fragment
 * NOTE: This is a greatly simplified version of the SC Passage.render() private function
 * This function does not trigger :passagestart and :passagedisplay events
 * @param {string} passageTitle
 * @returns {DocumentFragment} document fragment with passage contents
 */
App.UI.DOM.renderPassage = function(passageTitle) {
	const res = document.createDocumentFragment();
	if (ProfileInclude.IsEnabled) {
		ProfileInclude.IncludeBegins(passageTitle);
	}
	$(res).wiki(jsInclude(passageTitle));
	if (ProfileInclude.IsEnabled) {
		ProfileInclude.IncludeEnds();
	}
	return res;
};

/**
 * Render passage and append the rendered content to the container
 * @param {Node} container
 * @param {string} passageTitle
 */
App.UI.DOM.includePassage = function(container, passageTitle) {
	return $(container).append(this.renderPassage(passageTitle));
};

repo_name: NovX/fc

license: mit