Skip to content
Merged
63 changes: 63 additions & 0 deletions app/assets/javascripts/stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,71 @@ document.addEventListener("DOMContentLoaded", () => {
debounceTimer = window.setTimeout(updateMarkdown, 300);
});
});

const form = document.querySelector(".edit_story");
const backButton = document.getElementById("back");
const logo = document.getElementById("logo");
let isDirty = false;

if (form) {
// Snapshot the initial form state so we can compare against it. Tracking a
// plain "changed at least once" flag reported the form as dirty even after
// the user undid their edits (e.g. typed some text and then deleted it).
const initialState = serializeForm(form);

const refreshDirtyState = function () {
isDirty = serializeForm(form) !== initialState;
addBeforeUnloadEventListener(isDirty);
};

// "input" covers text fields; "change" covers selects like the status.
form.addEventListener("input", refreshDirtyState);
form.addEventListener("change", refreshDirtyState);

// Reset isDirty on form submission
form.addEventListener("submit", function () {
isDirty = false;
addBeforeUnloadEventListener(isDirty);
});
}

// Attach a click event to the custom back button
[backButton, logo].forEach(element => {
if (!element) return;

element.addEventListener("click", function (event) {
if (isDirty) {
const confirmLeave = confirm("You have unsaved changes. Are you sure you want to go back?");
if (confirmLeave) {
// Optionally, reset isDirty if leaving
isDirty = false;
addBeforeUnloadEventListener(isDirty)
} else {
// Prevent navigation if the user chooses not to leave
event.preventDefault();
}
}
})
});
});

function serializeForm(form) {
return new URLSearchParams(new FormData(form)).toString();
}

function addBeforeUnloadEventListener(isDirty) {
if (isDirty) {
window.addEventListener("beforeunload", warnUserIfUnsavedEdits);
} else {
window.removeEventListener("beforeunload", warnUserIfUnsavedEdits);
}
}

function warnUserIfUnsavedEdits(event) {
event.preventDefault();
event.returnValue = '';
}

function updateStatusButton(color, status) {
const button = document.querySelector(".story-title .dropdown-wrapper > button");
button.className = `button ${color}`;
Expand Down
40 changes: 40 additions & 0 deletions spec/features/stories_manage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,46 @@
expect(page).to have_content "Story updated!"
end

it "alerts me when I try to navigate away from the page without saving my edits", js: true do
visit project_path(id: project.id)
click_button "More actions"
click_link "Edit"

click_link "Back"
assert_current_path project_path(id: project.id)

click_button "More actions"
click_link "Edit"

fill_in "story[title]", with: "As a user, I want to edit stories"

dismiss_confirm("You have unsaved changes. Are you sure you want to go back?") do
find("#logo").click
end

assert_current_path edit_project_story_path(project, story)

accept_confirm("You have unsaved changes. Are you sure you want to go back?") do
click_link "Back"
end

assert_current_path project_path(id: project.id)
end

it "does not alert me when I revert my edits back to their original values", js: true do
visit edit_project_story_path(project, story)
original_title = find_field("story[title]").value

# Make a change and then undo it, returning the form to its initial state.
fill_in "story[title]", with: "A temporary edit"
fill_in "story[title]", with: original_title

# No unsaved-changes confirm should appear, so navigation happens directly.
click_link "Back"

assert_current_path project_path(id: project.id)
end

it "allows me to delete a story" do
visit project_path(id: project.id)

Expand Down