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