Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save minanagehsalalma/63e956ab9f402261bd618375b27f5899 to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/63e956ab9f402261bd618375b27f5899 to your computer and use it in GitHub Desktop.
Milanote Board to Markdown — right-click context menu + header button to copy board elements as Markdown
// ==UserScript==
// @name Milanote Board to Markdown
// @namespace tweeks.io
// @version 1.5.0
// @description Injects "Copy as Markdown" into Milanote's right-click context menu, with element picker and header button fallback
// @author Tweeks
// @match https://app.milanote.com/*
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// ==/UserScript==
(function () {
'use strict';
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
function getBoardTitle() {
const el = document.querySelector('.CurrentBoardHeaderTitle .HighlightedText');
if (el) return el.textContent.trim();
const titleEl = document.querySelector('.editable-title .HighlightedText');
if (titleEl) return titleEl.textContent.trim();
return document.title.replace(' - Milanote', '').trim() || 'Milanote Board';
}
function tiptapToMarkdown(container) {
if (!container) return '';
const lines = [];
function processNode(node) {
const tag = node.nodeName.toLowerCase();
if (tag === 'p') { const t = node.textContent.trim(); lines.push(t || ''); return; }
if (tag === 'h1') { lines.push('# ' + node.textContent.trim()); return; }
if (tag === 'h2') { lines.push('## ' + node.textContent.trim()); return; }
if (tag === 'h3') { lines.push('### ' + node.textContent.trim()); return; }
if (tag === 'h4') { lines.push('#### ' + node.textContent.trim()); return; }
if (tag === 'ul') {
node.querySelectorAll(':scope > li').forEach(li => {
const cb = li.querySelector('input[type="checkbox"]');
if (cb) lines.push('- ' + (cb.checked ? '[x]' : '[ ]') + ' ' + li.textContent.trim());
else { const t = li.textContent.trim(); if (t) lines.push('- ' + t); }
});
return;
}
if (tag === 'ol') {
node.querySelectorAll(':scope > li').forEach((li, idx) => {
const t = li.textContent.trim(); if (t) lines.push((idx + 1) + '. ' + t);
});
return;
}
if (tag === 'blockquote') { const t = node.textContent.trim(); if (t) lines.push('> ' + t); return; }
if (tag === 'hr') { lines.push('---'); return; }
node.childNodes.forEach(child => { if (child.nodeType === Node.ELEMENT_NODE) processNode(child); });
}
container.childNodes.forEach(child => { if (child.nodeType === Node.ELEMENT_NODE) processNode(child); });
return lines.join('\n').trim();
}
function draftEditorToText(container) {
if (!container) return '';
const blocks = container.querySelectorAll('[data-block="true"]');
if (blocks.length === 0) return container.textContent.trim();
return Array.from(blocks).map(b => b.textContent.trim()).join(' ');
}
// Extract markdown from a table cell — preserves links inside cells
function cellToMarkdown(td) {
// Check for anchor links inside the cell
const anchors = td.querySelectorAll('a[href]');
if (anchors.length > 0) {
// Build a text that replaces each anchor with [text](href)
let text = td.innerHTML;
anchors.forEach(a => {
const href = a.href || a.getAttribute('href') || '';
const label = a.textContent.trim();
if (href && label) {
// Replace the outer HTML of the anchor with markdown link
text = text.replace(a.outerHTML, '[' + label + '](' + href + ')');
}
});
// Strip remaining HTML tags
const tmp = document.createElement('div');
tmp.innerHTML = text;
return tmp.textContent.trim().replace(/\|/g, '\\|');
}
// DraftJS editor
const editor = td.querySelector('.public-DraftEditor-content');
if (editor) return draftEditorToText(editor).replace(/\|/g, '\\|');
return td.textContent.trim().replace(/\|/g, '\\|');
}
function getPosition(el) {
return { top: parseInt(el.style.top) || 0, left: parseInt(el.style.left) || 0 };
}
// -------------------------------------------------------------------------
// Element converters
// -------------------------------------------------------------------------
function convertCard(el) {
const tiptap = el.querySelector('.CardTiptapEditor .tiptap');
if (!tiptap) return null;
return tiptapToMarkdown(tiptap) || null;
}
function convertLink(el) {
const urlEl = el.querySelector('.LinkURL');
const titleEl = el.querySelector('.LinkMetadata .HighlightedText, .LinkMetadata .title-text, .EditableTitle .HighlightedText');
const captionEl = el.querySelector('.Caption .tiptap, .Caption .ProseMirror');
const url = urlEl ? (urlEl.href || urlEl.textContent.trim()) : '';
const rawTitle = titleEl ? titleEl.textContent.trim() : '';
const title = rawTitle || (urlEl ? urlEl.textContent.trim() : url);
const caption = captionEl ? captionEl.textContent.trim() : '';
if (!url) return null;
let md = (title && title !== url) ? '[' + title + '](' + url + ')' : '<' + url + '>';
if (caption) md += '\n\n> ' + caption;
return md;
}
function convertTable(el) {
const table = el.querySelector('table.htCore');
if (!table) return null;
const tbody = table.querySelector('tbody');
if (!tbody) return null;
const rows = [];
tbody.querySelectorAll('tr').forEach(tr => {
rows.push(Array.from(tr.querySelectorAll('td')).map(td => cellToMarkdown(td)));
});
if (rows.length === 0) return null;
const colCount = Math.max(...rows.map(r => r.length));
const pad = (row) => Array.from({ length: colCount }, (_, i) => row[i] || '');
return [
'| ' + pad(rows[0]).join(' | ') + ' |',
'| ' + pad(rows[0]).map(() => '---').join(' | ') + ' |',
...rows.slice(1).map(r => '| ' + pad(r).join(' | ') + ' |'),
].join('\n');
}
// -------------------------------------------------------------------------
// Collect + convert
// -------------------------------------------------------------------------
function collectElements() {
const canvasElements = Array.from(document.querySelectorAll('.CanvasElement'));
canvasElements.sort((a, b) => {
const pa = getPosition(a), pb = getPosition(b);
const rowDiff = pa.top - pb.top;
return Math.abs(rowDiff) > 60 ? rowDiff : pa.left - pb.left;
});
const items = [];
canvasElements.forEach((canvasEl, idx) => {
const card = canvasEl.querySelector('.Card.element-instance');
const link = canvasEl.querySelector('.Link.element-instance');
const table = canvasEl.querySelector('.Table.element-instance');
let type = null, md = null;
if (card) { type = 'card'; md = convertCard(card); }
else if (link) { type = 'link'; md = convertLink(link); }
else if (table) { type = 'table'; md = convertTable(table); }
if (!md) return;
const firstLine = md.split('\n').find(l => l.trim()) || '';
const preview = firstLine.length > 80 ? firstLine.slice(0, 78) + '…' : firstLine;
items.push({ idx, type, md, preview });
});
return items;
}
function itemsToMarkdown(items) {
const title = getBoardTitle();
const sections = ['# ' + title, ''];
items.forEach(item => { sections.push(item.md); sections.push(''); });
return sections.join('\n').trim();
}
function copyAllNow() {
const items = collectElements();
if (!items.length) { showToast('No copyable content found.', true); return; }
const md = itemsToMarkdown(items);
try { GM_setClipboard(md); }
catch (e) { navigator.clipboard.writeText(md).catch(() => {}); }
showToast('Copied all ' + items.length + ' elements as Markdown!', false);
}
// -------------------------------------------------------------------------
// Toast
// -------------------------------------------------------------------------
function showToast(message, isError) {
const existing = document.getElementById('milanote-md-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'milanote-md-toast';
toast.textContent = message;
Object.assign(toast.style, {
position: 'fixed', bottom: '24px', right: '24px',
background: isError ? '#c0392b' : '#1B2536', color: '#fff',
padding: '10px 18px', borderRadius: '6px', fontSize: '14px',
fontFamily: 'Inter, sans-serif', zIndex: '999999',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)', opacity: '1',
transition: 'opacity 0.3s ease',
});
document.body.appendChild(toast);
setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 350); }, 2500);
}
// -------------------------------------------------------------------------
// Picker Modal
// -------------------------------------------------------------------------
const TYPE_ICONS = { card: '📝', link: '🔗', table: '📊' };
function showPickerModal(items) {
const existing = document.getElementById('milanote-md-picker');
if (existing) existing.remove();
if (items.length === 0) { showToast('No copyable content found on the board.', true); return; }
const checked = {};
items.forEach(item => { checked[item.idx] = true; });
const overlay = document.createElement('div');
overlay.id = 'milanote-md-picker';
Object.assign(overlay.style, {
position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.5)',
zIndex: '999998', display: 'flex', alignItems: 'center',
justifyContent: 'center', fontFamily: 'Inter, sans-serif',
});
const modal = document.createElement('div');
Object.assign(modal.style, {
background: '#fff', borderRadius: '12px',
width: '560px', maxWidth: '92vw', maxHeight: '82vh',
display: 'flex', flexDirection: 'column',
boxShadow: '0 12px 40px rgba(0,0,0,0.25)', overflow: 'hidden',
});
// Header
const header = document.createElement('div');
Object.assign(header.style, {
padding: '16px 20px 12px', borderBottom: '1px solid #eee',
display: 'flex', flexDirection: 'column', gap: '10px',
});
const titleRow = document.createElement('div');
Object.assign(titleRow.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const titleEl = document.createElement('div');
titleEl.textContent = 'Select elements to copy';
Object.assign(titleEl.style, { fontSize: '15px', fontWeight: '600', color: '#1B2536' });
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '&#x2715;';
Object.assign(closeBtn.style, { background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px', color: '#888', padding: '0 2px' });
closeBtn.addEventListener('click', () => overlay.remove());
titleRow.appendChild(titleEl); titleRow.appendChild(closeBtn);
const controlRow = document.createElement('div');
Object.assign(controlRow.style, { display: 'flex', gap: '8px', alignItems: 'center' });
const countLabel = document.createElement('span');
Object.assign(countLabel.style, { fontSize: '12px', color: '#888', flex: '1' });
function updateCount() {
const n = items.filter(i => checked[i.idx]).length;
countLabel.textContent = n + ' of ' + items.length + ' selected';
}
updateCount();
function makeBtn(label) {
const b = document.createElement('button');
b.textContent = label;
Object.assign(b.style, { fontSize: '11px', padding: '3px 9px', borderRadius: '5px', border: '1px solid #ddd', background: '#f5f5f5', cursor: 'pointer', color: '#444' });
return b;
}
const selAll = makeBtn('Select all');
const selNone = makeBtn('Select none');
selAll.addEventListener('click', () => { items.forEach(i => { checked[i.idx] = true; }); refreshList(); updateCount(); });
selNone.addEventListener('click', () => { items.forEach(i => { checked[i.idx] = false; }); refreshList(); updateCount(); });
controlRow.appendChild(countLabel); controlRow.appendChild(selAll); controlRow.appendChild(selNone);
header.appendChild(titleRow); header.appendChild(controlRow);
// List
const list = document.createElement('div');
Object.assign(list.style, { overflowY: 'auto', flex: '1', padding: '8px 12px' });
function refreshList() {
list.innerHTML = '';
items.forEach(item => {
const row = document.createElement('label');
Object.assign(row.style, { display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '8px', borderRadius: '7px', cursor: 'pointer', marginBottom: '2px' });
row.addEventListener('mouseenter', () => { row.style.background = '#f4f6fa'; });
row.addEventListener('mouseleave', () => { row.style.background = 'transparent'; });
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.checked = checked[item.idx];
Object.assign(cb.style, { marginTop: '2px', flexShrink: '0', cursor: 'pointer', accentColor: '#1B2536' });
cb.addEventListener('change', () => { checked[item.idx] = cb.checked; updateCount(); });
const icon = document.createElement('span');
icon.textContent = TYPE_ICONS[item.type] || '•';
Object.assign(icon.style, { fontSize: '14px', flexShrink: '0', marginTop: '1px' });
const text = document.createElement('span');
Object.assign(text.style, { fontSize: '13px', color: '#2c3e50', lineHeight: '1.4', flex: '1', wordBreak: 'break-word' });
const badge = document.createElement('span');
badge.textContent = item.type + ' ';
Object.assign(badge.style, { fontSize: '10px', fontWeight: '600', textTransform: 'uppercase', color: '#aaa', letterSpacing: '0.04em' });
const preview = document.createElement('span');
preview.textContent = item.preview;
text.appendChild(badge); text.appendChild(preview);
row.appendChild(cb); row.appendChild(icon); row.appendChild(text);
list.appendChild(row);
});
}
refreshList();
// Preview panel
const previewPanel = document.createElement('div');
Object.assign(previewPanel.style, { display: 'none', borderTop: '1px solid #eee', background: '#f8f9fa' });
const previewTextarea = document.createElement('textarea');
previewTextarea.readOnly = true;
Object.assign(previewTextarea.style, { width: '100%', height: '180px', resize: 'none', border: 'none', background: 'transparent', padding: '12px 20px', fontFamily: '"Roboto Mono","Courier New",monospace', fontSize: '12px', lineHeight: '1.5', color: '#2c3e50', boxSizing: 'border-box', outline: 'none' });
previewPanel.appendChild(previewTextarea);
// Footer
const footer = document.createElement('div');
Object.assign(footer.style, { padding: '12px 20px', borderTop: '1px solid #eee', display: 'flex', gap: '8px', justifyContent: 'flex-end', alignItems: 'center' });
const previewToggle = makeBtn('Preview');
Object.assign(previewToggle.style, { marginRight: 'auto', fontSize: '13px', padding: '7px 14px', borderRadius: '7px' });
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy Markdown';
Object.assign(copyBtn.style, { fontSize: '13px', padding: '7px 18px', borderRadius: '7px', border: 'none', background: '#1B2536', cursor: 'pointer', color: '#fff', fontWeight: '500' });
let previewOpen = false;
previewToggle.addEventListener('click', () => {
previewOpen = !previewOpen;
previewPanel.style.display = previewOpen ? 'block' : 'none';
previewToggle.textContent = previewOpen ? 'Hide preview' : 'Preview';
if (previewOpen) previewTextarea.value = itemsToMarkdown(items.filter(i => checked[i.idx]));
});
copyBtn.addEventListener('click', () => {
const selected = items.filter(i => checked[i.idx]);
if (!selected.length) { showToast('No elements selected.', true); return; }
const markdown = itemsToMarkdown(selected);
try { GM_setClipboard(markdown); } catch (e) { navigator.clipboard.writeText(markdown).catch(() => {}); }
copyBtn.textContent = '✓ Copied!'; copyBtn.style.background = '#27ae60';
setTimeout(() => { copyBtn.textContent = 'Copy Markdown'; copyBtn.style.background = '#1B2536'; }, 2000);
showToast('Copied ' + selected.length + ' element' + (selected.length !== 1 ? 's' : '') + ' as Markdown!', false);
});
footer.appendChild(previewToggle); footer.appendChild(copyBtn);
modal.appendChild(header); modal.appendChild(list); modal.appendChild(previewPanel); modal.appendChild(footer);
overlay.appendChild(modal);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
const onKey = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); } };
document.addEventListener('keydown', onKey);
document.body.appendChild(overlay);
}
// -------------------------------------------------------------------------
// Context menu injection
//
// Milanote renders its own React-based context menus as DOM nodes.
// We watch for them to appear and inject our item(s).
//
// Board background menu (Image 2) → identified by containing "New Note"
// Element menu (Image 1) → identified by containing "Move to Trash"
// -------------------------------------------------------------------------
function makeMenuSeparator() {
const sep = document.createElement('div');
Object.assign(sep.style, {
height: '1px', background: '#e8e8e8', margin: '4px 0',
});
return sep;
}
// Clone the visual style of an existing menu item row
function makeMenuItem(label, onClick, existingItem) {
const item = document.createElement('div');
// Copy classes from an existing item so Milanote's CSS applies naturally
if (existingItem) {
item.className = existingItem.className;
}
// Override / ensure our own inline styles so it always looks right
Object.assign(item.style, {
display: 'flex',
alignItems: 'center',
padding: '6px 14px',
fontSize: '13px',
cursor: 'pointer',
userSelect: 'none',
color: '#1B2536',
gap: '8px',
borderRadius: '4px',
whiteSpace: 'nowrap',
});
const icon = document.createElement('span');
icon.textContent = '📋';
Object.assign(icon.style, { fontSize: '13px', flexShrink: '0' });
const text = document.createElement('span');
text.textContent = label;
item.appendChild(icon);
item.appendChild(text);
item.addEventListener('mouseenter', () => { item.style.background = '#f0f2f5'; });
item.addEventListener('mouseleave', () => { item.style.background = 'transparent'; });
item.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Close the context menu first
const menu = item.closest('[class*="ContextMenu"], [class*="context-menu"], [class*="PopupMenu"], [class*="popup-menu"]')
|| item.parentElement?.parentElement;
if (menu) menu.remove();
onClick();
});
return item;
}
// Detect what kind of menu this is by scanning its text content
function isBoardContextMenu(menuEl) {
const text = menuEl.textContent || '';
return text.includes('New Note') || text.includes('New Link') || text.includes('Select All');
}
function isElementContextMenu(menuEl) {
const text = menuEl.textContent || '';
return text.includes('Move to Trash') || text.includes('Duplicate');
}
function injectIntoContextMenu(menuEl) {
// Avoid double-injection
if (menuEl.dataset.mdInjected) return;
menuEl.dataset.mdInjected = '1';
// Find first real menu item to clone its class
const firstItem = menuEl.querySelector('[class*="item"], [class*="Item"], [class*="row"], [class*="Row"]');
if (isBoardContextMenu(menuEl)) {
// Board background right-click: add "Copy all as Markdown" + "Select & Copy…"
const sep = makeMenuSeparator();
const copyAll = makeMenuItem('Copy board as Markdown', () => copyAllNow(), firstItem);
const picker = makeMenuItem('Select elements to copy…', () => showPickerModal(collectElements()), firstItem);
// Insert at top, before the first child
menuEl.insertBefore(sep, menuEl.firstChild);
menuEl.insertBefore(picker, menuEl.firstChild);
menuEl.insertBefore(copyAll, menuEl.firstChild);
} else if (isElementContextMenu(menuEl)) {
// Element right-click: add "Copy element as Markdown" at the top
const sep = makeMenuSeparator();
const copyEl = makeMenuItem('Copy element as Markdown', () => {
// Try to figure out which element was right-clicked via selection or position
const items = collectElements();
if (!items.length) { showToast('Nothing to copy.', true); return; }
// Heuristic: copy the element nearest the mouse cursor at time of right-click
// We stored the last contextmenu position in lastContextMenuPos
if (window._milaMdLastPos) {
const { x, y } = window._milaMdLastPos;
let best = null, bestDist = Infinity;
document.querySelectorAll('.CanvasElement').forEach(ce => {
const rect = ce.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dist = Math.hypot(cx - x, cy - y);
if (dist < bestDist) { bestDist = dist; best = ce; }
});
if (best) {
const card = best.querySelector('.Card.element-instance');
const link = best.querySelector('.Link.element-instance');
const table = best.querySelector('.Table.element-instance');
let md = card ? convertCard(card) : link ? convertLink(link) : table ? convertTable(table) : null;
if (md) {
try { GM_setClipboard(md); } catch (e) { navigator.clipboard.writeText(md).catch(() => {}); }
showToast('Element copied as Markdown!', false);
return;
}
}
}
// Fallback: open picker
showPickerModal(items);
}, firstItem);
menuEl.insertBefore(sep, menuEl.firstChild);
menuEl.insertBefore(copyEl, menuEl.firstChild);
}
}
// Track mouse position so we can identify which element was right-clicked
document.addEventListener('contextmenu', (e) => {
window._milaMdLastPos = { x: e.clientX, y: e.clientY };
}, true);
// Watch for Milanote's context menu appearing in the DOM
const ctxObserver = new MutationObserver((mutations) => {
for (const mut of mutations) {
for (const node of mut.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
// Check the node itself and its children for likely context menu containers
const candidates = [node, ...node.querySelectorAll('*')];
for (const el of candidates) {
const cls = (el.className || '').toString();
const isMenu = (
cls.includes('ContextMenu') ||
cls.includes('context-menu') ||
cls.includes('PopupMenu') ||
cls.includes('popup-menu') ||
cls.includes('DropdownMenu') ||
cls.includes('dropdown-menu')
);
if (isMenu && (isBoardContextMenu(el) || isElementContextMenu(el))) {
// Small delay to let React finish rendering all items
setTimeout(() => injectIntoContextMenu(el), 30);
break;
}
}
}
}
});
ctxObserver.observe(document.body, { childList: true, subtree: true });
// -------------------------------------------------------------------------
// Draggable floating button — persists position in localStorage
// -------------------------------------------------------------------------
const STORAGE_KEY = 'milanote-md-btn-pos';
function loadPos() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch (e) {}
return null;
}
function savePos(top, left) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ top, left })); } catch (e) {}
}
function clampPos(top, left, w, h) {
const maxTop = window.innerHeight - h - 8;
const maxLeft = window.innerWidth - w - 8;
return {
top: Math.max(8, Math.min(top, maxTop)),
left: Math.max(8, Math.min(left, maxLeft)),
};
}
function injectDraggableButton() {
if (document.getElementById('milanote-md-float-wrapper')) return;
// ---- Wrapper (the draggable pill) ----
const wrapper = document.createElement('div');
wrapper.id = 'milanote-md-float-wrapper';
Object.assign(wrapper.style, {
position: 'fixed',
zIndex: '999997',
display: 'inline-flex',
alignItems: 'center',
height: '32px',
borderRadius: '8px',
background: '#1B2536',
boxShadow: '0 2px 10px rgba(0,0,0,0.4)',
userSelect: 'none',
overflow: 'hidden',
visibility: 'visible',
opacity: '1',
});
// ---- Drag handle (left side) ----
const handle = document.createElement('div');
handle.title = 'Drag to move';
handle.innerHTML = `
<svg width="12" height="16" viewBox="0 0 12 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3.5" cy="3.5" r="1.5" fill="rgba(255,255,255,0.45)"/>
<circle cx="8.5" cy="3.5" r="1.5" fill="rgba(255,255,255,0.45)"/>
<circle cx="3.5" cy="8" r="1.5" fill="rgba(255,255,255,0.45)"/>
<circle cx="8.5" cy="8" r="1.5" fill="rgba(255,255,255,0.45)"/>
<circle cx="3.5" cy="12.5" r="1.5" fill="rgba(255,255,255,0.45)"/>
<circle cx="8.5" cy="12.5" r="1.5" fill="rgba(255,255,255,0.45)"/>
</svg>`;
Object.assign(handle.style, {
padding: '0 6px 0 8px',
cursor: 'grab',
display: 'flex',
alignItems: 'center',
height: '100%',
borderRight: '1px solid rgba(255,255,255,0.12)',
flexShrink: '0',
});
// ---- Click button (right side) ----
const btn = document.createElement('button');
btn.id = 'milanote-md-float-btn';
btn.type = 'button';
Object.assign(btn.style, {
display: 'inline-flex',
alignItems: 'center',
gap: '5px',
padding: '0 12px',
height: '100%',
border: 'none',
background: 'transparent',
color: '#fff',
fontSize: '12px',
fontWeight: '500',
fontFamily: 'Inter, sans-serif',
cursor: 'pointer',
whiteSpace: 'nowrap',
});
const clipIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
clipIcon.setAttribute('width', '13'); clipIcon.setAttribute('height', '13');
clipIcon.setAttribute('viewBox', '0 0 24 24'); clipIcon.setAttribute('fill', 'none');
clipIcon.setAttribute('stroke', 'currentColor'); clipIcon.setAttribute('stroke-width', '2');
clipIcon.setAttribute('stroke-linecap', 'round'); clipIcon.setAttribute('stroke-linejoin', 'round');
clipIcon.style.flexShrink = '0';
clipIcon.innerHTML = '<rect x="9" y="2" width="6" height="4" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>';
const label = document.createElement('span');
label.textContent = 'Copy Markdown';
btn.appendChild(clipIcon);
btn.appendChild(label);
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.1)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
let wasDragged = false;
btn.addEventListener('click', () => {
if (wasDragged) return; // ignore click at end of drag
try { showPickerModal(collectElements()); }
catch (e) { showToast('Error: ' + e.message, true); }
});
wrapper.appendChild(handle);
wrapper.appendChild(btn);
// ---- Position: restore saved or default top-right ----
const saved = loadPos();
if (saved) {
const { top, left } = clampPos(saved.top, saved.left, 160, 32);
wrapper.style.top = top + 'px';
wrapper.style.left = left + 'px';
} else {
wrapper.style.top = '12px';
wrapper.style.right = '16px';
}
document.body.appendChild(wrapper);
// ---- Drag logic ----
let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;
handle.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
dragging = true;
wasDragged = false;
handle.style.cursor = 'grabbing';
const rect = wrapper.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
// Remove right so left takes full control
wrapper.style.right = '';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) wasDragged = true;
const rect = wrapper.getBoundingClientRect();
const { top, left } = clampPos(startTop + dy, startLeft + dx, rect.width, rect.height);
wrapper.style.top = top + 'px';
wrapper.style.left = left + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
handle.style.cursor = 'grab';
if (wasDragged) {
const rect = wrapper.getBoundingClientRect();
savePos(rect.top, rect.left);
}
setTimeout(() => { wasDragged = false; }, 50);
});
// Re-clamp on window resize
window.addEventListener('resize', () => {
const rect = wrapper.getBoundingClientRect();
const { top, left } = clampPos(rect.top, rect.left, rect.width, rect.height);
wrapper.style.top = top + 'px';
wrapper.style.left = left + 'px';
});
}
function tryInject() {
injectDraggableButton();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryInject);
else tryInject();
// -------------------------------------------------------------------------
// Tampermonkey menu fallbacks
// -------------------------------------------------------------------------
GM_registerMenuCommand('Copy board as Markdown (all)', () => copyAllNow());
GM_registerMenuCommand('Select & copy board as Markdown…', () => {
try { showPickerModal(collectElements()); } catch (e) { showToast('Error: ' + e.message, true); }
});
GM_registerMenuCommand('[Debug] Log context menu DOM', () => {
const candidates = document.querySelectorAll('[class*="ContextMenu"],[class*="context-menu"],[class*="PopupMenu"],[class*="popup-menu"]');
console.group('[Milanote Markdown] Context menu candidates');
candidates.forEach(el => console.log(el.className, el));
console.groupEnd();
showToast('Context menu DOM logged to console (F12)', false);
});
})();
@minanagehsalalma
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment