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
priv/static/assets/phoenix_kit_markdown_editor.js
/**
* PhoenixKit Markdown Editor JavaScript
*
* This file provides the JavaScript functionality for the MarkdownEditor
* LiveComponent. It must be loaded once in your application for the editor
* to work correctly with LiveView navigation.
*
* Include in your app.js:
* import "../../../deps/phoenix_kit/priv/static/assets/phoenix_kit_markdown_editor.js"
*
* Or via script tag in your layout:
* <script src={~p"/assets/phoenix_kit_markdown_editor.js"}></script>
*/
(function () {
// Prevent multiple initializations
if (window.PhoenixKitMarkdownEditor) return;
window.PhoenixKitMarkdownEditor = true;
// Global state for all markdown editors
window.markdownEditors = window.markdownEditors || {};
/**
* Initialize a single markdown editor element
* @param {HTMLElement} editorEl - The editor container element
* @param {number} attempt - Retry attempt number
*/
window.initMarkdownEditor = function (editorEl, attempt) {
attempt = attempt || 0;
var maxAttempts = 20;
var editorId = editorEl.id;
var globalId = editorEl.dataset.globalId;
var textareaId = editorId + "-textarea";
var protectNavigation = editorEl.dataset.protectNavigation === "true";
var textarea = document.getElementById(textareaId);
var warningEl = document.getElementById(editorId + "-js-warning");
if (!textarea) {
if (attempt >= maxAttempts) {
console.error(
"[MarkdownEditor] Failed to initialize:",
editorId,
"- textarea not found after",
maxAttempts,
"attempts"
);
// Show warning on failure
if (warningEl) warningEl.classList.remove("hidden");
return;
}
// Textarea not ready yet, retry shortly
setTimeout(function () {
window.initMarkdownEditor(editorEl, attempt + 1);
}, 50);
return;
}
// Check if already initialized with THIS EXACT textarea element
var existing = window.markdownEditors[editorId];
if (existing && existing.textarea === textarea && existing.initialized) {
return;
}
// Clear any stale state
if (existing) {
// Editor was re-mounted, re-initialize
}
// Store state in namespaced object
var state = {
textarea: textarea,
lastCursorPosition: 0,
hasUnsavedChanges: false,
initialized: true,
};
window.markdownEditors[editorId] = state;
// Setup cursor tracking
var events = ["blur", "select", "click", "keyup"];
events.forEach(function (event) {
textarea.addEventListener(event, function () {
var s = window.markdownEditors[editorId];
if (s) s.lastCursorPosition = textarea.selectionStart;
});
});
// Auto-continue lists on Enter
textarea.addEventListener("keydown", function (e) {
if (e.key !== "Enter") return;
var pos = textarea.selectionStart;
var value = textarea.value;
// Find current line
var lineStart = value.lastIndexOf("\n", pos - 1) + 1;
var lineEnd = value.indexOf("\n", pos);
var currentLine = value.substring(
lineStart,
lineEnd === -1 ? value.length : lineEnd
);
// Check if cursor is at end of line
var cursorInLine = pos - lineStart;
if (cursorInLine < currentLine.length) return; // Cursor not at end, let default happen
// Match bullet list: " - text" or " * text" or " + text"
var bulletMatch = currentLine.match(/^(\s*)(-|\*|\+)\s(.*)$/);
if (bulletMatch) {
e.preventDefault();
var indent = bulletMatch[1];
var marker = bulletMatch[2];
var content = bulletMatch[3];
if (content.trim() === "") {
// Empty list item - remove marker
var newValue = value.substring(0, lineStart) + value.substring(pos);
textarea.value = newValue;
textarea.selectionStart = textarea.selectionEnd = lineStart;
} else {
// Continue list
var insertion = "\n" + indent + marker + " ";
var newValue =
value.substring(0, pos) + insertion + value.substring(pos);
textarea.value = newValue;
textarea.selectionStart = textarea.selectionEnd =
pos + insertion.length;
}
var s = window.markdownEditors[editorId];
if (s) s.lastCursorPosition = textarea.selectionStart;
textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
return;
}
// Match numbered list: " 1. text"
var numberMatch = currentLine.match(/^(\s*)(\d+)\.\s(.*)$/);
if (numberMatch) {
e.preventDefault();
var indent = numberMatch[1];
var num = numberMatch[2];
var content = numberMatch[3];
if (content.trim() === "") {
// Empty list item - remove marker
var newValue = value.substring(0, lineStart) + value.substring(pos);
textarea.value = newValue;
textarea.selectionStart = textarea.selectionEnd = lineStart;
} else {
// Continue with next number
var nextNum = parseInt(num, 10) + 1;
var insertion = "\n" + indent + nextNum + ". ";
var newValue =
value.substring(0, pos) + insertion + value.substring(pos);
textarea.value = newValue;
textarea.selectionStart = textarea.selectionEnd =
pos + insertion.length;
}
var s = window.markdownEditors[editorId];
if (s) s.lastCursorPosition = textarea.selectionStart;
textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
return;
}
});
// Register global insert function
window["markdownEditorInsert_" + globalId] = function (text) {
var s = window.markdownEditors[editorId];
if (!s || !s.textarea) return;
var start = s.lastCursorPosition || 0;
var currentValue = s.textarea.value;
var newValue =
currentValue.substring(0, start) +
text +
currentValue.substring(start);
s.textarea.value = newValue;
var newCursorPos = start + text.length;
s.textarea.selectionStart = s.textarea.selectionEnd = newCursorPos;
s.lastCursorPosition = newCursorPos;
s.textarea.focus();
s.textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
};
// Register global format function (wraps selection)
window["markdownFormat_" + globalId] = function (prefix, suffix) {
var s = window.markdownEditors[editorId];
if (!s || !s.textarea) return;
var start = s.textarea.selectionStart;
var end = s.textarea.selectionEnd;
var selected = s.textarea.value.substring(start, end);
var before = s.textarea.value.substring(0, start);
var after = s.textarea.value.substring(end);
if (selected.length > 0) {
s.textarea.value = before + prefix + selected + suffix + after;
s.textarea.selectionStart = start + prefix.length;
s.textarea.selectionEnd = end + prefix.length;
} else {
var placeholder = "text";
s.textarea.value = before + prefix + placeholder + suffix + after;
s.textarea.selectionStart = start + prefix.length;
s.textarea.selectionEnd = start + prefix.length + placeholder.length;
}
s.textarea.focus();
s.textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
s.lastCursorPosition = s.textarea.selectionEnd;
};
// Register global line prefix function (adds prefix to line)
window["markdownLinePrefix_" + globalId] = function (prefix) {
var s = window.markdownEditors[editorId];
if (!s || !s.textarea) return;
var start = s.textarea.selectionStart;
var value = s.textarea.value;
var lineStart = value.lastIndexOf("\n", start - 1) + 1;
var before = value.substring(0, lineStart);
var after = value.substring(lineStart);
s.textarea.value = before + prefix + after;
var newPos = start + prefix.length;
s.textarea.selectionStart = s.textarea.selectionEnd = newPos;
s.textarea.focus();
s.textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
s.lastCursorPosition = newPos;
};
// Register global link function
window["markdownLink_" + globalId] = function () {
var s = window.markdownEditors[editorId];
if (!s || !s.textarea) return;
var url = prompt("Enter URL:");
if (!url || !url.trim()) return;
var start = s.textarea.selectionStart;
var end = s.textarea.selectionEnd;
var selected = s.textarea.value.substring(start, end);
var linkText = selected.length > 0 ? selected : "link text";
var before = s.textarea.value.substring(0, start);
var after = s.textarea.value.substring(end);
s.textarea.value =
before + "[" + linkText + "](" + url.trim() + ")" + after;
var newPos = start + linkText.length + url.trim().length + 4;
s.textarea.selectionStart = s.textarea.selectionEnd = newPos;
s.textarea.focus();
s.textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
s.lastCursorPosition = newPos;
};
// Browser exit protection
if (
protectNavigation &&
!window["markdownEditorBeforeUnload_" + globalId]
) {
window["markdownEditorBeforeUnload_" + globalId] = true;
window.addEventListener("beforeunload", function (e) {
var s = window.markdownEditors && window.markdownEditors[editorId];
if (s && s.hasUnsavedChanges) {
e.preventDefault();
e.returnValue = "";
return "";
}
});
}
};
/**
* Initialize all markdown editors on the page
*/
function initAllEditors() {
var editors = document.querySelectorAll('[data-markdown-editor="true"]');
editors.forEach(window.initMarkdownEditor);
}
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initAllEditors);
} else {
initAllEditors();
}
// Re-initialize on LiveView navigation
window.addEventListener("phx:page-loading-stop", function () {
// Staggered initialization to handle DOM morphing timing
setTimeout(initAllEditors, 50);
setTimeout(initAllEditors, 150);
});
// Watch for new editors added to DOM (LiveView navigation)
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is an editor
if (node.dataset && node.dataset.markdownEditor === "true") {
window.initMarkdownEditor(node);
}
// Check children for editors
if (node.querySelectorAll) {
node
.querySelectorAll('[data-markdown-editor="true"]')
.forEach(window.initMarkdownEditor);
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
// Listen for insert events from LiveView
window.addEventListener("phx:markdown-editor-insert", function (e) {
var globalId = e.detail.global_id;
var text = e.detail.text;
var insertFn = window["markdownEditorInsert_" + globalId];
if (insertFn) {
insertFn(text);
}
});
})();