Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,51 @@ <h3 class="modal-section-title">Open-source credits</h3>
</div>
</div>

<!-- Share Modal -->
<div id="share-modal" class="reset-modal-overlay modal-overlay" role="dialog" aria-modal="true" aria-labelledby="share-modal-title" aria-hidden="true" style="display:none;">
<div class="reset-modal-box reset-modal-box--wide modal-box">
<div class="modal-header">
<p id="share-modal-title" class="reset-modal-message">Share Document</p>
<button type="button" class="modal-close-btn" id="share-modal-close-icon" aria-label="Close share dialog">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="modal-body">
<p class="share-modal-description">Choose how recipients can interact with this document.</p>
<div class="share-mode-cards">
<label class="share-mode-card" id="share-card-view" for="share-mode-view">
<input type="radio" id="share-mode-view" name="share-mode" value="view" checked />
<span class="share-card-icon"><i class="bi bi-eye"></i></span>
<span class="share-card-body">
<span class="share-card-title">View only</span>
<span class="share-card-desc">Opens in preview mode. The editor is hidden.</span>
</span>
<span class="share-card-check"><i class="bi bi-check-lg"></i></span>
</label>
<label class="share-mode-card" id="share-card-edit" for="share-mode-edit">
<input type="radio" id="share-mode-edit" name="share-mode" value="edit" />
<span class="share-card-icon"><i class="bi bi-pencil-square"></i></span>
<span class="share-card-body">
<span class="share-card-title">Edit</span>
<span class="share-card-desc">Opens in split editor + preview mode.</span>
</span>
<span class="share-card-check"><i class="bi bi-check-lg"></i></span>
</label>
</div>
<div class="share-url-row">
<input type="text" id="share-url-input" class="rename-modal-input share-url-input" readonly placeholder="Generating link…" aria-label="Share URL" />
<button class="reset-modal-btn share-copy-btn" id="share-copy-btn" title="Copy link">
<i class="bi bi-clipboard"></i>
</button>
</div>
<p class="share-modal-notice"><i class="bi bi-info-circle"></i> The entire document is encoded in the URL. No data is sent to any server.</p>
</div>
<div class="reset-modal-actions">
<button class="reset-modal-btn reset-modal-cancel" id="share-modal-close">Close</button>
</div>
</div>
</div>

Comment on lines +484 to +528
<!-- Rename Modal -->
<div id="rename-modal" class="reset-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="rename-modal-title" style="display:none;">
<div class="reset-modal-box reset-modal-box--wide">
Expand Down Expand Up @@ -717,4 +762,4 @@ <h3 class="modal-section-title">Open-source credits</h3>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script>
<script src="script.js"></script>
</body>
</html>
</html>
150 changes: 120 additions & 30 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,10 @@ document.addEventListener("DOMContentLoaded", function () {
return markdown;
}
resetExtendedMarkdownState();
return applyFootnotes(extractFootnoteDefinitions(markdown));
// ✅ Replace escaped dollar signs before marked.js strips the backslash.
// This prevents MathJax from treating lone $ as a math delimiter.
const protectedMarkdown = markdown.replace(/\\\$/g, '&#36;');
Comment on lines +669 to +670
return applyFootnotes(extractFootnoteDefinitions(protectedMarkdown));
},
},
});
Expand Down Expand Up @@ -5009,64 +5012,151 @@ This is a fully client-side application. Your content never leaves your browser
return new TextDecoder().decode(pako.inflate(bytes));
}

function copyShareUrl(btn) {
// ============================================
// Share Modal
// ============================================

const shareModal = document.getElementById('share-modal');
const shareModalCloseX = document.getElementById('share-modal-close-icon');
const shareModalClose = document.getElementById('share-modal-close');
const shareUrlInput = document.getElementById('share-url-input');
const shareCopyBtn = document.getElementById('share-copy-btn');
const shareModeView = document.getElementById('share-mode-view');
const shareModeEdit = document.getElementById('share-mode-edit');
const shareCardView = document.getElementById('share-card-view');
const shareCardEdit = document.getElementById('share-card-edit');

function buildShareUrl(mode) {
const markdownText = markdownEditor.value;
let encoded;
try {
encoded = encodeMarkdownForShare(markdownText);
} catch (e) {
console.error("Share encoding failed:", e);
alert("Failed to encode content for sharing: " + e.message);
console.error('Share encoding failed:', e);
return null;
}
// mode=view → #share=<encoded> (opens preview-only)
// mode=edit → #share=<encoded>&edit=1
const base = window.location.origin + window.location.pathname + '#share=' + encoded;
return mode === 'edit' ? base + '&edit=1' : base;
}

function updateShareUrlField() {
const mode = shareModeView.checked ? 'view' : 'edit';
const url = buildShareUrl(mode);
if (!url) {
shareUrlInput.value = 'Error generating link.';
shareCopyBtn.disabled = true;
return;
}
const tooLarge = url.length > MAX_SHARE_URL_LENGTH;
if (tooLarge) {
shareUrlInput.value = 'Document too large to share via URL.';
shareCopyBtn.disabled = true;
} else {
shareUrlInput.value = url;
shareCopyBtn.disabled = false;
}
}

const shareUrl = window.location.origin + window.location.pathname + '#share=' + encoded;
const tooLarge = shareUrl.length > MAX_SHARE_URL_LENGTH;
function openShareModal() {
// Reset to view-only by default each time
shareModeView.checked = true;
syncShareCardStyles();
updateShareUrlField();
shareModal.style.display = '';
requestAnimationFrame(() => {
shareModal.classList.add('is-visible');
shareModal.setAttribute('aria-hidden', 'false');
});
}

const originalHTML = btn.innerHTML;
const copiedHTML = '<i class="bi bi-check-lg"></i> Copied!';
function closeShareModal() {
shareModal.classList.remove('is-visible');
shareModal.setAttribute('aria-hidden', 'true');
shareModal.addEventListener('transitionend', function handler() {
shareModal.style.display = 'none';
shareModal.removeEventListener('transitionend', handler);
});
}
Comment on lines +5062 to +5081

function syncShareCardStyles() {
if (shareModeView.checked) {
shareCardView.classList.add('is-selected');
shareCardEdit.classList.remove('is-selected');
} else {
shareCardEdit.classList.add('is-selected');
shareCardView.classList.remove('is-selected');
}
}

shareModeView.addEventListener('change', function () {
syncShareCardStyles();
updateShareUrlField();
});
shareModeEdit.addEventListener('change', function () {
syncShareCardStyles();
updateShareUrlField();
});

shareCopyBtn.addEventListener('click', function () {
const url = shareUrlInput.value;
if (!url || shareCopyBtn.disabled) return;

function onCopied() {
if (!tooLarge) {
window.location.hash = 'share=' + encoded;
}
btn.innerHTML = copiedHTML;
setTimeout(() => { btn.innerHTML = originalHTML; }, 2000);
const orig = shareCopyBtn.innerHTML;
shareCopyBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
setTimeout(() => { shareCopyBtn.innerHTML = orig; }, 2000);
}

if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(shareUrl).then(onCopied).catch(() => {
// clipboard.writeText failed; nothing further to do in secure context
});
navigator.clipboard.writeText(url).then(onCopied).catch(() => {});
} else {
try {
const tempInput = document.createElement("textarea");
tempInput.value = shareUrl;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand("copy");
document.body.removeChild(tempInput);
const tmp = document.createElement('textarea');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
onCopied();
} catch (_) {
// copy failed silently
}
} catch (_) {}
}
}
});

shareModalCloseX.addEventListener('click', closeShareModal);
shareModalClose.addEventListener('click', closeShareModal);
shareModal.addEventListener('click', function (e) {
if (e.target === shareModal) closeShareModal();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && shareModal.classList.contains('is-visible')) closeShareModal();
});

shareButton.addEventListener("click", function () { copyShareUrl(shareButton); });
mobileShareButton.addEventListener("click", function () { copyShareUrl(mobileShareButton); });
shareButton.addEventListener('click', openShareModal);
mobileShareButton.addEventListener('click', openShareModal);

function loadFromShareHash() {
if (typeof pako === 'undefined') return;
const hash = window.location.hash;
if (!hash.startsWith('#share=')) return;
const encoded = hash.slice('#share='.length);

// Parse encoded content and optional &edit=1 flag.
// Hash format: #share=<encoded> or #share=<encoded>&edit=1
const rest = hash.slice('#share='.length);
const ampIdx = rest.indexOf('&');
const encoded = ampIdx === -1 ? rest : rest.slice(0, ampIdx);
const params = ampIdx === -1 ? '' : rest.slice(ampIdx + 1);
const isEdit = params.split('&').includes('edit=1');

if (!encoded) return;
try {
const decoded = decodeMarkdownFromShare(encoded);
markdownEditor.value = decoded;
renderMarkdown();
saveCurrentTabState();
// Apply the correct view mode: edit=1 → split, default → preview only
setViewMode(isEdit ? 'split' : 'preview');
} catch (e) {
console.error("Failed to load shared content:", e);
alert("The shared URL could not be decoded. It may be corrupted or incomplete.");
Expand Down Expand Up @@ -5495,4 +5585,4 @@ This is a fully client-side application. Your content never leaves your browser
container.appendChild(toolbar);
});
}
});
});
122 changes: 122 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2686,3 +2686,125 @@ a:focus {
border-left: 0;
border-right: 0.25em solid currentColor;
}

/* ============================================
SHARE MODAL
============================================ */

.share-modal-description {
font-size: 13px;
color: var(--text-secondary, #57606a);
margin: 0;
}

.share-mode-cards {
display: flex;
flex-direction: column;
gap: 8px;
}

.share-mode-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg-color);
cursor: pointer;
transition: border-color 0.15s ease, background-color 0.15s ease;
user-select: none;
}

.share-mode-card:hover {
border-color: var(--accent-color);
background: var(--button-hover);
}

.share-mode-card.is-selected {
border-color: var(--accent-color);
background: color-mix(in srgb, var(--accent-color) 8%, transparent);
}

.share-mode-card input[type="radio"] {
display: none;
Comment on lines +2724 to +2730
}

.share-card-icon {
font-size: 18px;
width: 28px;
text-align: center;
color: var(--text-secondary, #57606a);
flex-shrink: 0;
}

.share-mode-card.is-selected .share-card-icon {
color: var(--accent-color);
}

.share-card-body {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}

.share-card-title {
font-size: 13px;
font-weight: 600;
color: var(--text-color);
}

.share-card-desc {
font-size: 12px;
color: var(--text-secondary, #57606a);
}

.share-card-check {
width: 18px;
text-align: center;
color: var(--accent-color);
opacity: 0;
transition: opacity 0.15s ease;
flex-shrink: 0;
}

.share-mode-card.is-selected .share-card-check {
opacity: 1;
}

.share-url-row {
display: flex;
gap: 8px;
align-items: center;
}

.share-url-input {
flex: 1;
font-size: 12px;
font-family: var(--font-mono, monospace);
color: var(--text-secondary, #57606a);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.share-copy-btn {
flex-shrink: 0;
padding: 6px 10px;
}

.share-copy-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.share-modal-notice {
font-size: 11px;
color: var(--text-secondary, #57606a);
margin: 0;
display: flex;
align-items: center;
gap: 5px;
}
Loading