Packages
phoenix_kit
1.7.21
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/pages/editor.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="h-screen flex flex-col">
<%!-- Header --%>
<div class="border-b border-base-300 bg-base-100">
<div class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<%!-- Left: Back button and filename --%>
<div class="flex items-center gap-4">
<button
type="button"
class="btn btn-ghost btn-sm"
phx-click="attempt_cancel"
id="back-button"
>
<PhoenixKitWeb.Components.Core.Icons.icon_arrow_left class="w-4 h-4" /> Back
</button>
<div>
<h1 class="text-xl font-bold">{Path.basename(@file_path || "")}</h1>
<%= if @has_changes do %>
<p class="text-sm text-warning">Unsaved changes</p>
<% else %>
<p class="text-sm text-base-content/60">All changes saved</p>
<% end %>
</div>
</div>
<%!-- Right: Action buttons --%>
<div class="flex gap-2">
<%!-- Status Dropdown --%>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-sm gap-2">
<.page_status_badge status={@current_status} />
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</label>
<ul
tabindex="0"
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52 mt-2"
>
<li>
<a phx-click="change_status" phx-value-status="draft">
<.page_status_badge status="draft" /> Draft
</a>
</li>
<li>
<a phx-click="change_status" phx-value-status="published">
<.page_status_badge status="published" /> Published
</a>
</li>
<li>
<a phx-click="change_status" phx-value-status="archived">
<.page_status_badge status="archived" /> Archived
</a>
</li>
</ul>
</div>
<button
type="button"
class="btn btn-ghost"
phx-click="attempt_cancel"
id="cancel-button"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
phx-click="save"
disabled={!@has_changes}
>
<PhoenixKitWeb.Components.Core.Icons.icon_check class="w-4 h-4" /> Save
</button>
</div>
</div>
</div>
</div>
<%!-- Editor Area --%>
<div class="flex-1 overflow-hidden">
<form phx-change="update_content" class="h-full">
<textarea
name="value"
class="w-full h-full p-6 font-mono text-sm resize-none focus:outline-none bg-base-100"
phx-debounce="300"
placeholder="Start writing..."
>{@file_content}</textarea>
</form>
</div>
</div>
<script>
// Initialize the editor unsaved changes protection
if (!window.editorUnsavedChanges) {
window.editorUnsavedChanges = {
hasChanges: false,
isNavigating: false
};
// Handle browser navigation (back button, closing tab, URL changes)
window.addEventListener('beforeunload', function(e) {
if (window.editorUnsavedChanges.hasChanges && !window.editorUnsavedChanges.isNavigating) {
// Modern browsers require exactly this format
e.preventDefault();
// Chrome requires returnValue to be set
e.returnValue = '';
// Some browsers show the return value, others show a default message
return '';
}
});
// Intercept LiveView navigation links
document.addEventListener('click', function(e) {
if (!window.editorUnsavedChanges.hasChanges || window.editorUnsavedChanges.isNavigating) return;
// Check if the clicked element or any parent is a link
let target = e.target;
while (target && target !== document) {
// Check for Phoenix LiveView links (nav menu, etc.)
if (target.hasAttribute && (
target.hasAttribute('data-phx-link') ||
target.hasAttribute('data-phx-link-state') ||
(target.tagName === 'A' && target.hasAttribute('href'))
)) {
// Found a navigation link, check if it's trying to leave the editor
const href = target.getAttribute('href') || target.getAttribute('data-phx-link');
// If the link is navigating away from the current page
if (href && !href.includes(window.location.pathname)) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (confirm('You have unsaved changes. Are you sure you want to leave without saving?')) {
// User confirmed, allow navigation by triggering a new click
window.editorUnsavedChanges.isNavigating = true;
window.editorUnsavedChanges.hasChanges = false;
target.click(); // Re-trigger the click
}
return false;
}
}
target = target.parentElement;
}
}, true); // Use capture phase to intercept before LiveView
}
// Reset state when editor mounts
window.editorUnsavedChanges.hasChanges = false;
window.editorUnsavedChanges.isNavigating = false;
</script>
<script>
// Listen for changes status from LiveView
window.addEventListener('phx:changes-status', function(event) {
window.editorUnsavedChanges.hasChanges = event.detail.has_changes;
});
// Listen for confirmation request from LiveView
window.addEventListener('phx:confirm-navigation', function(event) {
console.log("Confirm navigation event received:", event.detail);
const message = event.detail.message;
if (confirm(message)) {
console.log("User confirmed, navigating...");
// User confirmed, mark as navigating and trigger cancel event
window.editorUnsavedChanges.isNavigating = true;
window.editorUnsavedChanges.hasChanges = false;
// Trigger the hidden button to navigate
const hiddenCancelButton = document.getElementById('confirmed-cancel-button');
if (hiddenCancelButton) {
hiddenCancelButton.click();
}
} else {
console.log("User cancelled navigation");
}
});
</script>
<%!-- Hidden button for confirmed cancellation --%>
<button
id="confirmed-cancel-button"
phx-click="cancel"
style="display: none;"
aria-hidden="true"
>
</button>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>