/ src / scripts / help.js
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Eswara Rajesh Pinapala epinapala@live.com
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
current_activity_closed = true;
current_activity_top = 0;
previous_help = null;
/*
 * Toggles Help element from display:none and display block.
 * Also changes the width of the Current activity accordingly.
 * @param String help element's id to toggle.
 * @param String is_mobile flag true/false.
 * @param String target_controller Edit page's controller name.
 */
/*
 * How much of the window is kept below the top of the help panel, so that
 * a panel brought into view shows its heading and the start of what it
 * says rather than only its topmost edge.
 */
var HELP_TOP_ROOM = 60;
/*
 * Brings the top of the help panel into the window. The panel keeps
 * wherever it was last put, and a page scrolled since, or a window made
 * shorter, can leave that place above or below what is on screen; a
 * reader then presses the mark and sees nothing happen. Where the top is
 * already in view the panel is left alone, so this does not undo a
 * reader's own dragging.
 *
 * @param String id which panel to bring into view
 */
function showHelpTop(id)
{
    var frame = elt(id);
    if (!frame || frame.style.display === "none") {
        return;
    }
    var rect = frame.getBoundingClientRect();
    var lowest = window.innerHeight - HELP_TOP_ROOM;
    if (rect.top >= 0 && rect.top <= lowest) {
        return;
    }
    frame.style.position = "fixed";
    frame.style.top = Math.max(0, Math.min(rect.top, lowest)) + "px";
    frame.style.left = Math.max(0, rect.left) + "px";
    frame.style.right = "auto";
    frame.style.margin = "0";
}
function toggleHelp(id, is_mobile, target_controller)
{
    let activity = 'current-activity';
    let images = document.querySelectorAll(".wiki-resource-image");
    let all_help_elements =
        document.getElementsByClassName(activity);
    let help_node = all_help_elements[0];
    if (current_activity_closed === true && help_node) {
        current_activity_top = getCssProperty(help_node, 'top');
    }
    obj = elt(id);
    if (is_mobile === false) {
        toggleDisplay(id, "inline-block");
    } else {
        toggleDisplay(id, "inline-block");
        let height_after_toggle = (elt("mobile-help").clientHeight);
        /* clientHeight only returns in pixels */
        if (obj.style.display === "none") {
            /* on closing, restore top */
            if (help_node) {
                help_node.style.top = current_activity_top + "px";
            }
            current_activity_closed = true;
        } else {
            if (help_node) {
                help_node.style.top = current_activity_top +
                height_after_toggle + "px";
            }
            /* The div.clientHeight doesn't include the height
             of the images inside the div before the images are completely
             loaded. So we iterate through the
             image elements, as each image loads add the image
             height to the top of the current_activity div dynamically. */
            for (let i = 0; i < images.length; i++) {
                let image = images[i];
                image.onload = function ()
                {
                    if (help_node) {
                        help_node.style.top = getCssProperty(help_node,
                            'top') + this.height + 'px';
                    }
                };
            }
            current_activity_closed = false;
        }
    }
}
/*
 * Gets the Css property given an element and property name.
 * @param Object elm Element to get the Css property for.
 * @param String property Css property name.
 * @return String Css property value
 */
function getCssProperty(elm, property)
{
    /* always returns in px */
    return parseInt(window.getComputedStyle(elm, null)
        .getPropertyValue(property));
}
/*
 * The wiki markup the help panel shows is parsed by the two classes below,
 * MarkUpScanner and WikiParser, ported from the server's
 * src/library/MarkUpScanner.php and src/library/WikiParser.php so a help
 * page renders here the same way the server renders it. Only the mediawiki
 * engine is ported, which is all the help pages use. The parseWikiContent
 * function after them wraps the parser: it drops any head-variable section,
 * parses the body against the reading url for this group, then resolves the
 * cross-group markers and resource references the parser leaves behind into
 * links, the same way the wiki view assembles a read page.
 */
/* ---- small string helpers the parser leans on ---- */


/*
 * Whether MathJax has been asked for and not yet arrived, so a preview
 * full of math asks for it once.
 */

/*
 * getPageWithCallback does a GET HTTP call on the url passed.
 * Also fires the callback functions passed as
 * params appropriately.
 *
 * @param String url the url used for making GET HTTP call.
 * @param String response_type The response type expected.
 * @param Function object success_call_back Callback function on success.
 * @param Function object error_handler Callback function on failure.
 */
function getPageWithCallback(url, response_type, success_call_back,
    error_handler)
{
    let request = makeRequest();
    request.open('GET', url, true);
    request.onload = function ()
    {
        let status = request.status;
        if (status == 200) {
            success_call_back && success_call_back(
                JSON.parse(request.responseText));
        } else {
            error_handler && error_handler(status,
                JSON.parse(request.responseText));
        }
    };
    request.send();
};
/*
 * Takes in the help point id, uses it to fetch wiki content, then
 * wiki content is being eval'd to be painted int he help pane.
 * Ajax call happens only if help needs to be displayed.
 * @param Object help_point element
 * @param String is_mobile flag to check if the client is mobile
 * or not.
 * @param String target_controller Wiki page's controller name.
 * @param String csrf_token_key the dynamic name used for CSRF token.
 * @param String csrf_token_value The CSRF token to render edit page.
 * @param String help_group_id  help's group_id required to render resources.
 * @param String api_controller api's controller name.
 * @param String api_action api's action name.
 * @param String mode r/w mode , usually read.
 */
function displayHelpForId(help_point, is_mobile, target_controller,
    current_action, csrf_token_key, csrf_token_value, help_group_id,
    api_controller, api_action, mode)
{
    if ((elt("help-frame").style.display) === "inline-block") {
        toggleHelp('help-frame', is_mobile, target_controller);
        showHelpTop('help-frame');
        if (previous_help == help_point) {
            return;
        }
    }
    previous_help = help_point;
    let tl = eval('(' + help_point.getAttribute("data-tl") + ')');
    let back_params = eval('(' + help_point.getAttribute("data-back-params")
    + ')');
    let page_name = help_point.getAttribute("data-pagename");
    getPageWithCallback("?c=" + api_controller + "&group_id=" +
        help_group_id + "&" +
        "arg=" + mode + "&" +
        "a=" + api_action + "&" +
        csrf_token_key + '=' + csrf_token_value + "&" +
        "page_name=" + page_name,
        'json',
        function(data)
        {
            elt("help-frame-body").innerHTML = parseWikiContent(
                data.wiki_content,
                data.group_id,
                data.page_id,
                target_controller,
                csrf_token_key,
                csrf_token_value
            );
            console.log(data);
            if (!data.page_title) {
                data.page_title = page_name;
            }
            elt('page_name').innerHTML = data.page_view_title ||
            data.page_title || page_name;
            if (data.can_edit) {
                elt('page_name').innerHTML +=
                    " <div class='position-context'>"+
                    "<a class='media-anchor-button white'  href=\"" +
                    getEditLink(
                        target_controller,
                        current_action,
                        csrf_token_key,
                        csrf_token_value,
                        help_group_id,
                        data.page_title,
                        back_params) + '">' +
                    "<span role='img' aria-label='" +
                    tl["helpbutton_helper_edit"] +
                    "'>✏️</span></a>";
            }
            toggleHelp('help-frame', is_mobile, target_controller);
        showHelpTop('help-frame');
        },
        function(status, response)
        {
            if (status === 404 && response && response.can_edit) {
                elt('page_name').innerHTML = help_point
                    .getAttribute("data-pagename");
                elt('page_name').innerHTML +=
                    " <div class='position-context'>"+
                    "<a class='media-anchor-button white'  href=\"" +
                    getEditLink(
                        target_controller,
                        current_action,
                        csrf_token_key,
                        csrf_token_value,
                        help_group_id,
                        help_point.getAttribute("data-pagename"),
                        back_params) + '">' +
                        "<span role='img' aria-label='" +
                        tl["helpbutton_helper_edit"] +
                        "'>✏️</span></a>";
                elt("help-frame-body").innerHTML =
                    (tl["helpbutton_helper_page_no_exist"]).replace("%s", "'" +
                    help_point.getAttribute("data-pagename") + "'") +
                    tl["helpbutton_helper_create_edit"];
                toggleHelp('help-frame', is_mobile, target_controller);
        showHelpTop('help-frame');
            } else {
                doMessage("<h2 class='red'>" +
                tl["helpbutton_helper_not_available"] +
                "</h2>");
            }
        });
}
/*
 * Simple function to construct the Wiki Edit hyperlink with passed in params.
 * @param String target_controller Edit page's controller name.
 * @param String csrf_token_key the dynamic name used for CSRF token.
 * @param String csrf_token_value The CSRF token to render edit page.
 * @param String group_id GroupId of the group which has the wiki.
 * @param String page_name Page name,unique Identifier for wiki edit page.
 * @return String the edit link
 */
function getEditLink(target_controller, current_action, csrf_token_key,
    csrf_token_value, group_id, page_name, back_params)
{
    let edit_link = '?c=group' +
        '&' + csrf_token_key + '=' + csrf_token_value +
        '&group_id=' + group_id +
        '&arg=edit' +
        '&a=wiki' +
        '&page_name=' + page_name +
        '&back_params[open_help_page]=' + page_name +
        '&back_params[c]=' + target_controller +
        '&back_params[a]=' + current_action;
    for (let key in back_params) {
        let value = back_params[key];
        edit_link += "&back_params[" + key + "]=" + value;
    }
    return edit_link;
}
/**
 * Makes the help panel draggable by its title bar so a reader can move it
 * out of the way of the page beneath it. On the first drag the panel is
 * switched to fixed positioning at its current spot and then follows the
 * pointer, kept inside the browser window. A press that lands on a link or
 * button in the bar (such as the close control) is left alone so its own
 * click still works.
 *
 * @param String frame_id id of the help panel to move
 * @param String handle_id id of the title bar that acts as the drag grip
 */
function makeHelpDraggable(frame_id, handle_id)
{
    let frame = elt(frame_id);
    let handle = elt(handle_id);
    if (!frame || !handle) {
        return;
    }
    let offset_x = 0;
    let offset_y = 0;
    let dragging = false;
    listen(handle, "mousedown", function (event)
    {
        if (event.target.closest("a") || event.target.closest("button")) {
            return;
        }
        let rect = frame.getBoundingClientRect();
        frame.style.position = "fixed";
        frame.style.left = rect.left + "px";
        frame.style.top = rect.top + "px";
        frame.style.right = "auto";
        frame.style.margin = "0";
        offset_x = event.clientX - rect.left;
        offset_y = event.clientY - rect.top;
        dragging = true;
        event.preventDefault();
    });
    listen(document, "mousemove", function (event)
    {
        if (!dragging) {
            return;
        }
        let max_left = window.innerWidth - frame.offsetWidth;
        let max_top = window.innerHeight - frame.offsetHeight;
        let left = Math.max(0, Math.min(event.clientX - offset_x, max_left));
        let top = Math.max(0, Math.min(event.clientY - offset_y, max_top));
        frame.style.left = left + "px";
        frame.style.top = top + "px";
    });
    listen(document, "mouseup", function ()
    {
        dragging = false;
    });
}
listen(window, "DOMContentLoaded", function ()
{
    makeHelpDraggable("help-frame", "help-frame-head");
});
/*
 * Draws whatever the preview holds that needs more than markup: a chart
 * written as a resource becomes a drawing, which the chart script does.
 * Scripts written into a pane do not run by themselves, so the drawing is
 * asked for here instead.
 *
 * The chart script is loaded with the editor. Should it be missing, the
 * chart is left as an empty space rather than the call failing and taking
 * the rest of the preview with it.
 *
 * @param Object target the element the preview is drawn into
 */
function drawWikiPreviewExtras(target)
{
    let charts = target.querySelectorAll("[data-chart]");
    if (charts.length == 0) {
        return;
    }
    if (typeof Chart !== "function") {
        return;
    }
    for (let index = 0; index < charts.length; index++) {
        let holder = charts[index];
        if (attr(holder, "data-drawn") === "yes") {
            continue;
        }
        holder.setAttribute("data-drawn", "yes");
        /* A chart that will not draw is left as an empty space rather
           than allowed to stop the rest of the preview being shown. */
        try {
                let described = JSON.parse(attr(holder, "data-chart"));
            /* Making a chart sets it up; drawing it is a second step, and
               without it only the title appears. */
            let drawing = new Chart(holder.id, described.points,
                described.properties);
            drawing.draw();
        } catch (problem) {
            holder.setAttribute("data-drawn", "no");
        }
    }
}
/*
 * Sets any math the preview holds. Math is written between backticks, the
 * same as it reaches a saved page, and MathJax reads it from there.
 *
 * MathJax is loaded with the editor the way every other page that shows
 * math loads it, so nothing is fetched here; this only asks it to set
 * what has just been drawn. Anything that goes wrong leaves the words of
 * the page on screen as they were.
 *
 * @param Object target the element the preview is drawn into
 */
function setWikiPreviewMath(target)
{
    if (typeof MathJax !== "object" || !MathJax.typesetPromise) {
        return;
    }
    if (target.textContent.indexOf("`") < 0) {
        return;
    }
    MathJax.typesetPromise([target]);
}
X