/**
* 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 (edited Chris Pollett)
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2014-2021
* @filesource
*/
/*
* This script adds buttons to textareas on a page which when clicked insert
* the relevant way to format something using wiki syntax.
*
* The editor automatically renders the editor buttons using the following list
* of button names for configuration. These can be set on the data-buttons
* attribute of the textarea
* {
* "wikibtn-bold"
* "wikibtn-italic"
* "wikibtn-underline"
*" wikibtn-strike"
* "wikibtn-nowiki"
* "wikibtn-hyperlink"
* "wikibtn-bullets"
* "wikibtn-numbers"
* "wikibtn-hr"
* "wikibtn-heading"
* "wikibtn-search"
* "wikibtn-table"
* "wikibtn-slide"
* "wikibtn-definitionlist"
* }
*/
/**
* Used to contain formatting info about all buttons on the wiki editor
* @var Array
*/
var editor_all_buttons = [];
/**
* Used to store formatting info buttons on a particular textarea
* @var Array
*/
var editor_buttons = [];
/**
* Object that buffers selection information.
* @var Object
*/
var editor_buffer = {};
/**
* Object containing all icons for editor toolbar
* @var Object
*/
var editor_toolbar_icons = {
"wikibtn-bold": "<b>B</b>",
"wikibtn-italic": "<i>I</i>",
"wikibtn-underline": "<u>U</u>",
"wikibtn-strike": "<s>S</s>",
"wikibtn-nowiki": `<div class='wiki-icon-nowiki'>W</div>`,
"wikibtn-hyperlink": "🔗",
"wikibtn-bullets": `<div class="wikibtn-icon-bullets"
><div>⋮</div></div>≡`,
"wikibtn-numbers": `<div class='wikibtn-icon-numbers'
><div>1</div><div>2</div><div>3</div></div>≡`,
"wikibtn-hr": "⎯",
"wikibtn-heading": "H",
"wikibtn-search-widget": "🔎",
"wikibtn-more-syntax": "…",
"wikibtn-table": "⊞",
"wikibtn-slide": "🧑🏫",
"wikibtn-definitionlist": "≝",
"wikibtn-leftaligned": `<div class="wikibtn-icon-aligned
wikibtn-icon-leftaligned"><span></span><span></span><span
></span><span></span></div>`,
"wikibtn-centeraligned": `<div class="wikibtn-icon-aligned
wikibtn-icon-centeraligned"><span></span><span></span><span
></span><span></span></div>`,
"wikibtn-rightaligned": `<div class="wikibtn-icon-aligned
wikibtn-icon-rightaligned"><span></span><span></span><span
></span><span></span></div>`,
};
/*
* Add buttons for common wiki operation before the textareas on a page.
*/
function editorizeAll()
{
var text_areas = tag("textarea");
var len = text_areas.length;
var ids = new Array();
for (i = 0; i < len; i++)
{
if (text_areas[i].id){
editorize(text_areas[i].id);
}
}
}
/**
* Adds Wiki editor buttons to a textarea with the given id
*
* @param String id identifier of the text area to have wiki buttons added to
*/
function editorize(id)
{
/*
check if the editor div exists.
*/
var node = elt(id);
if (node === null) {
/*
If the div to render wiki_editor is not found, do nothing.
*/
alert("No textarea found with id = " + id);
return false;
}else{
node.addEventListener("focus", function(event)
{
enableKBShortcuts(id);
}, true);
}
var button_string = "";
/*
Initialize tool bar wiki_buttons object
*/
editor_buttons[id] = {};
/*
filter the buttons according to the data-buttons
attribute.
*/
filterButtons(id);
/*
Buttons are rendered below.
*/
for (var prop in editor_buttons[id]) {
var no_buttons = ['wikibtn-search', 'wikibtn-table', 'wikibtn-heading'];
if (editor_buttons[id].hasOwnProperty(prop)
&& (no_buttons.indexOf(prop) === -1)) {
translate_prop = tl[prop.replace(/wikibtn-/, 'wiki_js_')];
button_string +=
'<div class="' + prop +' icon-button-container">' +
'<a class="icon-anchor-button">' +
'<span role="img" ' +
'aria-label="' + translate_prop +
'" onclick="wikifySelection(\'' +prop + '\', \''+ id +'\');">'+
editor_toolbar_icons[prop] +
'</span></a></div>';
}
}
/*
Render the wiki-popup-prompt div used to prompt
user input.
*/
var editor_toolbar = '<div id="wiki-popup-prompt-' + id +
'" class="wiki-popup-prompt" style="display: none;"></div>' +
'<div class="wiki-buttons">' + button_string;
/*
check if heading was desired and render if was.
*/
if (editor_buttons[id].hasOwnProperty('wikibtn-heading')) {
translate_prop = tl['wiki_js_heading'];
editor_toolbar += '<select id="wiki-heading-' +id +
'" title="' + translate_prop +
'" value="heading" ' +
'onchange="addWikiHeading(\''+ id +'\');">' +
'<option selected="" disabled="">'+
tl['wiki_js_heading'] + '</option>' +
'<option value="1">H1</option>' +
'<option value="2">H2</option>' +
'<option value="3">H3</option>' +
'<option value="4">H4</option>' +
'</select>';
}
/*
check if table was desired and render if was.
*/
if (editor_buttons[id].hasOwnProperty('wikibtn-table')) {
translate_prop = tl['wiki_js_add_wiki_table'];
editor_toolbar += '<div class="wikibtn-table icon-button-container">' +
'<a class="icon-anchor-button">' +
'<span role = "img" ' +
'aria-label = "' +translate_prop +
'" onclick = "addWikiTable(\''+ id +'\');">'+
editor_toolbar_icons["wikibtn-table"] +
'</span></a></div>';
}
/*
check if search was desired and render if was.
*/
if (editor_buttons[id].hasOwnProperty('wikibtn-search')) {
translate_prop = tl['wiki_js_add_search'];
editor_toolbar += '<div class="wikibtn-search-widget ' +
'icon-button-container">' +
'<a class="icon-anchor-button">' +
'<span role="img" aria-label="' + translate_prop +
'" onclick = "addWikiSearch(\''+ id +'\');">'+
editor_toolbar_icons["wikibtn-search-widget"] +
'</span></a></div>';
}
/*
The marks above cover the markup a writer reaches for most. The rest
of it is offered from a list, so that everything the parser reads can
be put in without remembering how it is spelt.
*/
if (editor_buttons[id].hasOwnProperty('wikibtn-search')) {
translate_prop = tl['wiki_js_more_syntax'];
editor_toolbar += '<div class="wikibtn-more-syntax ' +
'icon-button-container" id="more-syntax-holder-' + id + '">' +
'<a class="icon-anchor-button">' +
'<span role="img" aria-label="' + translate_prop +
'" aria-haspopup="true" aria-expanded="false" ' +
'id="more-syntax-mark-' + id + '"' +
' onclick = "addWikiMoreSyntax(\''+ id +'\');">'+
editor_toolbar_icons["wikibtn-more-syntax"] +
'</span></a></div>';
}
/*
A page that can be saved is offered saving on its own. A field with
no buttons of its own is a form to be filled in and sent, such as a
page's settings, so the keeping is not offered there and the save
button stays where it is.
*/
if (elt("save-container") && elt(id) && elt(id).form &&
((editor_buttons[id] &&
Object.keys(editor_buttons[id]).length > 0) ||
(elt("resource-save-controls") && id == "wiki-page"))) {
editor_toolbar += '<div class="wikibtn-autosave ' +
'icon-button-container">' +
'<a class="icon-anchor-button">' +
'<button type="button" id="autosave-button-' + id + '" ' +
'class="autosave-on" aria-label="' + tl['wiki_js_autosave'] +
'" title="' + tl['wiki_js_autosave'] + '" ' +
'onclick="toggleWikiAutoSave(\'' + id + '\');">' +
WIKI_AUTOSAVE_ICON + '</button></a></div>' +
'<span id="autosave-saying-' + id +
'" class="autosave-saying"></span>';
}
editor_toolbar += '</div>';
/*
Insert the toolbar div before the textarea
*/
node.insertAdjacentHTML("beforebegin", editor_toolbar);
/* Saving by hand belongs beside the control that says whether it is
needed, rather than at the foot of a long page where a writer has to
go looking for it. It is moved rather than drawn again so the form
keeps one submit button. */
var save_by_hand = elt("wiki-save-button");
var keeping = elt("autosave-button-" + id);
if (save_by_hand && keeping && keeping.parentNode &&
keeping.parentNode.parentNode) {
keeping.parentNode.parentNode.appendChild(save_by_hand);
}
/* A file kept with a page has no row of marks to stand in, so its two
controls go beside its name at the top instead. */
var beside_name = elt("resource-save-controls");
if (beside_name && id == "wiki-page" && keeping && keeping.parentNode &&
keeping.parentNode.parentNode) {
beside_name.appendChild(keeping.parentNode.parentNode);
/* The word that says a save is happening goes with them, so that
it appears beside the control on every screen rather than being
left behind where the marks used to be. */
var saying = elt("autosave-saying-" + id);
if (saying) {
beside_name.appendChild(saying);
}
}
startWikiAutoSave(id);
return;
}
/*
* How long the writer must leave the page alone before it is saved for
* them, in milliseconds. Long enough that a save does not land in the
* middle of a sentence, short enough that little is ever at risk.
*/
var WIKI_AUTOSAVE_WAIT = 5000;
/*
* The two arrows going round that stand for a page keeping itself. They
* are drawn rather than written so the control sits in the toolbar the
* size the other buttons are, and the line across them is drawn here too
* and shown only while the keeping is off.
*/
var WIKI_AUTOSAVE_ICON = '<svg viewBox="0 0 24 24" width="100%" ' +
'height="100%" fill="none" stroke="currentColor" ' +
'stroke-width="2.4" stroke-linecap="butt" ' +
'xmlns="http://www.w3.org/2000/svg">' +
'<path d="M3.6 12A8.4 8.4 0 0 1 18 6.1"/>' +
'<polygon points="21.4 2.2 21.4 9.4 14.2 9.4" ' +
'fill="currentColor" stroke="none"/>' +
'<path d="M20.4 12A8.4 8.4 0 0 1 6 17.9"/>' +
'<polygon points="2.6 21.8 2.6 14.6 9.8 14.6" ' +
'fill="currentColor" stroke="none"/>' +
'<line class="autosave-bar" x1="0.9" y1="0.9" x2="23.1" ' +
'y2="23.1" stroke-width="1.6"/></svg>';
/*
* How long the word that a save is happening stays on screen, in
* milliseconds. A save of a page this size is over in a moment, so
* without a floor the word would come and go unread and a writer would
* never know their work had been kept.
*/
var WIKI_AUTOSAVE_SHOWN = 1200;
/*
* The timer counting down to a save, kept so a further change can push
* it back rather than let two saves run.
*/
var wiki_autosave_waiting = null;
/*
* Whether the page is being saved for the writer without their asking.
* It starts on, so a page is kept without anyone having to remember.
*/
var wiki_autosave_on = true;
/*
* The name under which the writer's choice is remembered for as long as
* they are working, so turning it off stays off from one page to the
* next rather than coming back on at every save.
*/
var WIKI_AUTOSAVE_CHOICE = "wiki-autosave";
/*
* Which field's control is the one on screen. A page can hold more than
* one editor, and each gets a control of its own, so the one belonging
* to the field being written in is the one to speak through.
*/
var wiki_autosave_field = "";
/*
* What the mark guarding a save is called in a form. The page carries
* it, so it is read from there rather than written in twice.
*/
var wiki_token_name = "YIOOP_TOKEN";
/*
* Takes the mark the site sends back and puts it in the form in place
* of the one that was there. The mark carries the time the page was
* fetched, and the site refuses a save whose mark is older than the
* page's last change. Without this, the first save of a sitting would
* land and every one after it would be turned away as though someone
* else had been editing.
*
* @param Object form the form the page is saved through
* @param String page what the site sent back, holding a fresh mark
*/
function takeFreshWikiToken(form, page)
{
if (!form || !page) {
return;
}
var field = form.elements[wiki_token_name];
if (!field) {
return;
}
var pattern = new RegExp('name=["\']' + wiki_token_name +
'["\'][^>]*value=["\']([^"\']+)["\']');
var found = page.match(pattern);
if (!found) {
pattern = new RegExp('value=["\']([^"\']+)["\'][^>]*name=["\']' +
wiki_token_name + '["\']');
found = page.match(pattern);
}
if (found) {
field.value = found[1];
}
}
/*
* Writes down where the writer was in the page, so that when the page
* comes back after a save it comes back at the same place with the
* cursor where they left it. The form used to do this itself as it was
* submitted, which left anything sent another way, such as a file added
* to the page, coming back at the top.
*/
function noteWikiPlace()
{
var caret_at = elt("caret-pos");
var scrolled_to = elt("scroll-top");
var field = elt("wiki-page");
if (!caret_at || !scrolled_to || !field) {
return;
}
caret_at.value = field.selectionStart ? field.selectionStart : 0;
scrolled_to.value = field.scrollTop ? field.scrollTop : 0;
}
/*
* Takes the page's own id from what the site sends back and puts it on
* the field. A page written for the first time has no id until it is
* saved, and a page that saves itself never reloads, so without this the
* preview went on asking the site for a file belonging to page nothing
* and drew a picture that could not be found.
*
* @param Object field the text area holding the page's source
* @param String page what the site sent back
*/
function takeFreshWikiPageId(field, page)
{
if (!field || !page || field.getAttribute("data-page-id")) {
return;
}
var found = page.match(/data-page-id=['"](\d+)['"]/);
if (found && found[1] !== "0") {
field.setAttribute("data-page-id", found[1]);
}
}
/*
* Which parts of a page's settings each kind of page uses. A kind not
* named here is treated as a standard page. Anything not listed for the
* kind chosen is put away, so a writer sees only what applies: an alias
* page has nothing but the page it stands for, a shortened address has
* no icon, and a front page has no resource path.
*/
var PAGE_TYPE_SHOWS = {
"standard": ["non-alias-type", "page-border-setting",
"page-theme-setting", "page-toc-setting", "public-source-setting",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "alt-path-setting",
"page-header-setting", "page-footer-setting"],
"page_and_feedback": ["non-alias-type", "page-border-setting",
"page-theme-setting", "page-toc-setting", "public-source-setting",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "alt-path-setting",
"page-header-setting", "page-footer-setting"],
"page_alias": ["alias-type"],
"media_list": ["non-alias-type", "page-border-setting",
"page-theme-setting", "static-html-folder-setting",
"public-source-setting", "page-title-setting",
"meta-author-setting", "page-icon-setting", "meta-robots-setting",
"meta-description-setting", "meta-properties-setting",
"alt-path-setting", "update-description-setting",
"page-header-setting", "page-footer-setting"],
"presentation": ["non-alias-type", "page-border-setting",
"page-theme-setting", "public-source-setting",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "alt-path-setting"],
"url_shortener": ["non-alias-type", "shortener-container",
"short-url-label",
"public-source-setting", "page-title-setting",
"meta-author-setting", "meta-robots-setting",
"meta-description-setting"],
"share": ["share-container", "non-alias-type", "page-border-setting",
"page-theme-setting", "public-source-setting",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "alt-path-setting",
"page-header-setting", "page-footer-setting"],
"git_repository": ["non-alias-type", "page-border-setting",
"page-theme-setting", "public-source-setting",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "alt-path-setting",
"page-header-setting", "page-footer-setting"],
"news_article": ["non-alias-type", "page-border-setting",
"page-theme-setting", "page-toc-setting", "public-source-setting",
"meta-robots-setting", "page-header-setting",
"page-footer-setting", "page-icon-setting", "page-title-setting",
"meta-author-setting", "meta-description-setting",
"article-categories-setting"],
"front_page": ["non-alias-type", "front-page-settings",
"page-title-setting", "meta-author-setting", "page-icon-setting",
"meta-robots-setting", "meta-description-setting",
"meta-properties-setting", "page-header-setting",
"page-footer-setting"]
};
/*
* Shows the controls a front page needs and hides the ones it does not,
* as soon as the kind of page is chosen rather than after a save.
*/
/*
* Where each control that an article moves lives when the page is not an
* article, so it can be put back. The place is remembered the first time
* a control is moved, since after that its neighbors have changed.
*/
var ARTICLE_MOVED_HOME = {};
/*
* The controls an article shows above its writing area, in the order a
* writer meets them: the thumbnail, the title, the author, then the
* abstract.
*/
var ARTICLE_MOVES = ["page-icon-setting", "page-title-setting",
"meta-author-setting", "meta-description-setting"];
/*
* Which part of the article block each control is put into, so the title
* and the author stand together beside the thumbnail rather than each
* taking a line of its own.
*/
var ARTICLE_AREAS = {
"page-icon-setting": "article-header-thumb",
"page-title-setting": "article-header-fields",
"meta-author-setting": "article-header-fields",
"meta-description-setting": "article-header-fields"
};
/*
* Reads the label a control should carry, keeping the one it arrived
* with so it can be given back when the page stops being an article.
*
* @param Element part the control being relabeled
* @param String wanted what the label should say now, or "" for the one
* it arrived with
*/
function setArticleLabel(part, wanted)
{
var label = part ? part.getElementsByTagName("label")[0] : null;
var bold = label ? label.getElementsByTagName("b")[0] : null;
if (!bold) {
return;
}
if (bold.getAttribute("data-first-label") === null) {
bold.setAttribute("data-first-label", bold.textContent);
}
bold.textContent = (wanted === "") ?
bold.getAttribute("data-first-label") : wanted;
}
/*
* Puts the thumbnail, title, author and abstract above the writing area
* when an article is being written, and back in the settings panel when
* it is not. The controls are moved rather than drawn a second time, so
* the form never holds two fields of one name; a control put away with a
* style still sends its value, and two would disagree.
*
* @param Boolean is_article whether the kind chosen is a news article
*/
function placeArticleHeader(is_article)
{
var header = elt("article-header");
for (var index = 0; index < ARTICLE_MOVES.length; index++) {
var name = ARTICLE_MOVES[index];
var part = elt(name);
if (!part) {
continue;
}
if (!header) {
/* A screen showing the settings on their own has nowhere to
put these, and an article does not want them there, so
they are put away rather than moved. */
part.style.display = is_article ? "none" : "";
continue;
}
if (ARTICLE_MOVED_HOME[name] === undefined) {
ARTICLE_MOVED_HOME[name] = {"parent": part.parentNode,
"before": part.nextSibling};
}
if (is_article) {
elt(ARTICLE_AREAS[name]).appendChild(part);
} else {
var home = ARTICLE_MOVED_HOME[name];
home.parent.insertBefore(part, home.before);
}
}
if (!header) {
return;
}
setArticleLabel(elt("page-icon-setting"),
is_article ? header.getAttribute("data-thumbnail-label") : "");
setArticleLabel(elt("meta-description-setting"),
is_article ? header.getAttribute("data-abstract-label") : "");
}
/*
* Rebuilds the one line with commas between that the hidden field carries
* for an article's categories, from the rows now standing in the list.
*/
function syncArticleCategories()
{
var hidden = elt("article-categories");
if (!hidden) {
return;
}
var texts = sel("#article-category-list .tag-text");
var names = [];
for (var index = 0; index < texts.length; index++) {
names.push(texts[index].textContent);
}
hidden.value = names.join(",");
}
/*
* Wires the picker that files an article. Choosing a name from the
* group's own list puts a row in; the mark on a row takes it out. Either
* way the hidden field is rebuilt, and that field is what the save reads.
* Removal is listened for on the list rather than on each mark, so a row
* added after the page loaded behaves like one that arrived with it.
*/
function initArticleCategories()
{
var add_button = elt("article-category-add-button");
var chooser = elt("article-category-add");
var list = elt("article-category-list");
if (!add_button || !chooser || !list) {
return;
}
var remove_label = add_button.getAttribute("data-remove-label") || "";
listen(add_button, "click", function () {
var name = chooser.value;
if (!name) {
return;
}
var texts = list.querySelectorAll(".tag-text");
for (var index = 0; index < texts.length; index++) {
if (texts[index].textContent === name) {
chooser.value = "";
return;
}
}
var row = ce("li");
row.className = "tag-item";
var text = ce("span");
text.className = "tag-text";
text.textContent = name;
var remove = ce("button");
remove.type = "button";
remove.className = "tag-remove article-category-remove";
remove.title = remove_label;
remove.innerHTML = "×";
row.appendChild(text);
row.appendChild(remove);
list.appendChild(row);
chooser.value = "";
syncArticleCategories();
});
listen(list, "click", function (event) {
var pressed = event.target;
if (!pressed ||
!pressed.classList.contains("article-category-remove")) {
return;
}
var row = pressed.parentNode;
if (row && row.parentNode) {
row.parentNode.removeChild(row);
syncArticleCategories();
}
});
}
function showFrontPageSettings()
{
var chooser = elt("page-type");
if (!chooser) {
return;
}
/* Only the front page's own block wants that block to be there. The
showing and hiding belongs to every kind of page, so requiring it
here left every screen without one showing every control. */
var shows = PAGE_TYPE_SHOWS[chooser.value];
if (shows === undefined) {
shows = PAGE_TYPE_SHOWS["standard"];
}
/* Every named part of the settings is put away, then the ones this
kind of page uses are brought back, so the form always shows what
belongs to what is being made. */
for (var name in PAGE_TYPE_SHOWS) {
var list = PAGE_TYPE_SHOWS[name];
for (var index = 0; index < list.length; index++) {
var part = elt(list[index]);
if (part) {
part.style.display = "none";
}
}
}
for (var wanted = 0; wanted < shows.length; wanted++) {
var showing = elt(shows[wanted]);
if (showing) {
showing.style.display = "";
}
}
/* The article's own controls are moved after the showing and hiding,
since moving one first would leave it hidden in its new place. */
placeArticleHeader(chooser.value == "news_article");
showFrontPageSlots();
}
/*
* Shows the places belonging to the shape a writer has chosen and puts
* the others away, so the fields on screen always match the picture
* beside them.
*/
function showFrontPageSlots()
{
var picked = document.querySelector(
"input[name='front_page_layout']:checked");
var sets = document.querySelectorAll(".front-page-slots");
for (var index = 0; index < sets.length; index++) {
var set = sets[index];
set.style.display = (picked &&
set.getAttribute("data-layout") === picked.value) ? "" : "none";
}
}
/*
* Sets each shape to show its own places as it is chosen.
*/
function watchFrontPageLayout()
{
var buttons = document.querySelectorAll(
"input[name='front_page_layout']");
for (var index = 0; index < buttons.length; index++) {
listen(buttons[index], "change", showFrontPageSlots);
}
var kinds = document.querySelectorAll(".front-page-kind");
for (var which = 0; which < kinds.length; which++) {
listen(kinds[which], "change", function ()
{
showFrontPageSlotKind(this.getAttribute("data-slot"));
});
showFrontPageSlotKind(kinds[which].getAttribute("data-slot"));
}
}
/*
* Shows the control a place needs for what it holds: a page to name
* where it holds one article, a category to pick where it holds a whole
* category of them.
*
* @param String slot which place within its layout
*/
function showFrontPageSlotKind(slot)
{
var chooser = elt("front-kind-" + slot);
var page = elt("front-slot-" + slot);
var category = elt("front-category-" + slot);
if (!chooser) {
return;
}
var wants_page = (chooser.value === "article");
if (page) {
page.style.display = wants_page ? "" : "none";
}
if (category) {
category.style.display = wants_page ? "none" : "";
}
}
/*
* Sets the settings form to follow the kind of page chosen, so the
* controls on show match what is being made.
*/
function watchPageTypeChoice()
{
var chooser = elt("page-type");
if (!chooser) {
return;
}
listen(chooser, "change", showFrontPageSettings);
watchFrontPageLayout();
initArticleCategories();
showFrontPageSettings();
}
/*
* Says whether the page is keeping itself, so work elsewhere can tell
* whether there will be a save to carry something along with.
*
* @return bool true when the page is being saved without asking
*/
function wikiAutoSaveIsOn()
{
return wiki_autosave_on;
}
/*
* Gives back the control belonging to one editor. A page can hold more
* than one, so each is named after the field it saves rather than all
* sharing one name, which left two of the three without a place on the
* page at all.
*
* @param String id identifier of the text area holding the page
* @return Object the control, or nothing where there is none
*/
function wikiAutoSaveButton(id)
{
return elt("autosave-button-" + id);
}
/*
* Gives back what the writer last chose about the page saving itself,
* which is remembered for as long as they are working so the choice is
* not undone by a save or by moving to another page.
*
* @return String "off" when they turned it off, otherwise an empty
* answer, which leaves it on
*/
function rememberedWikiAutoSave()
{
try {
return window.sessionStorage.getItem(WIKI_AUTOSAVE_CHOICE) || "";
} catch (problem) {
return "";
}
}
/*
* Remembers what the writer chose about the page saving itself. A
* browser that will not remember is no reason to refuse the choice, so
* nothing is made of being turned away.
*
* @param String choice "on" or "off"
*/
function rememberWikiAutoSave(choice)
{
try {
window.sessionStorage.setItem(WIKI_AUTOSAVE_CHOICE, choice);
} catch (problem) {
return;
}
}
/*
* Sets the page up to save itself: the controls a writer would have used
* to save by hand are put away while it does, and every change to the
* source starts the wait over.
*
* @param String id identifier of the text area holding the page
*/
function startWikiAutoSave(id)
{
var field = elt(id);
if (!field || !wikiAutoSaveButton(id)) {
return;
}
wiki_autosave_field = id;
wiki_autosave_on = (rememberedWikiAutoSave() !== "off");
showWikiSaveControls();
field.addEventListener("input", function ()
{
wikiChangeMade(id);
});
}
/*
* Offers keeping and saving for a file shown as a grid rather than as
* text. Such a file has no writing area to hang them on, but the grid
* keeps what it holds in a hidden field the form sends, so that field is
* watched in place of one. The two controls stand beside the file's name,
* where a writing area's would stand beside its marks.
*/
function initSpreadsheetKeeping()
{
var beside_name = elt("resource-save-controls");
var field = elt("spreadsheet-data");
if (!beside_name || !field || !field.form) {
return;
}
var id = field.id;
var says = tl['wiki_js_autosave'];
beside_name.innerHTML = '<div class="wikibtn-autosave ' +
'icon-button-container"><a class="icon-anchor-button">' +
'<button type="button" id="autosave-button-' + id + '" ' +
'class="autosave-on" aria-label="' + says + '" title="' + says +
'" onclick="toggleWikiAutoSave(\'' + id + '\');">' +
WIKI_AUTOSAVE_ICON + '</button></a></div>' +
'<span id="autosave-saying-' + id + '" class="autosave-saying">' +
'</span>';
var save_by_hand = elt("wiki-save-button");
var keeping = elt("autosave-button-" + id);
if (save_by_hand && keeping) {
keeping.parentNode.parentNode.appendChild(save_by_hand);
}
startWikiAutoSave(id);
}
/*
* Turns saving on its own on or off, and says which it is: the button
* stands green while the page is being kept, and red while it is not.
*
* @param String id identifier of the text area holding the page
*/
function toggleWikiAutoSave(id)
{
wiki_autosave_on = !wiki_autosave_on;
rememberWikiAutoSave(wiki_autosave_on ? "on" : "off");
if (!wiki_autosave_on && wiki_autosave_waiting !== null) {
clearTimeout(wiki_autosave_waiting);
wiki_autosave_waiting = null;
}
showWikiSaveControls();
if (wiki_autosave_on) {
wikiChangeMade(id);
}
}
/*
* Puts the button in the state saving is in, and shows or hides the
* controls for saving by hand. While the page keeps itself there is
* nothing for a writer to press, so the reason for an edit, the save
* button and the way to see the page are left out.
*/
function showWikiSaveControls()
{
var button = wikiAutoSaveButton(wiki_autosave_field);
if (!button) {
return;
}
button.className = wiki_autosave_on ? "autosave-on" : "autosave-off";
var says = wiki_autosave_on ? tl['wiki_js_autosave'] :
tl['wiki_js_autosave_off'];
button.setAttribute("aria-label", says);
button.setAttribute("title", says);
var saying = elt("autosave-saying-" + wiki_autosave_field);
if (saying) {
saying.textContent = "";
}
var by_hand = [elt("edit-reason-container"), elt("save-container"),
elt("wiki-save-button")];
for (var index = 0; index < by_hand.length; index++) {
if (by_hand[index]) {
by_hand[index].style.display = wiki_autosave_on ? "none" : "";
}
}
}
/*
* Says a change has been made, which is what makes a save possible. The
* wait starts over on every change, so the page is kept once the writer
* has stopped rather than in the middle of a sentence.
*
* @param String id identifier of the text area holding the page
*/
function wikiChangeMade(id)
{
if (!wiki_autosave_on) {
return;
}
if (wiki_autosave_waiting !== null) {
clearTimeout(wiki_autosave_waiting);
}
wiki_autosave_waiting = setTimeout(function ()
{
wiki_autosave_waiting = null;
saveWikiPageQuietly(id);
}, WIKI_AUTOSAVE_WAIT);
}
/*
* Saves the page where the writer cannot see it happening, saying so on
* the button while it goes. Nothing further is possible to save until
* another change is made.
*
* @param String id identifier of the text area holding the page
*/
function saveWikiPageQuietly(id)
{
var field = elt(id);
var button = wikiAutoSaveButton(id);
if (!field || !field.form) {
return;
}
var began = Date.now();
var saying = elt("autosave-saying-" + id);
if (saying) {
saying.textContent = tl['wiki_js_saving'];
}
if (button) {
button.setAttribute("aria-label", tl['wiki_js_saving']);
}
var settled = function ()
{
var left = WIKI_AUTOSAVE_SHOWN - (Date.now() - began);
setTimeout(showWikiSaveControls, (left > 0) ? left : 0);
};
noteWikiPlace();
try {
var sending = new FormData(field.form);
/* A save made this way has no one to ask why, so it says so
itself and the history reads as it should. */
sending.set("edit_reason", tl['wiki_js_autosave_reason']);
fetch(field.form.action || window.location.href, {method: "POST",
body: sending, credentials: "same-origin"})
.then(function (answer)
{
return answer.text();
})
.then(function (page)
{
takeFreshWikiToken(field.form, page);
takeFreshWikiPageId(field, page);
settled();
})
.catch(settled);
} catch (problem) {
settled();
}
}
/**
* Method to return standard buttons as an object based on the current
* render engine (mediawiki, markdown, etc).
*
* @return Object containing button configurations with their markup syntax
*/
function getStandardButtonsObject()
{
if (tl?.wiki_js_render_engine === "markdown") {
return {
'wikibtn-bold': ['**', '**'],
'wikibtn-italic': ['*', '*'],
'wikibtn-strike': ['~~', '~~'],
'wikibtn-nowiki': ['```', '```'],
'wikibtn-hyperlink': ['[', '](url)'],
'wikibtn-bullets': ['* ' + tl['wiki_js_bullet'] + ' \n'],
'wikibtn-numbers': ['1. ' + tl['wiki_js_enum'] + '. \n'],
'wikibtn-hr': ['---\n'],
'wikibtn-heading': '', // Handled separately
'wikibtn-table': '',
'wikibtn-slide': ['=' + tl['wiki_js_slide_sample_title'] + '=\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '....' + '\n'],
'wikibtn-definitionlist': ['**Term**: Definition\n'],
};
}
return {
'wikibtn-bold': ['---', '---'],
'wikibtn-italic': ['--', '--'],
'wikibtn-underline': ['<u>', '</u>'],
'wikibtn-strike': ['<s>', '</s>'],
'wikibtn-nowiki': ['<nowiki>', '</nowiki>'],
'wikibtn-hyperlink': ['[[', ']]'],
'wikibtn-slide': ['=' + tl['wiki_js_slide_sample_title'] + '=\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '* ' + tl['wiki_js_slide_sample_bullet'] + '\n'
+ '....' + '\n'],
'wikibtn-bullets': ['* ' + tl['wiki_js_bullet'] + ' \n'],
'wikibtn-numbers': ['# ' + tl['wiki_js_enum'] + ' \n'],
'wikibtn-definitionlist': [
'; ' + tl['wiki_js_definitionlist_item']
+ ' : ' + tl['wiki_js_definitionlist_definition']
+ '' + '\n'
+ '; ' + tl['wiki_js_definitionlist_item']
+ ' : ' + tl['wiki_js_definitionlist_definition']
+ '' + '\n'],
'wikibtn-leftaligned': ['{{left|','}}'],
'wikibtn-centeraligned': ['{{center|','}}'],
'wikibtn-rightaligned': ['{{right|','}}'],
'wikibtn-hr': ['---- \n']
};
}
/**
* Filters the buttons according to exclusion/inclusion rules.
* If the 'data-buttons' attribute value starts with 'all' - Only the buttons
* need to be excluded should be followed, with their name specified with a '!'
* prefix.
* Any buttons with no '!' prefix will eb ignored
* example : all,!bol,!italic will render editor with all buttns excluding bold
* and italic
*
* If the 'data-buttons' attribute value does not starts with 'all', any button
* names followed will only be included in the editor.
* Any buttons with '!' prefix will be ignored.
* example : bol,italic will render editor only with bold and italic buttons.
*
* @param String id identifier of the textarea that we want editor buttons on
*/
function filterButtons(id)
{
editor_all_buttons[id] = getStandardButtonsObject();
editor_all_buttons[id]['wikibtn-heading'] = '';
editor_all_buttons[id]['wikibtn-search'] = '';
editor_all_buttons[id]['wikibtn-table'] = '';
var wiki_text_div = elt(id);
var wiki_buttons = wiki_text_div.getAttribute("data-buttons");
if (wiki_buttons) {
var wiki_buttons_array = wiki_buttons.split(',');
var buttons_array_length = wiki_buttons_array.length;
var included_buttons = new Array();
var excluded_buttons = new Array();
var exc = false;
if (wiki_buttons_array[0].trim() === 'all') {
exc = true;
}
if (wiki_buttons_array[0].trim() === 'none') {
editor_buttons[id] = [];
return;
}
for (var i = 0; i < buttons_array_length; i++) {
wiki_buttons_array[i] = wiki_buttons_array[i].trim();
var firstChar = wiki_buttons_array[i].charAt(0);
if (wiki_buttons_array[i] && (exc === true) && firstChar === '!') {
wiki_buttons_array[i] = wiki_buttons_array[i].substr(1);
if (editor_all_buttons[id].hasOwnProperty(
wiki_buttons_array[i])) {
excluded_buttons.push(wiki_buttons_array[i]);
delete editor_all_buttons[id][
wiki_buttons_array[i]];
}
} else if (wiki_buttons_array[i] && exc === false) {
if (editor_all_buttons[id].hasOwnProperty(
wiki_buttons_array[i])) {
included_buttons.push(wiki_buttons_array[i]);
editor_buttons[id][wiki_buttons_array[i]] =
editor_all_buttons[id][wiki_buttons_array[i]];
}
}
}
} else {
editor_buttons[id] = editor_all_buttons[id];
}
if (Object.keys(editor_buttons[id]).length === 0) {
editor_buttons[id] = editor_all_buttons[id];
}
}
/**
* This function returns the selected text from text_area.
* The returned Object has properties for
* selected text, entire prefix and entire suffix strings to the
* selected text.
*
* @param String id the identifier for the textarea to get the selected text for
* @return Object with properties of the selected text set
*/
function getSelection(id)
{
/*
Select the DOM element
*/
var text_area = elt(id);
/*
IE?
*/
if (document.selection) {
var selection_bookmark =
document.selection.createRange().getBookmark();
var sel = text_area.createTextRange();
sel.moveToBookmark(selection_bookmark);
var sleft = text_area.createTextRange();
sleft.collapse(true);
sleft.setEndPoint("EndToStart", sel);
text_area.selectionStart = sleft.text.length;
text_area.selectionEnd = sleft.text.length + sel.text.length;
text_area.selectedText = sel.text;
var selected_text_prefix = text_area.value.substring(0,
text_area.selectionStart);
var selection = sel.text;
var selected_text_suffix = text_area.value.substring(
text_area.selectionEnd, text_area.textLength);
} else if (typeof (text_area.selectionStart) !== "undefined") {
/**
Things are pretty straightforward in Mozilla based
browsers & IE > 10.
We can get the selectionStart & selectionEnd character
position directly.
Then compute the prefix and suffix using substr method.
*/
var selected_text_prefix = text_area.value.substr(0,
text_area.selectionStart);
var selection = text_area.value.substr(
text_area.selectionStart,
text_area.selectionEnd - text_area.selectionStart);
var selected_text_suffix = text_area.value.substr(
text_area.selectionEnd);
}
var obj = {};
obj.selection = selection;
obj.selected_text_prefix = selected_text_prefix;
obj.selected_text_suffix = selected_text_suffix;
setCaretPosition(text_area, text_area.selectionEnd);
return obj;
}
/**
* This function can be used to set
* the caret position in a text field object like a text area.
*
* @param element text_field in which the caret is to be set.
* @param int pos position of the caret to be set.
* @return undefined
*/
function setCaretPosition(text_field, pos)
{
if (text_field.setSelectionRange) {
text_field.focus();
text_field.setSelectionRange(pos, pos);
} else if (text_field.createTextRange) {
var range = text_field.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
/**
* Looks up the name'd wiki formatting task, then uses it type to
* render it in the textarea with identifier id
*
* @param String name identifier of the wiki task to be performed
* @param String id identifier of the textarea to add wiki code for the given
* task
*/
function wikifySelection(name, id)
{
var length = 0;
length = editor_buttons[id][name].length;
if (length === 2) {
wikify(editor_buttons[id][name][0].replace(new RegExp('-', 'g'), "\'"),
editor_buttons[id][name][1].replace(new RegExp('-', 'g'), "\'"),
name, id);
} else if (length === 1) {
insertTextAtCursor(editor_buttons[id][name][0], id);
}
}
/**
* This creates a wiki string from wiki_prefix, wiki_suffix, task_name or
* currently selected text and then replaces the currently selected text in the
* DOM element id with this string. Currently, selected text is computed using
* getSelection(id).
*
* @param String wiki_prefix prefix to be added to wikify the selected text.
* @param String wiki_suffix suffix to be added to wikify the selected text.
* @param String task_name name of the task to render default selection text.
* @param String id of textarea to wikify
*/
function wikify(wiki_prefix, wiki_suffix, task_name, id)
{
var br = '';
/*
Select the DOM element - wikiText
*/
var text_area = elt(id);
var obj = getSelection(id);
var selection = obj.selection;
var selected_text_prefix = obj.selected_text_prefix;
var selected_text_suffix = obj.selected_text_suffix;
if (!selection && wiki_prefix === '[[') {
wiki_popup_prompt = elt('wiki-popup-prompt-'+id);
if (wiki_popup_prompt.hasChildNodes()) {
/*
Remove childnodes, if any exist.
*/
while (wiki_popup_prompt.hasChildNodes()) {
wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
}
}
wiki_popup_prompt.appendChild(createHyperlinkForm(id));
editor_buffer = obj;
toggleDisplay('wiki-popup-prompt-' + id);
}
if (!selection && wiki_prefix !== '[[') {
br = '\n';
selection = tl[task_name.replace('wikibtn-', 'wiki_js_')];
}
if (!selection && wiki_prefix !== '[[') {
selection = task_name;
}
/*
Now Add the wrap the selected text between the wiki stuff,
and then wrap the selected
text between actual selected text's prefix and suffix.
Replace the text_area contents with the result.
*/
if (selection) {
var written = wiki_prefix + selection + wiki_suffix + br;
writeIntoWikiSource(text_area, selected_text_prefix.length,
text_area.value.length - selected_text_suffix.length, written);
}
}
/*
* Puts words into a page's source in place of the stretch between two
* points, the way a person's own typing would, so what the browser
* remembers of the writing is kept and the change can be taken back with
* the undo key. Where the browser will not write for us, the words are
* put in by hand and the source is told it has changed, since a browser
* raises nothing when a script writes into a field.
*
* @param Object field the text area holding the page's source
* @param int start where the stretch being replaced begins
* @param int end where it ends
* @param String words what to put there
*/
function writeIntoWikiSource(field, start, end, words)
{
if (!field) {
return;
}
field.focus();
field.setSelectionRange(start, end);
var written = false;
try {
written = document.execCommand("insertText", false, words);
} catch (problem) {
written = false;
}
if (!written) {
var whole = field.value;
field.value = whole.slice(0, start) + words + whole.slice(end);
var after = start + words.length;
field.setSelectionRange(after, after);
wikiSourceChanged(field);
}
}
/*
* Says that the source of a page has been changed by the editor rather
* than by someone typing. A browser raises nothing when a script writes
* into a field, so whatever watches the source, the preview above all,
* would go on showing what was there before the markup was put in.
*
* @param Object field the text area holding the page's source
*/
function wikiSourceChanged(field)
{
if (!field) {
return;
}
field.dispatchEvent(new Event("input", {bubbles: true}));
}
/**
* This is a special function for processing user input
* when the user clicks on hyperlink, and inserts a hyper link.
*
* @param String id of textarea the wiki editor instance is associated with
* @return String
*/
function addWikiHyperlink(id)
{
toggleDisplay('wiki-popup-prompt-' + id);
var title = elt('wikify-link-text-' + id).value;
var link = elt('wikify-link-url-' + id).value;
if (!link && !title) {
return false;
}
if (!link || !title) {
out = "" + link + title;
} else {
out = link + "|" + title;
}
insertTextAtCursor("[[" + out + "]]" + '\n', id);
}
/**
* This function takes in number of rows, columns and
* header_text(if desired) and constructs a table in wiki markup.
*
* @param int rows number of rows intended in the table.
* @param int cols number of cols intended in the table.
* @param string example_text placeholder text to be inserted in each
* table field.
* @param string header_text if header text is intended, placeholder
* text for column header.
* @return string
*/
function createWikiTable(rows, cols, example_text, header_text)
{
var br = "\n";
var table = "{|" + br;
for (i = 0; i < rows; i++) {
if (header_text && i === 0) {
table = table + "|- " + br;
table = table + "! ";
for (j = 0; j < cols; j++) {
table = table + header_text + " ";
if (j < (cols - 1)) {
table = table + "!!" + " ";
} else {
table = table + br;
}
}
}
table = table + "|- " + br;
table = table + "| ";
for (j = 0; j < cols; j++) {
table = table + example_text + " ";
if (j < (cols - 1)) {
table = table + "||" + " ";
} else {
table = table + br;
}
}
}
table = table + "|}";
return table;
}
/**
* Inserts wiki search widget in textarea identified by id
*
* @param String id of textarea to insert wiki search widget
*/
/*
* The markup this list offers, by the words a writer sees and the shape
* that is put in for them. A shape carries an example of each part it
* takes, so what goes in reads as something to edit rather than something
* to look up. Only markup with no mark of its own in the row above is
* here, since anything with one is a press away already.
*/
/*
* What is listening for a press elsewhere or for the escape key while the
* list of markup is open, kept so both can be taken off again when it
* closes.
*/
var wiki_more_syntax_away = null;
var wiki_more_syntax_key = null;
var WIKI_MORE_SYNTAX = [
["wiki_js_syntax_textfield", "{{textfield|Label|field_name}}"],
["wiki_js_syntax_required_textfield",
"{{r-textfield|Label|field_name}}"],
["wiki_js_syntax_textarea", "{{textarea|Label|field_name}}"],
["wiki_js_syntax_required_textarea",
"{{r-textarea|Label|field_name}}"],
["wiki_js_syntax_checkbox", "{{checkbox|Label|field_name}}"],
["wiki_js_syntax_left_checkbox", "{{lcheckbox|Label|field_name}}"],
["wiki_js_syntax_radio", "{{radio|Label|field_name}}"],
["wiki_js_syntax_left_radio", "{{lradio|Label|field_name}}"],
["wiki_js_syntax_dropdown",
"{{dropdown|field_name}}\nFirst\nSecond\n{{end-dropdown}}"],
["wiki_js_syntax_submit", "{{submit|Send|submit_button}}"],
["wiki_js_syntax_keyword_captcha",
"{{keyword-captcha|Question to answer|expected answer}}"],
["wiki_js_syntax_require_signin",
"{{require-signin|Shown to anyone not signed in}}"],
["wiki_js_syntax_username", "{{username|field_name}}"],
["wiki_js_syntax_date", "{{date|field_name}}"],
["wiki_js_syntax_timestamp", "{{timestamp|field_name}}"],
["wiki_js_syntax_presubmit",
"{{presubmit|Shown before the form is sent}}"],
["wiki_js_syntax_same_unit",
"{{same-unit Several lines held together as one thing. }}"],
["wiki_js_syntax_category", "{{category|Name}}"],
["wiki_js_syntax_category_list", "{{category-list|Name|classes}}"],
["wiki_js_syntax_class", "{{class=\"a-class\" Held text}}"],
["wiki_js_syntax_no_contents", "<notoc>\nHeld text\n</notoc>"],
["wiki_js_syntax_preformatted", "<pre>\nHeld text\n</pre>"],
["wiki_js_syntax_math", "<math>\n\\sum_{i=1}^{n} i\n</math>"],
["wiki_js_syntax_indent", ": Indented text"]
];
/*
* Opens the list of markup that has no mark of its own, and puts in
* whichever shape is chosen. The list scrolls, since there is more of it
* than fits above a page being written.
*
* @param String id identifier of the text area holding the page
*/
function addWikiMoreSyntax(id)
{
var holder = elt('more-syntax-holder-' + id);
if (!holder) {
return;
}
var standing = elt('more-syntax-menu-' + id);
if (standing) {
closeWikiMoreSyntax(id);
return;
}
var box = ce("div");
box.className = "wiki-syntax-menu";
box.id = 'more-syntax-menu-' + id;
var listing = ce("ul");
listing.className = "wiki-syntax-menu-list";
/* The list is put in the order of the words a writer reads rather
than the order it is written in here, and those words differ by
language, so the sorting is done as the list is built. */
var offered = [];
for (var index = 0; index < WIKI_MORE_SYNTAX.length; index++) {
offered.push({"says": tl[WIKI_MORE_SYNTAX[index][0]] ||
WIKI_MORE_SYNTAX[index][0],
"shape": WIKI_MORE_SYNTAX[index][1]});
}
/* Sorted in the language the page is written in, which the page
declares on itself, rather than in whichever language the browser
happens to be set to. */
var reading_in = document.documentElement.lang || undefined;
offered.sort(function (one, other) {
return one.says.localeCompare(other.says, reading_in);
});
for (var place = 0; place < offered.length; place++) {
var row = ce("li");
var choice = ce("button");
choice.type = "button";
choice.className = "wiki-syntax-menu-choice";
choice.textContent = offered[place].says;
choice.setAttribute("data-shape", offered[place].shape);
listen(choice, "click", function () {
useWikiMoreSyntax(id, this.getAttribute("data-shape"));
});
row.appendChild(choice);
listing.appendChild(row);
}
box.appendChild(listing);
holder.appendChild(box);
var mark = elt('more-syntax-mark-' + id);
if (mark) {
mark.setAttribute("aria-expanded", "true");
}
/* Pressing anywhere else puts the list away, which is what a reader
expects of a list that drops from a mark, and the escape key does
the same for anyone working from the keyboard. */
wiki_more_syntax_away = function (event) {
if (!holder.contains(event.target)) {
closeWikiMoreSyntax(id);
}
};
wiki_more_syntax_key = function (event) {
if (event.key === "Escape") {
closeWikiMoreSyntax(id);
if (mark) {
mark.focus();
}
}
};
document.addEventListener("mousedown", wiki_more_syntax_away);
document.addEventListener("keydown", wiki_more_syntax_key);
var first = box.querySelector(".wiki-syntax-menu-choice");
if (first) {
first.focus();
}
}
/*
* Puts the list away and stops it listening for a press elsewhere.
*
* @param String id identifier of the text area holding the page
*/
function closeWikiMoreSyntax(id)
{
var box = elt('more-syntax-menu-' + id);
if (box && box.parentNode) {
box.parentNode.removeChild(box);
}
var mark = elt('more-syntax-mark-' + id);
if (mark) {
mark.setAttribute("aria-expanded", "false");
}
if (wiki_more_syntax_away) {
document.removeEventListener("mousedown", wiki_more_syntax_away);
wiki_more_syntax_away = null;
}
if (wiki_more_syntax_key) {
document.removeEventListener("keydown", wiki_more_syntax_key);
wiki_more_syntax_key = null;
}
}
/*
* Puts one shape of markup where the writer was, and closes the list.
*
* @param String id identifier of the text area holding the page
* @param String shape the markup to put in
*/
function useWikiMoreSyntax(id, shape)
{
var field = elt(id);
if (!field) {
return;
}
var start = field.selectionStart;
var end = field.selectionEnd;
closeWikiMoreSyntax(id);
writeIntoWikiSource(field, start, end, shape);
}
function addWikiSearch(id)
{
wiki_popup_prompt = elt('wiki-popup-prompt-'+id);
if (wiki_popup_prompt.hasChildNodes()) {
/*
Remove child nodes, if any exist.
*/
while (wiki_popup_prompt.hasChildNodes()) {
wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
}
}
wiki_popup_prompt.appendChild(createSearchWidgetForm(id));
toggleDisplay('wiki-popup-prompt-'+id);
}
/**
* Gets the size of the search widget to load.
*
* @param String id identifier of the textarea to put search form on
*/
function useInputForSearch(id)
{
toggleDisplay('wiki-popup-prompt-'+id);
size_elt = elt('wiki-search-size-'+id);
var size = size_elt.options[size_elt.selectedIndex].value;
insertTextAtCursor( "{{search:default|size:" + size+ "|placeholder:" +
tl['wiki_js_placeholder'] + "}}\n", id);
}
/**
* Util function to Stringify an JS Object
*
* @param Object js_object the javascript object to be converted to string.
* @return String
*/
function objToString(js_object)
{
var json_string = [];
for (var property in js_object) {
if (js_object.hasOwnProperty(property)) {
json_string.push('"' + property + '"' + ':' + js_object[property] );
}
}
json_string.push();
return '{' + json_string.join(',') + '}';
}
/**
* This is invoked by the editor to insert a wiki table.
* This functions takes care okf the user input for rows/columns/etc
* and leverages createWikiTable function to construct the table.
*
* @param String id
*/
function addWikiTable(id)
{
wiki_popup_prompt = elt('wiki-popup-prompt-'+id);
if (wiki_popup_prompt.hasChildNodes()) {
/*
Remove childnodes, if any exist.
*/
while (wiki_popup_prompt.hasChildNodes()) {
wiki_popup_prompt.removeChild(wiki_popup_prompt.lastChild);
}
}
wiki_popup_prompt.appendChild(createTableForm(id));
toggleDisplay('wiki-popup-prompt-'+id);
}
/**
* Prompt/Draws a form to get input from a user, then uses that input to draw
* a wiki table in the textarea with identifier id
*
* @param String id string identifier of the textarea to draw table in
*/
function useInputForTable(id)
{
var rows = elt('wiki-rows-'+id).value;
var cols = elt('wiki-cols-'+id).value;
var head_checked = elt('wiki-insert-heading-'+id).checked;
addWikiTableFromInput(rows, cols, head_checked, id);
}
/**
* Using the input from the user, like number of rows,cols etc.
* builds a table in wiki markup and inserts at cursor.
* @param int rows number of rows desired
* @param int cols number of cols desired
* @param Boolean head_checked heading desired or not.
*/
function addWikiTableFromInput(rows, cols, head_checked, id)
{
toggleDisplay('wiki-popup-prompt-' + id);
if (!cols || !rows) {
return;
}
if (head_checked) {
var table = createWikiTable(rows, cols,
tl['wiki_js_example'], tl['wiki_js_table_title']);
} else {
table = createWikiTable(rows, cols, tl['wiki_js_example']);
}
insertTextAtCursor(table + '\n', id);
}
/**
* Accepts text as parameter to be inserted at caret's
* current position.
*
* @param string text string that is intended to be placed at current cursor.
*/
function insertTextAtCursor(text, textarea_id)
{
var field = elt(textarea_id);
if (document.selection) {
var range = document.selection.createRange();
if (!range || range.parentElement() !== field) {
field.focus();
range = field.createTextRange();
range.collapse(false);
}
range.text = text;
range.collapse(false);
range.select();
} else {
writeIntoWikiSource(field, field.selectionStart,
field.selectionEnd, text);
}
}
/**
* Used to mark-up selected text in textarea given by id with a heading whose
* size is decided by a select drop down
*
* @param String id of a text area that will mark
*/
function addWikiHeading(id)
{
select_object = elt("wiki-heading-" + id);
var heading_size = select_object.value;
var markup_text;
if (tl?.wiki_js_render_engine === "markdown") {
markup_text = fillChars("#", heading_size) + " ";
wikify(markup_text, "\n", "wikibtn-heading" + heading_size, id);
select_object.selectedIndex = 0;
return;
}
markup_text = fillChars("=", heading_size);
select_object.selectedIndex = 0;
wikify(markup_text, markup_text, "wikibtn-heading" + heading_size , id);
}
/**
* Fills a character array and returns as a string consisting given count
* of a given character.
*
* @param String c character to be filled into an array.
* @param int n number of times the character to be repeated.
* @return String consistring of n many c's
*/
function fillChars(c, n)
{
for (var e = ""; e.length < n; ) {
e += c;
}
return e;
}
/**
* Created an input prompt HTML element with 2 text fields and a checkbox.
* @param String id
* @return HTMLFormElement
*/
function createTableForm(id)
{
var wiki_prompt_for_table = ce("div");
var table_form =
'<div class="wiki-popup-content">' +
'<h2 class="center">'+tl['wiki_js_add_wiki_table']+'</h2>' +
'<form onsubmit="useInputForTable(\'' + id
+'\');return false;"><table><tr>'+
'<th class="float-opposite"><label for="wiki-rows-' + id + '" >' +
tl['wiki_js_for_table_rows'] + '</label></th>' +
'<td><input id="wiki-rows-' + id +
'" name="wiki-prompt-rows" '+ 'type="text"></td></tr>'+
'<tr><th class="float-opposite"><label for="wiki-cols-' + id + '">' +
tl['wiki_js_for_table_cols'] + '</label></th>'+
'<td><input id="wiki-cols-' + id +'" name="wiki-prompt-cols" '+
'type="text"></td></tr>'+
'<tr><th class="float-opposite"><label for="wiki-insert-heading-' +
id + '" >' + tl['wiki_js_prompt_heading'] + '</label></th>' +
'<td><input id="wiki-insert-heading-' + id + '"' +
'name="wiki-insert-heading" type="checkbox"></td></tr>' +
'</table>' +
'<div class="center"><button onmousedown="useInputForTable(\'' + id
+'\')" name="submit" class="button-box">'+ tl['wiki_js_submit'] +
'</button> ' +
'<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
'\')" name="close" class="button-box" >' + tl['wiki_js_cancel'] +
'</button></form></div>';
wiki_prompt_for_table.innerHTML = table_form;
return wiki_prompt_for_table;
}
/**
* Creates an HTMLFormElement with two text fields to get the URL and text of
* a link
* @param String id identifier of the text area that we want to add a wiki link
* to
* @return HTMLFormElement containing form that we will use to get info from
* the user so that we can later add a wiki link
*/
function createHyperlinkForm(id)
{
var wiki_prompt_for_hyperlink = ce("div");
wiki_prompt_for_hyperlink.innerHTML =
'<div class="wiki-popup-content">' +
'<h2 class="center">'+tl['wiki_js_add_hyperlink']+'</h2>' +
'<form onsubmit="addWikiHyperlink(\''+
id +'\');return false;" '+
'><table><tr><th><label for="wikify-link-text-'+id+'">' +
tl['wiki_js_link_text'] + '</label></th>' +
'<td><input id="wikify-link-text-'+id+'" '+
'name="wikify-link-text" type="text"></td></tr>' +
'<tr><th><label for="wiki-link-url-'+id+'">' +
tl['wiki_js_link_url'] + '</label></th>' +
'<td><input id="wikify-link-url-'+id+'" ' +
'name="wikify-enter-link" type="text"></td></tr></table>' +
'<div class="center"><button onmousedown="addWikiHyperlink(\''+
id +'\')" ' + 'name="submit" class="button-box">'+
tl['wiki_js_submit'] +'</button> ' +
'<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
'\')" name="close" class="button-box">' + tl['wiki_js_cancel'] +
'</button></div></form></div>';
return wiki_prompt_for_hyperlink;
}
/**
* Creates an an HTMLFormElement with a form to get the kind of search bar
* that is desired to be inserted into a wiki document
* @param String id identifier of the textarea used to insert wiki text
*/
function createSearchWidgetForm(id)
{
var search_elt = ce("div");
search_form = '<div class="wiki-popup-content">'+
'<h2 class="center">' + tl['wiki_js_add_search'] + '</h2>' +
'<div class="center"><form onsubmit="useInputForSearch(\'' + id +
'\');return false;">' +
'<select name="wiki_search_size" id="wiki-search-size-' + id + '" >' +
'<option disabled="" selected="" >' + tl['wiki_js_search_size'] +
'</option>'+
'<option value="small">' + tl['wiki_js_small'] + '</option>' +
'<option value="medium">' + tl['wiki_js_medium'] + '</option>' +
'<option value="large">' + tl['wiki_js_large'] + '</option>'+
'</select></div>' +
'<div class="center"><button onmousedown="useInputForSearch(\'' + id +
'\');"'+ 'name="submit" class="button-box">' +
tl['wiki_js_submit'] + '</button> ' +
'<button onmousedown="toggleDisplay(\'wiki-popup-prompt-' + id +
'\')" name="close" class="button-box">' + tl['wiki_js_cancel'] +
'</button></form></div></div>';
search_elt.innerHTML = search_form;
return search_elt;
}
/**
* Sets up keyboard events for a wiki textarea after it is in focus
* @param String id identifier of the textarea used to handle keyboard events
* for
*/
function enableKBShortcuts(id)
{
var ctrl_down = false;
/**
* onkeyup handler: clears the Ctrl-down flag when the user
* releases the Control key.
*
* @param {KeyboardEvent} e
*/
document.onkeyup = function keyUp(e)
{
if (e.which == 17) ctrl_down = false;
};
/**
* onkeydown handler: tracks Ctrl-down state and routes
* Ctrl-B / Ctrl-I / Ctrl-U into the bold / italic /
* underline wikify helpers.
*
* @param {KeyboardEvent} e
*/
document.onkeydown = function keyDown(e)
{
if (e.which == 17) {
ctrl_down = true;
}
if (e.which == 66 && ctrl_down == true) {
wikifySelection('wikibtn-bold', id);
return false;
}
if (e.which == 73 && ctrl_down == true) {
wikifySelection('wikibtn-italic', id);
return false;
}
if (e.which == 85 && ctrl_down == true) {
wikifySelection('wikibtn-underline', id);
return false;
}
};
}
/**
* Used to add the wiki text for including a resource into a wiki page
* @param String resource_name string name of resource to be added
* @param String id identifier of the textarea used to insert wiki text
* @param String sub_path path in resource folder to reasource
*/
function addToPage(resource_name, textarea_id, sub_path)
{
/* The word before the file's name and the name itself ran
together, giving Descriptionmyphoto.webp. */
var described = tl['wiki_js_resource_description'] + " " +
resource_name.replace('"', '"');
if (typeof sub_path === 'undefined' || sub_path == '') {
wikify("((resource:", "|" + described + "))",
resource_name.replace('"', '"'), textarea_id);
} else {
wikify("((resource:", "|" + sub_path + "|" + described + "))",
resource_name.replace('"', '"'), textarea_id);
}
}
/*
* Set up event listeners to manage column splitter resizes when we have
* tabular output such as in media lists
*/
document.addEventListener('DOMContentLoaded', function () {
const direction =
document.getElementsByTagName('html')[0].getAttribute('dir');
const direction_sign = (direction == 'rtl') ? -1 : 1;
let split_adjusters = document.getElementsByClassName('split-adjuster');
// Query the element
for (const split_adjuster of split_adjusters) {
let left_elt = split_adjuster.previousElementSibling;
let right_elt = split_adjuster.nextElementSibling;
// The current position of mouse
let x = 0;
let y = 0;
let left_width = 0;
/**
* mousedown handler on a column split adjuster: records
* the starting cursor position and the left column's
* current width, then arms the mousemove / mouseup
* handlers on the document.
*
* @param {MouseEvent} e
*/
let mouseDownHandler = function (e) {
x = e.clientX;
y = e.clientY;
left_width = left_elt.getBoundingClientRect().width;
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
};
/**
* mousemove handler bound during a split-adjuster drag:
* resizes the left column's width based on cursor delta,
* clamped to a minimum of 10%.
*
* @param {MouseEvent} e
*/
let mouseMoveHandler = function (e) {
const dx = direction_sign*(e.clientX - x);
const dy = e.clientY - y;
const new_left_width = Math.max(((left_width + dx) * 100) /
split_adjuster.parentNode.getBoundingClientRect().width, 10);
left_elt.style.width = `${new_left_width}%`;
split_adjuster.style.cursor = 'col-resize';
document.body.style.cursor = 'col-resize';
left_elt.style.userSelect = 'none';
left_elt.style.pointerEvents = 'none';
right_elt.style.userSelect = 'none';
right_elt.style.pointerEvents = 'none';
};
/**
* mouseup handler bound during a split-adjuster drag:
* resets cursor styles, restores text selection, and
* detaches itself plus the mousemove handler from the
* document.
*/
let mouseUpHandler = function () {
split_adjuster.style.removeProperty('cursor');
document.body.style.removeProperty('cursor');
left_elt.style.removeProperty('user-select');
left_elt.style.removeProperty('pointer-events');
right_elt.style.removeProperty('user-select');
right_elt.style.removeProperty('pointer-events');
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
};
split_adjuster.addEventListener('mousedown', mouseDownHandler);
}
});
/**
* Toggles the visibility of a wiki-page AI summary box. The
* first time it's shown, the summary is fetched asynchronously
* via the api/summarize endpoint and inserted on completion.
*
* @param {string} csrf_token CSRF token for the API call
* @param {string} base_url controller base URL ("?" suffix as
* appropriate)
* @param {string|number} summary_id the wiki page / thread id
* whose summary is being toggled
*/
function toggleAISummary(csrf_token, base_url, summary_id)
{
var summary_box = document.getElementById("ai-summary-" + summary_id);
if (!summary_box) return;
if (summary_box.style.display === "none" ||
summary_box.style.display === "") {
summary_box.style.display = "block";
summary_box.innerHTML = "<p>" + tl['wiki_js_summarizing'] + "</p>";
var url = base_url + "?thread_id=" + encodeURIComponent(summary_id) +
"&c=api&a=summarize&YIOOP_TOKEN=" +
encodeURIComponent(csrf_token);
fetch(url, { method: "POST",
headers: {
"User-Agent": "Yioop-Web-Client/1.0",
"Content-Type": "application/x-www-form-urlencoded"
},
credentials: "include"
})
.then(function(response) {
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
return response.json();
})
.then(function(data) {
if (data.SUMMARY) {
summary_box.innerHTML = "<p>" + data.SUMMARY + "</p>";
} else if (data.ERRORS) {
summary_box.innerHTML = "<p>" + data.ERRORS.join(", ") + "</p>";
} else {
summary_box.innerHTML = "<p>" +
tl['wiki_js_summary_unavailable'] + "</p>";
}
})
.catch(function() {
summary_box.innerHTML = "<p>" +
tl['wiki_js_summary_unavailable'] + "</p>";
});
} else {
summary_box.style.display = "none";
}
}
/*
* The preview beside the editor: what opens and closes it, what keeps
* the two panes in step, and what remembers whether a writer had it out.
* The reading of a page into html lives with the parsers rather than
* here.
*/
/*
* How long typing must pause, in milliseconds, before the preview is
* rendered again. Long enough that a run of keystrokes renders once,
* short enough that a pause to read shows the result straight away.
*/
var WIKI_PREVIEW_DELAY = 400;
/*
* The name under which having the preview out is remembered for as long
* as the writer is working.
*/
var WIKI_PREVIEW_CHOICE = "wiki-preview-out";
/*
* Whether the site serves pretty addresses. A file kept with a page is
* asked for as a path where they are on and as a query where they are
* off, and the two are not interchangeable.
*/
var wiki_pretty_addresses = false;
/*
* Where the site is rooted, so a file's address points at the site's own
* file route rather than under the page being edited.
*/
var wiki_base_address = "/";
/*
* The mark under which a file kept with the page is asked for, which for
* a group anyone may join is a row of zeros rather than the reader's own
* mark.
*/
var wiki_resource_mark = "";
/*
* The call that redraws the preview once it is set going, kept so that
* opening the pane can redraw at once rather than waiting on a keystroke.
*/
var wiki_preview_render = null;
/*
* How many letters a picked-out stretch must carry before the other pane
* is asked to find it. A word or two of letters is enough to land in one
* place; a handful would match all over the page.
*/
var WIKI_SELECTION_LEAST = 6;
/*
* Keeps a pane showing what the wiki source in a textarea will look like,
* rendering it in the browser with the parser ported above so that typing
* costs no request and no work on the server. The render waits for a short
* pause in typing rather than running on every keystroke, since parsing a
* long page on each letter would be felt on the keyboard.
*
* While the preview is closed nothing is rendered at all, so a writer who
* never opens it pays nothing for it. The two panes are kept at the same
* place in the document as either is scrolled, so what is being edited is
* what is being looked at.
*
* A page that names a resource is rendered without resolving it: the files
* of a group live on the server and the browser cannot reach them, so such
* a reference is left showing its markup and comes out right once the page
* is saved.
*
* @param String source_id id of the textarea holding the wiki source
* @param String target_id id of the element the rendering is placed into
* @param String group_id id of the group the page belongs to
* @param String page_id id of the page, used for resource addresses
* @param String controller_name the controller a page link should target
* @param String csrf_token_key the query variable name for the csrf token
* @param String csrf_token_value the csrf token value
*/
function initWikiPreview(source_id, target_id, group_id, page_id,
controller_name, csrf_token_key, csrf_token_value)
{
var source = elt(source_id);
var target = elt(target_id);
var pair = elt("wiki-edit-pair");
if (!source || !target || typeof parseWikiContent !== "function") {
return;
}
/* The page's own group and id are carried on the field, since the
page being edited knows them and whoever starts the preview may
not. A resource is drawn by asking the site for it, and the ask
has to say which page's files it wants. */
if (source.dataset) {
if (source.dataset.groupId) {
group_id = source.dataset.groupId;
}
if (source.dataset.pageId) {
page_id = source.dataset.pageId;
}
}
var waiting = null;
var render = function ()
{
waiting = null;
if (pair && pair.classList.contains("wiki-preview-closed")) {
return;
}
wiki_preview_marks_lines = true;
/* Read afresh each time rather than once at the start: a page
written for the first time has no id until it is saved, and
the field is told the id as soon as the site gives one. */
if (source.dataset) {
if (source.dataset.groupId) {
group_id = source.dataset.groupId;
}
if (source.dataset.pageId) {
page_id = source.dataset.pageId;
}
wiki_pretty_addresses = (source.dataset.prettyAddresses === "1");
if (source.dataset.baseAddress) {
wiki_base_address = source.dataset.baseAddress;
}
/* A file kept with the page is asked for under a mark of its
own, which is not always the reader's own mark. */
if (source.dataset.resourceMark !== undefined) {
wiki_resource_mark = source.dataset.resourceMark;
}
}
if (typeof parseMarkdownContent === "function" &&
tl['wiki_js_render_engine'] === "markdown") {
/* A page written in markdown is read by the reader written
for it, so such a page shows a preview of its own rather
than the words the writer typed. */
target.innerHTML = parseMarkdownContent(source.value,
group_id, page_id, csrf_token_key, wiki_resource_mark,
wiki_pretty_addresses, wiki_base_address);
} else {
target.innerHTML = fillWikiSearchBoxes(parseWikiContent(
source.value, group_id, page_id, controller_name,
csrf_token_key, wiki_resource_mark,
wiki_pretty_addresses, wiki_base_address));
}
wiki_preview_marks_lines = false;
/* A list written into a page takes its choices from a block of
kept words elsewhere on the page, which a page fills in as it
opens. The preview draws afresh each time, so it fills them
again over what it has just drawn. */
if (typeof fillCsvBlockUsers === "function") {
fillCsvBlockUsers(target);
}
/* Whatever goes wrong drawing a chart, the words of the page are
already on screen and picking text out of them must keep
working. */
try {
drawWikiPreviewExtras(target);
setWikiPreviewMath(target);
} catch (problem) {
wiki_pane_moved = null;
}
};
var schedule = function ()
{
if (waiting !== null) {
clearTimeout(waiting);
}
waiting = setTimeout(render, WIKI_PREVIEW_DELAY);
};
listen(source, "input", schedule);
/* Each pane is put at the same fraction of its own scrollable length
as the other, which lines the two up whatever the rendering does to
the length of the text. The pane being scrolled is left alone while
it moves the other, so the two do not push each other back and
forth. */
var follow = function (leader, follower)
{
if (wikiPaneEcho(leader)) {
return;
}
var room = leader.scrollHeight - leader.clientHeight;
var along = (room > 0) ? leader.scrollTop / room : 0;
moveWikiPane(follower, along *
(follower.scrollHeight - follower.clientHeight));
};
listen(source, "scroll", function ()
{
follow(source, target);
});
listen(target, "scroll", function ()
{
follow(target, source);
});
initWikiSelectionSync(source, target);
wiki_preview_render = render;
/* The writer had the preview out when last they looked at a page, so
it comes back out. A save reloads the page, and without this a
writer who was reading the preview lost it at every save. */
if (rememberedWikiPreview() === "open" && pair &&
pair.classList.contains("wiki-preview-closed")) {
toggleWikiPreview();
}
render();
}
/*
* Opens or closes the preview pane beside the wiki editor. Opening it
* renders what is written so far, since nothing is rendered while it is
* closed.
*/
function toggleWikiPreview()
{
var pair = elt("wiki-edit-pair");
if (!pair) {
return;
}
/* The editor's own row of controls is put in just before the textarea
by wiki.js, which leaves it inside the source column and starts the
writing area lower than the preview beside it. Moving it above the
two columns puts both at the same place on the screen. It is done
here rather than at page load because the controls are built by a
call the controller adds to the page, which may not have run yet
when the page finishes loading. */
var moved = ["wiki-popup-prompt", "wiki-buttons"];
for (var which = 0; which < moved.length; which++) {
var row = pair.querySelector("." + moved[which]);
if (row && pair.parentNode) {
pair.parentNode.insertBefore(row, pair);
}
}
pair.classList.toggle("wiki-preview-closed");
var closed = pair.classList.contains("wiki-preview-closed");
/* The book says what pressing it will do: shut while the preview is
out of sight, open while it is being read. */
var book = elt("preview-book");
if (book) {
book.innerHTML = closed ? "📕" : "📖";
}
rememberWikiPreview(closed ? "closed" : "open");
if (!closed && wiki_preview_render !== null) {
wiki_preview_render();
}
}
/*
* Remembers whether the writer had the preview out, for as long as they
* are working, so a page that comes back after a save comes back as they
* left it. A browser that will not remember is no reason to refuse the
* choice, so nothing is made of being turned away.
*
* @param String choice "open" or "closed"
*/
function rememberWikiPreview(choice)
{
try {
window.sessionStorage.setItem(WIKI_PREVIEW_CHOICE, choice);
} catch (problem) {
return;
}
}
/*
* Gives back whether the writer had the preview out when last they
* looked at a page.
*
* @return String "open" when they had it out, otherwise an empty answer
*/
function rememberedWikiPreview()
{
try {
return window.sessionStorage.getItem(WIKI_PREVIEW_CHOICE) || "";
} catch (problem) {
return "";
}
}
/*
* Reduces a piece of text to its letters and digits, lower-cased, and
* remembers where each of those came from. Markup and spacing differ
* between what is written and what is drawn from it, so the two are
* compared on their letters alone and the answer is carried back through
* the places recorded here.
*
* @param String text the text to reduce
* @return Object the reduced text under "letters" and, under "places", the
* position in the original of each letter kept
*/
function wikiLettersOnly(text)
{
let letters = "";
let places = [];
for (let index = 0; index < text.length; index++) {
let one = text.charAt(index).toLowerCase();
if ((one >= "a" && one <= "z") || (one >= "0" && one <= "9")) {
letters += one;
places.push(index);
}
}
return {letters: letters, places: places};
}
/*
* Gathers the pieces of text inside an element in the order they are read,
* so a stretch of the whole can be found and then traced back to the piece
* it falls in.
*
* @param Object root the element to gather from
* @return Array each piece as its node, the text of it, and where that
* text begins within the whole
*/
function wikiTextPieces(root)
{
let pieces = [];
let along = 0;
let walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let node = walker.nextNode();
while (node) {
pieces.push({node: node, text: node.nodeValue, begins: along});
along += node.nodeValue.length;
node = walker.nextNode();
}
return pieces;
}
/*
* Takes away any marks a previous match left in the preview, putting the
* pieces of text it split back together.
*
* @param Object target the element the preview is drawn into
*/
function clearWikiPreviewMarks(target)
{
let marks = target.querySelectorAll("." + WIKI_PREVIEW_MARK_CLASS);
for (let index = 0; index < marks.length; index++) {
let mark = marks[index];
while (mark.firstChild) {
mark.parentNode.insertBefore(mark.firstChild, mark);
}
mark.parentNode.removeChild(mark);
}
target.normalize();
}
/*
* Marks the stretch of the preview between two places in its text, and
* brings the first of it into view.
*
* @param Object target the element the preview is drawn into
* @param int from where the stretch begins in the preview's whole text
* @param int to where it ends
*/
function markWikiPreview(target, from, to)
{
clearWikiPreviewMarks(target);
let pieces = wikiTextPieces(target);
let first = null;
for (let index = 0; index < pieces.length; index++) {
let piece = pieces[index];
let ends = piece.begins + piece.text.length;
if (ends <= from || piece.begins >= to) {
continue;
}
let starts_at = Math.max(0, from - piece.begins);
let stops_at = Math.min(piece.text.length, to - piece.begins);
let range = document.createRange();
range.setStart(piece.node, starts_at);
range.setEnd(piece.node, stops_at);
let mark = ce("span");
mark.className = WIKI_PREVIEW_MARK_CLASS;
range.surroundContents(mark);
if (first === null) {
first = mark;
}
}
if (first !== null) {
first.scrollIntoView({block: "nearest"});
}
}
/*
* Lets a stretch of text picked out in one pane show up in the other. What
* is written and what is drawn from it do not read the same, since the
* markup is in one and not the other, so the two are matched on their
* letters and digits alone and the answer is carried back to where those
* letters sit in each.
*
* Picking something out in the preview selects the same words in the
* source, ready to be edited; picking something out in the source marks
* the same words in the preview, which leaves the writing where it is.
*
* @param Object source the textarea holding the wiki source
* @param Object target the element the preview is drawn into
*/
function initWikiSelectionSync(source, target)
{
let syncing = false;
/* From the source to the preview: the letters of what is picked out
are looked for in the letters of the preview, and the stretch they
fall in is marked. */
let fromSource = function ()
{
/* The pane being worked in never moves. Whatever else may scroll
it while this runs, focusing or the browser bringing a caret
into view, it is put back exactly where it was. */
let stay = source.scrollTop;
if (syncing || source.selectionStart == source.selectionEnd) {
return;
}
syncing = true;
let picked = wikiLettersOnly(source.value.substring(
source.selectionStart, source.selectionEnd));
let drawn = wikiLettersOnly(target.textContent);
/* The search begins at the block the picked-out line came from,
so words that also appear earlier, in a table of contents most
of all, are not what gets marked. */
let line = source.value.substring(0,
source.selectionStart).split("\n").length;
let from_letter = wikiPreviewLineLetter(target, drawn, line);
let at = (picked.letters.length < WIKI_SELECTION_LEAST) ? -1 :
drawn.letters.indexOf(picked.letters, from_letter);
if (at < 0 && from_letter > 0) {
at = drawn.letters.indexOf(picked.letters);
}
if (at >= 0) {
markWikiPreview(target, drawn.places[at],
drawn.places[at + picked.letters.length - 1] + 1);
/* Where the answering text sits comes from the line stamps
the parser left, which say outright which block came from
which line, rather than from measuring the mark. */
let line = source.value.substring(0,
source.selectionStart).split("\n").length;
/* The marked words themselves say where the answering text
is, which is what has to come into view. The line stamps
answer only when nothing was marked. */
let mark = target.querySelector("." + WIKI_PREVIEW_MARK_CLASS);
let answering = -1;
if (mark) {
answering = mark.getBoundingClientRect().top -
target.getBoundingClientRect().top + target.scrollTop;
} else {
answering = wikiPreviewLineTop(target, line);
}
if (answering >= 0) {
matchWikiPaneFraction(source,
wikiTextAreaOffset(source, source.selectionStart),
target, answering);
}
if (source.scrollTop !== stay) {
moveWikiPane(source, stay);
}
} else {
clearWikiPreviewMarks(target);
}
syncing = false;
};
/* From the preview to the source: the letters of what is picked out
are looked for in the letters of the source, and the words they came
from are selected there. */
let fromPreview = function ()
{
let stay = target.scrollTop;
if (syncing) {
return;
}
/* wiki.js declares a getSelection of its own at the top level,
which stands in front of the window's, so the document's is
asked for the reader's selection instead. */
let choice = document.getSelection();
if (!choice || choice.rangeCount == 0 || choice.isCollapsed) {
return;
}
if (!target.contains(choice.getRangeAt(0).commonAncestorContainer)) {
return;
}
syncing = true;
/* Where the picked-out words begin, counted in letters from the
start of the preview. Reading it before anything is marked keeps
it exact, and lets the same words be marked again afterwards:
putting the caret in the source takes the browser's own
highlight away, and without this an older mark elsewhere would
be the only thing still showing. */
let reach = document.createRange();
reach.setStart(target, 0);
reach.setEnd(choice.getRangeAt(0).startContainer,
choice.getRangeAt(0).startOffset);
let ahead = wikiLettersOnly(reach.toString()).letters.length;
let picked = wikiLettersOnly(choice.toString());
let written = wikiLettersOnly(source.value);
let at = (picked.letters.length < WIKI_SELECTION_LEAST) ? -1 :
written.letters.indexOf(picked.letters);
if (at >= 0) {
let from = written.places[at];
let to = written.places[at + picked.letters.length - 1] + 1;
let mark = choice.getRangeAt(0).getBoundingClientRect();
let seen = target.getBoundingClientRect();
let where = mark.top - seen.top + target.scrollTop;
source.focus();
source.setSelectionRange(from, to);
let drawn = wikiLettersOnly(target.textContent);
if (ahead + picked.letters.length <= drawn.places.length) {
markWikiPreview(target, drawn.places[ahead],
drawn.places[ahead + picked.letters.length - 1] + 1);
}
/* The line the picked-out text came from is read off the
stamp before it, and the source is put at that line. */
let line = wikiPreviewLineAt(target, where);
/* The stamp says which line of the source the words came
from; where that line sits on screen is measured the same
way, so a wrapped paragraph counts for all the height it
actually takes. */
let answering = wikiTextAreaOffset(source, from);
if (line > 0) {
let upto = source.value.split("\n").slice(0,
line - 1).join("\n").length;
answering = wikiTextAreaOffset(source, upto);
}
matchWikiPaneFraction(target, where, source, answering);
if (target.scrollTop !== stay) {
moveWikiPane(target, stay);
}
}
syncing = false;
};
listen(source, "mouseup", fromSource);
listen(source, "keyup", fromSource);
listen(target, "mouseup", fromPreview);
}
/*
* Which pane was last moved to answer the other, and to what. A pane moved
* this way reports a scroll of its own; recognizing that report by the
* position it carries is what keeps the two from chasing each other, and
* it needs no waiting on a clock to do it.
*/
/*
* The hidden element a textarea's text is laid out in again, to find where
* a place in it sits on screen.
*/
var WIKI_MIRROR_ID = "wiki-text-mirror";
/*
* How much of a pane's height is kept clear at the top and bottom when
* bringing answering text into view, so it is never flush against an edge.
*/
var WIKI_PANE_EDGE = 0.15;
var wiki_pane_moved = null;
/*
* Where that pane was put.
*/
var wiki_pane_moved_to = -1;
/*
* Moves a pane, remembering where it was put so the scroll this causes can
* be told from one the reader made.
*
* @param Object pane the pane to move
* @param int wanted where to put it, before its own limits are applied
*/
function moveWikiPane(pane, wanted)
{
let furthest = pane.scrollHeight - pane.clientHeight;
let put = Math.max(0, Math.min(furthest, wanted));
if (Math.abs(pane.scrollTop - put) < 1) {
return;
}
wiki_pane_moved = pane;
wiki_pane_moved_to = put;
pane.scrollTop = put;
}
/*
* Whether a pane's scroll is the one its own move just caused rather than
* one the reader made. Asking clears the record, so the next scroll of
* that pane is read as the reader's.
*
* @param Object pane the pane that reported a scroll
* @return bool whether this scroll was one we caused
*/
function wikiPaneEcho(pane)
{
if (pane !== wiki_pane_moved) {
return false;
}
if (Math.abs(pane.scrollTop - wiki_pane_moved_to) > 1) {
return false;
}
wiki_pane_moved = null;
return true;
}
/*
* Which letter of the preview the block that began on a given line of the
* source starts at, counting only the letters and digits the two panes are
* matched on. Searching from there keeps words that also appear earlier,
* in a table of contents above all, from being the ones marked.
*
* @param Object target the element the preview is drawn into
* @param Object drawn the preview's letters and where each came from
* @param int line the line of the source being looked for
* @return int the letter to search from, zero when it cannot be told
*/
function wikiPreviewLineLetter(target, drawn, line)
{
let stamps = target.querySelectorAll(".wiki-line");
let found = null;
for (let index = 0; index < stamps.length; index++) {
if (parseInt(attr(stamps[index], "data-line"), 10) <= line) {
found = stamps[index];
}
}
if (found === null) {
return 0;
}
/* How much text stands before the block, in the whole of what the
preview reads as, turned into a count of letters alone. */
let range = document.createRange();
range.setStart(target, 0);
range.setEndBefore(found);
let before = wikiLettersOnly(range.toString()).letters.length;
return Math.min(before, drawn.letters.length);
}
/*
* Where in the preview the block that began on a given line of the source
* was drawn, measured within everything the preview can scroll through.
* The block a line falls in is the last one stamped at or before it.
*
* @param Object target the element the preview is drawn into
* @param int line the line of the source being looked for
* @return int how far down the preview that block sits, or -1 when the
* preview holds no stamps
*/
function wikiPreviewLineTop(target, line)
{
let stamps = target.querySelectorAll(".wiki-line");
if (stamps.length == 0) {
return -1;
}
let found = null;
for (let index = 0; index < stamps.length; index++) {
if (parseInt(attr(stamps[index], "data-line"), 10) <= line) {
found = stamps[index];
}
}
if (found === null) {
found = stamps[0];
}
let seen = target.getBoundingClientRect();
return found.getBoundingClientRect().top - seen.top + target.scrollTop;
}
/*
* Which line of the source the block holding a place in the preview began
* on, by reading the stamp the parser left before it.
*
* @param Object target the element the preview is drawn into
* @param int at how far down the preview the place is, measured within
* everything it can scroll through
* @return int the line, or -1 when the preview holds no stamps
*/
function wikiPreviewLineAt(target, at)
{
let stamps = target.querySelectorAll(".wiki-line");
if (stamps.length == 0) {
return -1;
}
let seen = target.getBoundingClientRect();
let line = parseInt(attr(stamps[0], "data-line"), 10);
for (let index = 0; index < stamps.length; index++) {
let top = stamps[index].getBoundingClientRect().top - seen.top +
target.scrollTop;
if (top > at) {
break;
}
line = parseInt(attr(stamps[index], "data-line"), 10);
}
return line;
}
/*
* Puts the marked stretch of one pane at the same place down its visible
* height as the picked-out text sits down the other, so what is being
* looked at in one is at eye level in the other. The pane the text was
* picked out in is never moved.
*
* @param Object leader the pane the text was picked out in
* @param int leader_top where the picked-out text begins within the
* leader's whole scrollable height
* @param Object follower the pane to move
* @param int follower_top where the answering text begins within the
* follower's whole scrollable height
*/
function matchWikiPaneFraction(leader, leader_top, follower, follower_top)
{
let visible = leader.clientHeight;
if (visible < 1) {
return;
}
let along = (leader_top - leader.scrollTop) / visible;
/* Held away from either edge. The words being answered have to be
read, and at the very top or bottom they sit half out of the pane;
when the picked-out text is off screen in the pane being worked in
there is no fraction to match anyway, so this is where it lands. */
if (along < WIKI_PANE_EDGE) {
along = WIKI_PANE_EDGE;
}
if (along > 1 - WIKI_PANE_EDGE) {
along = 1 - WIKI_PANE_EDGE;
}
moveWikiPane(follower, follower_top - along * follower.clientHeight);
}
/*
* Where a place in a textarea's text sits down its whole scrollable
* height, in pixels. Counting newlines will not do: a paragraph too long
* for the width is one line of text but several lines on screen, and the
* two answers drift further apart the more of them a page has.
*
* The text up to that place is laid out again in a hidden element wearing
* the textarea's own font, width and padding, and the height it reaches is
* where that place sits.
*
* @param Object area the textarea to look in
* @param int at the position in its text
* @return int how far down its scrollable height that position is
*/
function wikiTextAreaOffset(area, at)
{
let mirror = elt(WIKI_MIRROR_ID);
if (!mirror) {
mirror = ce("div");
mirror.id = WIKI_MIRROR_ID;
document.body.appendChild(mirror);
}
let worn = getComputedStyle(area);
let copied = ["fontFamily", "fontSize", "fontWeight", "fontStyle",
"letterSpacing", "lineHeight", "textTransform", "wordSpacing",
"paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
"borderTopWidth", "borderBottomWidth", "borderLeftWidth",
"borderRightWidth", "boxSizing"];
for (let index = 0; index < copied.length; index++) {
mirror.style[copied[index]] = worn[copied[index]];
}
mirror.style.position = "absolute";
mirror.style.visibility = "hidden";
mirror.style.top = "0";
mirror.style.left = "-9999px";
mirror.style.whiteSpace = "pre-wrap";
mirror.style.overflowWrap = "break-word";
mirror.style.width = area.clientWidth + "px";
mirror.textContent = area.value.substring(0, at);
let here = ce("span");
here.textContent = "\u200b";
mirror.appendChild(here);
return here.offsetTop;
}