Skip to content
Snippets Groups Projects
Forked from pregmodfan / fc-pregmod
14615 commits behind the upstream repository.
utilsSC.js 9.49 KiB
/**
 * 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
	 * @template {function(...any):void} F
	 * @param {string} linkText link text
	 * @param {F} handler callable object
	 * @param {Parameters<F>} [args] arguments
	 * @param {string} [passage] the passage name to link to
	 * @param {string} [tooltip]
	 * @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);
};

App.UI.tabBar = function() {
	return {
		openTab: openTab,
		tabButton: tabButton,
		makeTab: makeTab,
		handlePreSelectedTab: handlePreSelectedTab,
		tabChoiceVarName: tabChoiceVarName,
		openLeftTab: openLeft,
		openRightTab: openRight
	};

	function openTab(evt, tabName) {
		/* var passage = passage().trim().replace(/ /g,"+");*/
		const tabcontent = /** @type {HTMLCollectionOf<HTMLElement>}*/(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
	 * @param {boolean} [plainLink]
	 * @returns {HTMLButtonElement|HTMLAnchorElement}
	 */
	function tabButton(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 {Node} content
	 * @returns {HTMLDivElement}
	 */
	function makeTab(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 = /** @type {HTMLElement}*/(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, '');
	}

	function openLeft() {
		const tabLinks = /** @type {HTMLCollection<HTMLButtonElement>} */ document.getElementsByClassName("tab-links");
		const index = currentIndex(tabLinks);
		if (index - 1 >= 0) {
			tabLinks[index - 1].click();
		}
	}

	function openRight() {
		const tabLinks = /** @type {HTMLCollection<HTMLButtonElement>} */ document.getElementsByClassName("tab-links");
		const index = currentIndex(tabLinks);
		if (index > -1 && index + 1 < tabLinks.length) {
			tabLinks[index + 1].click();
		}
	}

	function currentIndex(collection) {
		// get current tab button
		for (let i = 0; i < collection.length; i++) {
			if (collection[i].classList.contains("active")) {
				return i;
			}
		}
		return -1;
	}
}();

/**
 * 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 => `<li>${e}</li>`).join('')}</ul></div>`;
	return `<span class="textWithTooltip">${link}${tooltips}</span>`;
};

/** handler function for slaveDescriptionDialog. do not call directly. */
App.UI._showDescriptionDialog = function(slave, options) {
	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, options));
	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
 * @param {FC.Desc.LongSlaveOptions} options
 * @returns {string} link (in SC markup)
 */
App.UI.slaveDescriptionDialog = function(slave, options = {eventDescription: true}) {
	return App.UI.link(SlaveFullName(slave), App.UI._showDescriptionDialog, [slave, options]);
};

/**
 * 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
 * @param {string} [text] link text to use instead of slave name
 * @param {FC.Desc.LongSlaveOptions} options
 * @returns {HTMLElement} link
 */
App.UI.DOM.slaveDescriptionDialog = function(slave, text, options = {eventDescription: true}) {
	return App.UI.DOM.link(text ? text : SlaveFullName(slave), App.UI._showDescriptionDialog, [slave, options]);
};

/**
 * 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));
};