Merge pull request #15559 from Raroun/js/modal-compose-modernization

Modernize compose.js and modal.js with improved architecture and bug fixing
This commit is contained in:
Michael Vogel 2026-03-03 15:39:07 +01:00 committed by GitHub
commit 7660a94e7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 727 additions and 366 deletions

View file

@ -3,63 +3,374 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later
$(function () {
// Jot attachment live preview.
let $textarea = $("textarea[name=body]");
$textarea.linkPreview();
$textarea.keyup(function () {
var textlen = $(this).val().length;
$("#character-counter").text(textlen);
});
$textarea.editor_autocomplete(baseurl + "/search/acl");
$textarea.bbco_autocomplete("bbcode");
let location_button = document.getElementById("profile-location");
let location_input = document.getElementById("jot-location");
/**
* @file view/theme/frio/js/compose.js
* JavaScript for the compose page (/compose).
*
* Handles:
* - Link preview attachment
* - Character counter
* - ACL autocomplete (@-mentions)
* - BBCode autocomplete
* - Location button with geolocation support
*
* @requires jQuery
* @requires linkPreview.js
* @requires autocomplete.js
*/
if (location_button && location_input) {
updateLocationButtonDisplay(location_button, location_input);
(function ($, window, document) {
"use strict";
location_input.addEventListener("change", function () {
updateLocationButtonDisplay(location_button, location_input);
});
location_input.addEventListener("keyup", function () {
updateLocationButtonDisplay(location_button, location_input);
// DOM element references (cached)
let $textarea = null;
let locationButton = null;
let locationInput = null;
/**
* Initialize the compose page functionality.
*/
function init() {
initTextarea();
initLocation();
initFormReset();
}
/**
* Handle form reset after successful submission and draft saving.
* Clears the form when a post was successfully submitted to prevent
* showing old content on the next visit to /compose.
* Also saves/restores scheduling settings as draft.
*/
function initFormReset() {
const $form = $("form.comment-edit-form");
// Check if sessionStorage is available and if a post was submitted
// This flag is set in the submit handler below
if (isStorageAvailable() && sessionStorage.getItem("compose_post_submitted") === "true") {
// Clear the flag
sessionStorage.removeItem("compose_post_submitted");
// Clear saved draft data
sessionStorage.removeItem("compose_draft");
// Clear the form after a short delay to let browser autofill complete
setTimeout(function () {
if ($textarea && $textarea.length) {
$textarea.val("");
updateCharacterCounter(0);
}
// Also clear location if set
if (locationInput) {
locationInput.value = "";
updateLocationButtonDisplay(locationButton, locationInput);
}
// Clear any link preview
if (typeof window.linkPreview === "object" && window.linkPreview !== null) {
window.linkPreview.destroy();
window.linkPreview = null;
}
// Reset scheduled time
$('[name="scheduled_at"]').val("");
}, 50);
} else {
// No submission - try to restore draft if available
restoreDraft();
}
// Save draft when form values change (but not on submit)
$form.on("change.compose-draft input.compose-draft", function (e) {
// Don't save if this is triggered by programmatic changes
if (e.isTrigger) {
return;
}
saveDraft();
});
location_button.addEventListener("click", function () {
if (location_input.value) {
location_input.value = "";
updateLocationButtonDisplay(location_button, location_input);
} else if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
function (position) {
location_input.value = position.coords.latitude + ", " + position.coords.longitude;
updateLocationButtonDisplay(location_button, location_input);
},
function (error) {
location_button.disabled = true;
updateLocationButtonDisplay(location_button, location_input);
},
);
// Listen for form submission
$form.on("submit.compose", function () {
// Mark that the form was submitted
// This flag will be checked when the page loads next time
if (isStorageAvailable()) {
sessionStorage.setItem("compose_post_submitted", "true");
}
});
}
});
function updateLocationButtonDisplay(location_button, location_input) {
location_button.classList.remove("btn-primary");
if (location_input.value) {
location_button.disabled = false;
location_button.classList.add("btn-primary");
location_button.title = location_button.dataset.titleClear;
} else if (!"geolocation" in navigator) {
location_button.disabled = true;
location_button.title = location_button.dataset.titleUnavailable;
} else if (location_button.disabled) {
location_button.title = location_button.dataset.titleDisabled;
} else {
location_button.title = location_button.dataset.titleSet;
/**
* Check if sessionStorage is available.
* @returns {boolean}
*/
function isStorageAvailable() {
try {
const test = "__test__";
sessionStorage.setItem(test, test);
sessionStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
}
/**
* Save current form state as draft to sessionStorage.
*/
function saveDraft() {
if (!$textarea || !$textarea.length || !isStorageAvailable()) {
return;
}
const draft = {
body: $textarea.val(),
location: locationInput ? locationInput.value : "",
scheduled_at: $('[name="scheduled_at"]').val(),
};
try {
sessionStorage.setItem("compose_draft", JSON.stringify(draft));
} catch (e) {
// Ignore storage errors (e.g., quota exceeded, private mode)
}
}
/**
* Restore form state from sessionStorage draft.
*/
function restoreDraft() {
if (!isStorageAvailable()) {
return;
}
try {
const draftJson = sessionStorage.getItem("compose_draft");
if (!draftJson) {
return;
}
const draft = JSON.parse(draftJson);
// Restore values after a short delay to override browser autofill
setTimeout(function () {
if (typeof draft.body !== "undefined" && $textarea && $textarea.length) {
$textarea.val(draft.body);
updateCharacterCounter(draft.body.length);
}
// Restore location (check for undefined, not falsy, so empty string works)
if (typeof draft.location !== "undefined" && locationInput) {
locationInput.value = draft.location;
updateLocationButtonDisplay(locationButton, locationInput);
// Trigger change event so other scripts can react
$(locationInput).trigger("change");
}
// Restore scheduled time (check for undefined, not falsy)
const $scheduledAt = $('[name="scheduled_at"]');
if (typeof draft.scheduled_at !== "undefined" && $scheduledAt.length) {
$scheduledAt.val(draft.scheduled_at);
// Trigger change event for datepicker plugins
$scheduledAt.trigger("change");
}
}, 100);
} catch (e) {
// Ignore parse errors
sessionStorage.removeItem("compose_draft");
}
}
/**
* Initialize textarea features: link preview, autocomplete, character counter.
*/
function initTextarea() {
$textarea = $("textarea[name=body]");
if (!$textarea.length) {
return;
}
// Initialize link preview plugin
if (typeof $.fn.linkPreview === "function") {
$textarea.linkPreview();
}
// Initialize ACL autocomplete (@-mentions)
if (typeof $.fn.editor_autocomplete === "function" && typeof baseurl !== "undefined") {
$textarea.editor_autocomplete(baseurl + "/search/acl");
}
// Initialize BBCode autocomplete
if (typeof $.fn.bbco_autocomplete === "function") {
$textarea.bbco_autocomplete("bbcode");
}
// Character counter - use input event to catch all changes (paste, cut, etc.)
$textarea.on("input.compose", function () {
updateCharacterCounter($(this).val().length);
});
// Initial count
updateCharacterCounter($textarea.val().length);
}
/**
* Update the character counter display.
*
* @param {number} count - The character count
*/
function updateCharacterCounter(count) {
$("#character-counter").text(count);
}
/**
* Initialize location button functionality.
*/
function initLocation() {
locationButton = document.getElementById("profile-location");
locationInput = document.getElementById("jot-location");
if (!locationButton || !locationInput) {
return;
}
// Set initial button state
updateLocationButtonDisplay(locationButton, locationInput);
// Bind to both input and change events for robustness
$(locationInput)
.on("input.compose change.compose", function () {
updateLocationButtonDisplay(locationButton, locationInput);
});
// Location button click handler
$(locationButton).on("click.compose", function () {
handleLocationButtonClick();
});
}
/**
* Handle the location button click.
*/
function handleLocationButtonClick() {
if (!locationButton || !locationInput) {
return;
}
// If location is already set, clear it
if (locationInput.value) {
locationInput.value = "";
updateLocationButtonDisplay(locationButton, locationInput);
// Trigger change event to save draft
$(locationInput).trigger("change");
return;
}
// Otherwise, try to get geolocation
if (!("geolocation" in navigator)) {
// Geolocation not supported - button should already be disabled
return;
}
// Temporarily disable button while getting position
locationButton.disabled = true;
navigator.geolocation.getCurrentPosition(
handleGeolocationSuccess,
handleGeolocationError,
{
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 600000, // 10 minutes
}
);
}
/**
* Handle successful geolocation retrieval.
*
* @param {GeolocationPosition} position
*/
function handleGeolocationSuccess(position) {
if (!locationInput || !locationButton) {
return;
}
const coords = position.coords;
locationInput.value = coords.latitude + ", " + coords.longitude;
locationButton.disabled = false;
updateLocationButtonDisplay(locationButton, locationInput);
// Trigger change event to save draft
$(locationInput).trigger("change");
}
/**
* Handle geolocation error.
*
* @param {GeolocationPositionError} error
*/
function handleGeolocationError(error) {
if (!locationButton) {
return;
}
// Re-enable button so user can try again (unless geolocation is unsupported)
if ("geolocation" in navigator) {
locationButton.disabled = false;
}
updateLocationButtonDisplay(locationButton, locationInput);
// Log error for debugging (non-critical)
if (typeof console !== "undefined" && console.warn) {
console.warn("Geolocation error:", error.message);
}
}
/**
* Update the location button display based on current state.
*
* @param {HTMLButtonElement} button - The location button element
* @param {HTMLInputElement} input - The location input element
*/
function updateLocationButtonDisplay(button, input) {
if (!button || !input) {
return;
}
const hasValue = !!input.value;
const hasGeolocation = "geolocation" in navigator;
// Remove primary class first (will be re-added if needed)
button.classList.remove("btn-primary");
if (hasValue) {
// Location is set - button clears it
button.disabled = false;
button.classList.add("btn-primary");
button.title = button.dataset.titleClear || "Clear location";
} else if (!hasGeolocation) {
// Geolocation not supported
button.disabled = true;
button.title = button.dataset.titleUnavailable || "Geolocation not available";
} else if (button.disabled) {
// Geolocation supported but button disabled (error state)
button.title = button.dataset.titleDisabled || "Location unavailable";
} else {
// Ready to get location
button.title = button.dataset.titleSet || "Set location";
}
}
// Initialize on DOM ready
$(function () {
init();
});
// Expose public API
window.updateLocationButtonDisplay = updateLocationButtonDisplay;
})(jQuery, window, document);
// @license-end

View file

@ -3,369 +3,419 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later
/**
* Contains functions for bootstrap modal handling.
* @file view/theme/frio/js/modal.js
* Bootstrap modal handling for the Frio theme.
*
* This module provides functions for modal operations including:
* - Generic modal content loading
* - Jot (post composition) modal handling
* - File browser integration
*
* @requires jQuery
* @requires Bootstrap Modal
*/
$(document).ready(function () {
// Clear bs modal on close.
// We need this to prevent that the modal displays old content.
$("body, footer").on("hidden.bs.modal", ".modal", function () {
$(this).removeData("bs.modal");
$("#modal-title").empty();
$("#modal-body").empty();
// Remove the file browser from jot (else we would have problems
// with AjaxUpload.
$(".fbrowser").remove();
// Remove the AjaxUpload element.
$(".ajaxbutton-wrapper").remove();
});
// Clear bs modal on close.
// We need this to prevent that the modal displays old content.
$("body").on("hidden.bs.modal", "#jot-modal", function () {
// Restore cached jot at its hidden position ("#jot-content").
$("#jot-content").append(jotcache);
// Clear the jotcache.
jotcache = "";
// Destroy the attachment linkPreview for Jot.
if (typeof linkPreview === "object") {
linkPreview.destroy();
}
});
(function ($, window, document) {
"use strict";
// Navbar login.
$("body").on("click", "#nav-login", function (e) {
e.preventDefault();
Dialog.show(this.href, this.dataset.originalTitle || this.title);
});
// Module state
let isJotResetBound = false;
let isEditJotClosing = false;
// Jot nav menu..
$("body").on("click", "#jot-modal .jot-nav li .jot-nav-lnk", function (e) {
e.preventDefault();
toggleJotNav(this);
});
/**
* Initialize modal handlers on document ready.
*/
$(function () {
const $body = $("body");
// Bookmarklet page needs an jot modal which appears automatically.
if (window.location.pathname.indexOf("/bookmarklet") >= 0 && $("#jot-modal").length) {
jotShow();
}
// Clear bs modal on close - prevent old content display.
// Using a single namespaced event handler for all modals.
$body.on("hidden.bs.modal.frio", ".modal", function () {
const $modal = $(this);
$modal.removeData("bs.modal");
$("#modal-title").empty();
$("#modal-body").empty();
// Clean up file browser elements
$(".fbrowser").remove();
$(".ajaxbutton-wrapper").remove();
});
// Open filebrowser for elements with the class "image-select"
// The following part handles the filebrowser for field_fileinput.tpl.
$("body").on("click", ".image-select", function () {
// Set a extra attribute to mark the clicked button.
this.setAttribute("image-input", "select");
Dialog.doImageBrowser("input");
});
// Insert filebrowser images into the input field (field_fileinput.tpl).
$("body").on("fbrowser.photo.input", function (e, filename, embedcode, id, img) {
// Select the clicked button by it's attribute.
var elm = $("[image-input='select']");
// Select the input field which belongs to this button.
var input = elm.parent(".input-group").children("input");
// Remove the special indicator attribut from the button.
elm.removeAttr("image-input");
// Insert the link from the image into the input field.
input.val(img);
});
// Generic delegated event to open an anchor URL in a modal.
// Used in the hovercard.
document.getElementsByTagName("body")[0].addEventListener("click", function (e) {
var target = e.target;
while (target) {
if (target.matches && target.matches("a.add-to-modal")) {
addToModal(target.href);
e.preventDefault();
return false;
// Special handling for jot-modal close - restore cached jot content
$body.on("hidden.bs.modal.frio", "#jot-modal", function () {
// Restore cached jot at its hidden position ("#jot-content")
if (window.jotcache && window.jotcache.length) {
$("#jot-content").append(window.jotcache);
window.jotcache = "";
}
// Destroy the attachment linkPreview for Jot
if (typeof window.linkPreview === "object" && window.linkPreview !== null) {
window.linkPreview.destroy();
window.linkPreview = null;
}
});
target = target.parentNode || null;
// Navbar login
$body.on("click.frio", "#nav-login", function (e) {
e.preventDefault();
if (window.Dialog && typeof window.Dialog.show === "function") {
window.Dialog.show(this.href, this.dataset.originalTitle || this.title);
}
});
// Jot nav menu tabs
$body.on("click.frio", "#jot-modal .jot-nav li .jot-nav-lnk", function (e) {
e.preventDefault();
toggleJotNav(this);
});
// Bookmarklet page needs a jot modal which appears automatically
if (window.location.pathname.indexOf("/bookmarklet") >= 0 && $("#jot-modal").length) {
// jotShow is defined in jot-header.tpl
if (typeof window.jotShow === "function") {
window.jotShow();
}
}
});
});
// Overwrite Dialog.show from main js to load the filebrowser into a bs modal.
Dialog.show = function (url, title) {
if (typeof title === "undefined") {
title = "";
// Open filebrowser for elements with the class "image-select"
$body.on("click.frio", ".image-select", function () {
this.setAttribute("image-input", "select");
if (window.Dialog && typeof window.Dialog.doImageBrowser === "function") {
window.Dialog.doImageBrowser("input");
}
});
// Insert filebrowser images into the input field
$body.on("fbrowser.photo.input.frio", function (e, filename, embedcode, id, img) {
const $elm = $("[image-input='select']");
const $input = $elm.closest(".input-group").find("input");
$elm.removeAttr("image-input");
$input.val(img);
});
// Generic delegated event to open an anchor URL in a modal
$body.on("click.frio", "a.add-to-modal", function (e) {
e.preventDefault();
addToModal(this.href);
});
// Bind the edit-jot reset handler exactly once using event delegation
bindJotResetOnce();
});
/**
* Bind the jot reset handler exactly once to prevent duplicate event registration.
* This fixes the critical bug where calling editpost() multiple times would
* register multiple handlers, causing the reset code to run multiple times.
*/
function bindJotResetOnce() {
if (isJotResetBound) {
return;
}
isJotResetBound = true;
$("body").on("hidden.bs.modal.frio-edit", "#jot-modal.edit-jot", function () {
// Prevent concurrent execution
if (isEditJotClosing) {
return;
}
isEditJotClosing = true;
const $modal = $(this);
$modal.removeData("bs.modal");
$(".jot-nav .jot-perms-lnk").parent("li").removeClass("hidden");
$("#profile-jot-form #jot-title-wrap, #profile-jot-form #jot-category-wrap").show();
$modal.removeClass("edit-jot");
$("#jot-modal-content").empty();
// Reset flag after a short delay to allow Bootstrap to finish its cleanup
setTimeout(function () {
isEditJotClosing = false;
}, 0);
});
}
var modal = $("#modal").modal();
modal.find("#modal-header h4").html(title);
modal.find("#modal-body").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
modal.show();
/**
* Overwrite Dialog.show from main js to load the filebrowser into a bs modal.
*
* @param {string} url - The URL to load
* @param {string} [title] - Optional modal title
*/
window.Dialog.show = function (url, title) {
title = title || "";
const $modal = $("#modal").modal();
$modal.find("#modal-header h4").html(title);
$modal.find("#modal-body").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
$modal.show();
if (typeof window.Dialog._load === "function") {
window.Dialog._load(url);
}
}
});
};
$(function () {
Dialog._load(url);
/**
* Overwrite the function _get_url from main.js.
*
* @param {string} type - The browser type
* @param {string} name - The browser name
* @param {string|number} [id] - Optional ID
* @returns {string} The constructed URL
*/
window.Dialog._get_url = function (type, name, id) {
let hash = name;
if (id !== undefined) {
hash = hash + "-" + id;
}
return "media/" + type + "/browser?mode=none&theme=frio#" + hash;
};
/**
* Load the filebrowser into the jot modal.
*/
window.Dialog.showJot = function () {
const type = "photo";
const name = "main";
const url = window.Dialog._get_url(type, name);
if ($(".modal-body #jot-fbrowser-wrapper .fbrowser").length < 1) {
$("#jot-fbrowser-wrapper").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
if (typeof window.Dialog._load === "function") {
window.Dialog._load(url);
}
}
});
}
});
};
};
// Overwrite the function _get_url from main.js.
Dialog._get_url = function (type, name, id) {
var hash = name;
if (id !== undefined) hash = hash + "-" + id;
return 'media/' + type + '/browser?mode=none&theme=frio#' + hash;
};
/**
* Initialize the filebrowser after page load.
*
* @param {string} url - The URL being loaded
*/
window.Dialog._load = function (url) {
const filebrowser = document.getElementById("filebrowser");
const match = url.match(/media\/[a-z]+\/.*(#.*)/);
// Does load the filebrowser into the jot modal.
Dialog.showJot = function () {
var type = "photo";
var name = "main";
if (!filebrowser || match === null) {
return; // not fbrowser
}
var url = Dialog._get_url(type, name);
if ($(".modal-body #jot-fbrowser-wrapper .fbrowser").length < 1) {
// Load new content to fbrowser window.
$("#jot-fbrowser-wrapper").load(url, function (responseText, textStatus) {
// Initialize the filebrowser
if (typeof window.loadScript === "function") {
window.loadScript("view/js/ajaxupload.js");
window.loadScript("view/theme/frio/js/module/media/browser.js", function () {
if (typeof window.Browser !== "undefined" && typeof window.Browser.init === "function") {
window.Browser.init(filebrowser.dataset.nickname, filebrowser.dataset.type, match[1]);
}
});
}
};
/**
* Add first element with the class "heading" as modal title.
* Note: This should ideally be done in the template.
*/
function loadModalTitle() {
$("#modal-title").empty();
const $heading = $("#modal-body .heading").first();
$heading.hide();
let title = "";
// Special handling for event modals
if ($("#modal-body .event-wrapper .event-summary").length) {
const eventsum = $("#modal-body .event-wrapper .event-summary").html();
title = '<i class="fa fa-calendar" aria-hidden="true"></i>&nbsp;' + eventsum;
} else {
title = $heading.html();
}
if (title) {
$("#modal-title").append(title);
}
}
/**
* Load HTML content from a Friendica page into a modal.
*
* @param {string} url - The URL with HTML content
* @param {string} [id] - Optional ID of a specific HTML element to show
*/
function addToModal(url, id) {
const char = window.qOrAmp ? window.qOrAmp(url) : (url.indexOf("?") < 0 ? "?" : "&");
url = url + char + "mode=none";
if (typeof id !== "undefined") {
url = url + " div#" + id;
}
const $modal = $("#modal").modal();
$modal.find("#modal-body").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
$(function () {
Dialog._load(url);
});
$modal.show();
loadModalTitle();
// Re-initialize autosize for new modal content
if (typeof window.autosize === "function") {
window.autosize($(".modal .text-autosize"));
}
}
});
}
};
// Init the filebrowser after page load.
Dialog._load = function (url) {
// Get nickname & filebrowser type from the modal content.
let filebrowser = document.getElementById("filebrowser");
/**
* Add an element (by its id) to a bootstrap modal.
*
* @param {string} id - The element ID selector
*/
function addElmToModal(id) {
const elm = $(id).html();
const $modal = $("#modal").modal();
// Try to fetch the hash form the url.
let match = url.match(/media\/[a-z]+\/.*(#.*)/);
if (!filebrowser || match === null) {
return; //not fbrowser
}
// Initialize the filebrowser.
loadScript("view/js/ajaxupload.js");
loadScript("view/theme/frio/js/module/media/browser.js", function () {
Browser.init(filebrowser.dataset.nickname, filebrowser.dataset.type, match[1]);
});
};
/**
* Add first element with the class "heading" as modal title
*
* Note: this should be really done in the template
* and is the solution where we havent done it until this
* moment or where it isn't possible because of design
*/
function loadModalTitle() {
// Clear the text of the title.
$("#modal-title").empty();
// Hide the first element with the class "heading" of the modal body.
$("#modal-body .heading").first().hide();
var title = "";
// Get the text of the first element with "heading" class.
title = $("#modal-body .heading").first().html();
// for event modals we need some special handling
if ($("#modal-body .event-wrapper .event-summary").length) {
title = '<i class="fa fa-calendar" aria-hidden="true"></i>&nbsp;';
var eventsum = $("#modal-body .event-wrapper .event-summary").html();
title = title + eventsum;
}
// And append it to modal title.
if (title !== "") {
$("#modal-title").append(title);
}
}
/**
* This function loads html content from a friendica page into a modal.
*
* @param {string} url The url with html content.
* @param {string} id The ID of a html element (can be undefined).
* @returns {void}
*/
function addToModal(url, id) {
var char = qOrAmp(url);
url = url + char + "mode=none";
var modal = $("#modal").modal();
// Only search for an element if we have an ID.
if (typeof id !== "undefined") {
url = url + " div#" + id;
}
modal.find("#modal-body").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
modal.show();
//Get first element with the class "heading"
//and use it as title.
loadModalTitle();
// We need to initialize autosize again for new
// modal content.
autosize($(".modal .text-autosize"));
}
});
}
// Add an element (by its id) to a bootstrap modal.
function addElmToModal(id) {
var elm = $(id).html();
var modal = $("#modal").modal();
modal.find("#modal-body").append(elm).modal.show;
loadModalTitle();
}
// Function to load the html from the edit post page into
// the jot modal.
function editpost(url) {
// Next to normel posts the post can be an event post. The event posts don't
// use the normal Jot modal. For event posts we will use a normal modal
// But first we have to test if the url links to an event. So we will split up
// the url in its parts.
var splitURL = parseUrl(url);
// Test if in the url path containing "calendar/event/show". If the path containing this
// expression then we will call the addToModal function and exit this function at
// this point.
if (splitURL.path.indexOf("calendar/event/show") > -1) {
addToModal(splitURL.path);
return;
}
var modal = $("#jot-modal").modal();
url = url + " #jot-sections";
$(".jot-nav .jot-perms-lnk").parent("li").addClass("hidden");
// For editpost we load the modal html of "jot-sections" of the edit page. So we would have two jot forms in
// the page html. To avoid js conflicts we store the original jot in the variable jotcache.
// After closing the modal original jot should be restored at its original position in the html structure.
jotcache = $("#jot-content > #jot-sections");
// Remove the original Jot as long as the edit Jot is open.
jotcache.detach();
// Add the class "edit" to the modal to have some kind of identifier to
// have the possibility to e.g. put special event-listener.
$("#jot-modal").addClass("edit-jot");
jotreset();
modal.find("#jot-modal-content").load(url, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
// get the item type and hide the input for title and category if it isn't needed.
var type = $(responseText).find("#profile-jot-form input[name='type']").val();
if (type === "wall-comment" || type === "remote-comment") {
// Hide title and category input fields because we don't.
$("#profile-jot-form #jot-title-wrap").hide();
$("#profile-jot-form #jot-category-wrap").hide();
}
// To make dropzone fileupload work on editing a comment, we need to
// attach a new dropzone to modal
if ($('#jot-text-wrap').length > 0) {
dzFactory.setupDropzone('#jot-text-wrap', 'profile-jot-text');
}
modal.show();
$("#jot-popup").show();
if ($("#profile-jot-text").length > 0) {
linkPreview = $("#profile-jot-text").linkPreview();
}
}
});
}
// Remove content from the jot modal.
function jotreset() {
// Clear bs modal on close.
// We need this to prevent that the modal displays old content.
$("body").on("hidden.bs.modal", "#jot-modal.edit-jot", function () {
$(this).removeData("bs.modal");
$(".jot-nav .jot-perms-lnk").parent("li").removeClass("hidden");
$("#profile-jot-form #jot-title-wrap").show();
$("#profile-jot-form #jot-category-wrap").show();
// Remove the "edit-jot" class so we can the standard behavior on close.
$("#jot-modal.edit-jot").removeClass("edit-jot");
$("#jot-modal-content").empty();
});
}
// Give the active "jot-nav" list element the class "active".
function toggleJotNav(elm) {
// Get the ID of the tab panel which should be activated.
var tabpanel = elm.getAttribute("aria-controls");
var cls = hasClass(elm, "jot-nav-lnk-mobile");
// Select all li of jot-nav and remove the active class.
$(elm).parent("li").siblings("li").removeClass("active");
// Add the active class to the parent of the link which was selected.
$(elm).parent("li").addClass("active");
// Minimize all tab content wrapper and activate only the selected
// tab panel.
$("#profile-jot-form > [role=tabpanel]").addClass("minimize").attr("aria-hidden", "true");
$("#" + tabpanel)
.removeClass("minimize")
.attr("aria-hidden", "false");
// Set the aria-selected states
$("#jot-modal .modal-header .nav-tabs .jot-nav-lnk").attr("aria-selected", "false");
elm.setAttribute("aria-selected", "true");
// For some tab panels we need to execute other js functions.
if (tabpanel === "jot-preview-content") {
preview_post();
// Make Share button visible in preview
$("#jot-preview-share").removeClass("minimize").attr("aria-hidden", "false");
} else if (tabpanel === "jot-fbrowser-wrapper") {
$(function () {
Dialog.showJot();
});
}
// If element is a mobile dropdown nav menu we need to change the button text.
if (cls) {
toggleDropdownText(elm);
}
}
// Wall Message needs a special handling because in some cases
// it redirects you to your own server. In such cases we can't
// load it into a modal.
function openWallMessage(url) {
// Split the url in its parts.
var parts = parseUrl(url);
// If the host isn't the same we can't load it in a modal.
// So we will go to to the url directly.
if ("host" in parts && parts.host !== window.location.host) {
window.location.href = url;
} else {
// Otherwise load the wall message into a modal.
addToModal(url);
}
}
// This function load the content of the edit url into a modal.
/// @todo Rename this function because it can be used for more than events.
function eventEdit(url) {
var char = qOrAmp(url);
url = url + char + "mode=none";
$.get(url, function (data) {
$("#modal-body").empty();
$("#modal-body").append(data);
}).done(function () {
$modal.find("#modal-body").append(elm);
loadModalTitle();
});
}
}
/**
* Load the HTML from the edit post page into the jot modal.
*
* @param {string} url - The edit post URL
*/
function editpost(url) {
// Check if this is an event post
const splitURL = window.parseUrl ? window.parseUrl(url) : { path: "" };
if (splitURL.path && splitURL.path.indexOf("calendar/event/show") > -1) {
addToModal(splitURL.path);
return;
}
const $modal = $("#jot-modal").modal();
const loadUrl = url + " #jot-sections";
$(".jot-nav .jot-perms-lnk").parent("li").addClass("hidden");
// Store original jot and remove it to avoid conflicts
window.jotcache = $("#jot-content > #jot-sections");
window.jotcache.detach();
$("#jot-modal").addClass("edit-jot");
// Handler is bound once in document.ready via bindJotResetOnce()
// No need to call jotreset() here anymore
$modal.find("#jot-modal-content").load(loadUrl, function (responseText, textStatus) {
if (textStatus === "success" || textStatus === "notmodified") {
const type = $(responseText).find("#profile-jot-form input[name='type']").val();
if (type === "wall-comment" || type === "remote-comment") {
$("#profile-jot-form #jot-title-wrap").hide();
$("#profile-jot-form #jot-category-wrap").hide();
}
// Setup dropzone for comment editing
if ($("#jot-text-wrap").length && typeof window.dzFactory !== "undefined") {
window.dzFactory.setupDropzone("#jot-text-wrap", "profile-jot-text");
}
$modal.show();
$("#jot-popup").show();
const $profileJotText = $("#profile-jot-text");
if ($profileJotText.length && typeof $profileJotText.linkPreview === "function") {
window.linkPreview = $profileJotText.linkPreview();
}
}
});
}
/**
* Give the active "jot-nav" list element the class "active".
*
* @param {HTMLElement} elm - The navigation link element
*/
function toggleJotNav(elm) {
const tabpanel = elm.getAttribute("aria-controls");
const isMobile = elm.classList.contains("jot-nav-lnk-mobile");
// Toggle active class
$(elm).closest("li").siblings("li").removeClass("active");
$(elm).closest("li").addClass("active");
// Toggle tab panels
$("#profile-jot-form > [role=tabpanel]")
.addClass("minimize")
.attr("aria-hidden", "true");
$("#" + tabpanel)
.removeClass("minimize")
.attr("aria-hidden", "false");
// Set aria-selected states
$("#jot-modal .modal-header .nav-tabs .jot-nav-lnk").attr("aria-selected", "false");
elm.setAttribute("aria-selected", "true");
// Handle specific tab panels
if (tabpanel === "jot-preview-content") {
if (typeof window.preview_post === "function") {
window.preview_post();
}
$("#jot-preview-share").removeClass("minimize").attr("aria-hidden", "false");
} else if (tabpanel === "jot-fbrowser-wrapper") {
window.Dialog.showJot();
}
// Update mobile dropdown button text
if (isMobile && typeof window.toggleDropdownText === "function") {
window.toggleDropdownText(elm);
}
}
/**
* Wall Message special handling - redirects to own server if needed.
*
* @param {string} url - The wall message URL
*/
function openWallMessage(url) {
const parts = window.parseUrl ? window.parseUrl(url) : {};
if (parts.host && parts.host !== window.location.host) {
window.location.href = url;
} else {
addToModal(url);
}
}
/**
* Load the content of an edit URL into a modal.
*
* @param {string} url - The event edit URL
*/
function eventEdit(url) {
const char = window.qOrAmp ? window.qOrAmp(url) : (url.indexOf("?") < 0 ? "?" : "&");
const fullUrl = url + char + "mode=none";
$.get(fullUrl, function (data) {
$("#modal-body").empty().append(data);
}).done(function () {
loadModalTitle();
});
}
// Expose functions to global scope
window.addToModal = addToModal;
window.addElmToModal = addElmToModal;
window.editpost = editpost;
window.toggleJotNav = toggleJotNav;
window.openWallMessage = openWallMessage;
window.eventEdit = eventEdit;
// jotreset is no longer needed as a public function since we bind the handler once
// But keep it for backward compatibility just in case
window.jotreset = function () {
// Handler is now bound automatically in document.ready
// This function is kept for backward compatibility
};
})(jQuery, window, document);
// @license-end