compose.js: - Add draft autosave to sessionStorage (body, location, scheduled_at) - Restore draft when returning to compose page - Clear form after successful post submission - Fix geolocation operator precedence bug - Fix button staying disabled after geolocation error - Add sessionStorage availability check for private browsing - Wrap in IIFE with 'use strict' - Add JSDoc documentation - Use namespaced events (.compose) modal.js: - Fix critical double event registration bug in editpost() - Add isJotResetBound flag to ensure single handler binding - Add isEditJotClosing guard for race condition protection - Wrap in IIFE with 'use strict' - Add feature detection for all external dependencies - Use namespaced events (.frio, .frio-edit) - Add JSDoc documentation
376 lines
9.7 KiB
JavaScript
376 lines
9.7 KiB
JavaScript
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPLv3-or-later
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
|
|
(function ($, window, document) {
|
|
"use strict";
|
|
|
|
// 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();
|
|
});
|
|
|
|
// 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");
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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
|