Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
952 lines
40 KiB
HTML
952 lines
40 KiB
HTML
<style>
|
|
|
|
|
|
|
|
.editor-wrap {
|
|
/* Light theme */
|
|
--bg: #ffffff;
|
|
--surface: #ffffff;
|
|
--surface2: #f9fafb;
|
|
--surface3: #f3f4f6;
|
|
--border: #e5e7eb;
|
|
--border2: #d1d5db;
|
|
--text: #111827;
|
|
--text-muted:#6b7280;
|
|
--accent: #2563eb;
|
|
--accent-bg: #eff6ff;
|
|
--green: #16a34a;
|
|
--red: #dc2626;
|
|
--yellow: #d97706;
|
|
--tab-active:#ffffff;
|
|
--radius: 8px;
|
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
}
|
|
|
|
.dark .editor-wrap {
|
|
/* Dark theme */
|
|
--bg: #030712;
|
|
--surface: #111827;
|
|
--surface2: #1f2937;
|
|
--surface3: #374151;
|
|
--border: #374151;
|
|
--border2: #4b5563;
|
|
--text: #f9fafb;
|
|
--text-muted:#9ca3af;
|
|
--accent: #3b82f6;
|
|
--accent-bg: #1e3a8a;
|
|
--green: #22c55e;
|
|
--red: #ef4444;
|
|
--yellow: #f59e0b;
|
|
--tab-active:#111827;
|
|
}
|
|
|
|
|
|
|
|
.editor-wrap { width: 100%; max-width: 960px; position: relative; }
|
|
|
|
/* ── Mode switcher ── */
|
|
.editor-wrap .mode-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.editor-wrap .mode-pills {
|
|
display: flex;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 3px;
|
|
gap: 2px;
|
|
}
|
|
|
|
.editor-wrap .mode-pill {
|
|
background: none;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-family: var(--font-sans);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
padding: 4px 12px;
|
|
transition: all .15s;
|
|
display: flex; align-items: center; gap: 5px;
|
|
}
|
|
.editor-wrap .mode-pill:hover { color: var(--text); }
|
|
.editor-wrap .mode-pill.active { background: var(--surface3); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.4); }
|
|
|
|
/* ── Collab bar ── */
|
|
.editor-wrap .collab-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.editor-wrap .collab-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.editor-wrap .collab-dot {
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--text-muted);
|
|
transition: background .3s;
|
|
}
|
|
.editor-wrap .collab-dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
|
|
.editor-wrap .peer-avatars { display: flex; gap: -4px; }
|
|
|
|
.editor-wrap .peer-avatar {
|
|
width: 26px; height: 26px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--bg);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 11px; font-weight: 700;
|
|
color: #fff;
|
|
margin-left: -6px;
|
|
cursor: default;
|
|
position: relative;
|
|
transition: transform .15s;
|
|
}
|
|
.editor-wrap .peer-avatar:first-child { margin-left: 0; }
|
|
.editor-wrap .peer-avatar:hover { transform: translateY(-2px); z-index: 1; }
|
|
.editor-wrap .peer-avatar .peer-tip {
|
|
position: absolute;
|
|
bottom: calc(100% + 5px);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #111827;
|
|
border: 1px solid #374151;
|
|
border-radius: 6px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
color: #ffffff;
|
|
font-size: 11px;
|
|
padding: 3px 7px;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity .1s;
|
|
}
|
|
.editor-wrap .peer-avatar:hover .peer-tip { opacity: 1; }
|
|
|
|
.editor-wrap .collab-link-btn {
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
padding: 3px 9px;
|
|
transition: all .1s;
|
|
display: flex; align-items: center; gap: 5px;
|
|
}
|
|
.editor-wrap .collab-link-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
|
|
/* ── Tabs ── */
|
|
.editor-wrap .tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: -1px; align-items: flex-end; }
|
|
|
|
.editor-wrap .tab-btn {
|
|
background: none;
|
|
border: 1px solid transparent;
|
|
border-bottom: none;
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-family: var(--font-sans);
|
|
font-size: 14px;
|
|
padding: 8px 16px;
|
|
transition: color .15s;
|
|
}
|
|
.editor-wrap .tab-btn:hover { color: var(--text); }
|
|
.editor-wrap .tab-btn.active {
|
|
background: var(--tab-active);
|
|
border-color: var(--border);
|
|
border-bottom-color: var(--tab-active);
|
|
color: var(--text);
|
|
z-index: 1;
|
|
}
|
|
|
|
.editor-wrap .lang-select {
|
|
margin-left: auto;
|
|
margin-bottom: 1px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
outline: none;
|
|
}
|
|
.editor-wrap .lang-select option { background: var(--surface2); }
|
|
|
|
/* ── Toolbar ── */
|
|
.editor-wrap .toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
padding: 6px 10px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.editor-wrap .toolbar-sep { width: 1px; height: 18px; background: var(--border2); margin: 0 4px; }
|
|
|
|
.editor-wrap .tool-btn {
|
|
background: none; border: none; border-radius: 4px;
|
|
color: var(--text-muted); cursor: pointer;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
padding: 4px 6px;
|
|
transition: background .1s, color .1s;
|
|
position: relative;
|
|
}
|
|
.editor-wrap .tool-btn:hover { background: var(--border); color: var(--text); }
|
|
.editor-wrap .tool-btn svg { display: block; }
|
|
.editor-wrap .tool-btn::after {
|
|
content: attr(data-editor-tooltip);
|
|
position: absolute; bottom: calc(100% + 6px); left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #111827; border: 1px solid #374151; border-radius: 6px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
color: #ffffff; font-size: 12px; padding: 4px 8px;
|
|
white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 100;
|
|
}
|
|
.editor-wrap .tool-btn:hover::after { opacity: 1; }
|
|
|
|
/* code editor toolbar extras */
|
|
.editor-wrap .code-toolbar-info {
|
|
margin-left: auto;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ── Write Panel ── */
|
|
.editor-wrap .write-panel {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
position: relative;
|
|
}
|
|
.editor-wrap .write-panel.drag-over { border-color: var(--accent); background: rgba(47,129,247,.05); }
|
|
.editor-wrap .write-panel:focus-within { border-color: var(--accent); }
|
|
|
|
.editor-wrap textarea {
|
|
background: transparent; border: none;
|
|
caret-color: var(--accent); color: var(--text);
|
|
display: block; font-family: var(--font-mono); font-size: 14px;
|
|
line-height: 1.6; min-height: 44px; max-height: 600px;
|
|
outline: none; padding: 10px 16px; resize: none; width: 100%;
|
|
overflow-y: auto; transition: min-height .15s ease;
|
|
}
|
|
.editor-wrap textarea::placeholder { color: var(--text-muted); }
|
|
|
|
.editor-wrap .drag-overlay {
|
|
position: absolute; inset: 0;
|
|
display: flex; align-items: center; justify-content: center; gap: 10px;
|
|
background: rgba(47,129,247,.08); border: 2px dashed var(--accent);
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|
color: var(--accent); font-size: 15px; font-weight: 500;
|
|
pointer-events: none; z-index: 20;
|
|
}
|
|
|
|
.editor-wrap .attach-bar {
|
|
border-top: 1px solid var(--border);
|
|
padding: 8px 12px;
|
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
}
|
|
.editor-wrap .attach-hint { color: var(--text-muted); font-size: 12px; flex: 1; display: flex; align-items: center; gap: 6px; }
|
|
.editor-wrap .attach-label {
|
|
cursor: pointer; color: var(--accent); font-size: 12px;
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 3px 8px; border: 1px solid var(--border); border-radius: 4px; transition: background .1s;
|
|
}
|
|
.editor-wrap .attach-label:hover { background: var(--border); }
|
|
.editor-wrap #file-input { display: none; }
|
|
|
|
.editor-wrap .img-previews { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px 12px 10px; border-top: 1px solid var(--border); }
|
|
.editor-wrap .img-thumb { position: relative; border: 1px solid var(--border); border-radius: 5px; overflow: hidden; width: 80px; height: 60px; flex-shrink: 0; }
|
|
.editor-wrap .img-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
.editor-wrap .img-thumb .remove-btn {
|
|
position: absolute; top: 2px; right: 2px;
|
|
background: rgba(0,0,0,.75); border: none; border-radius: 50%;
|
|
color: #fff; cursor: pointer; width: 18px; height: 18px; font-size: 11px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.editor-wrap .img-thumb .remove-btn:hover { background: var(--red); }
|
|
.editor-wrap .img-thumb .uploading-bar { position: absolute; bottom: 0; left: 0; height: 3px; background: var(--accent); transition: width .3s; }
|
|
|
|
.editor-wrap .char-count { color: var(--text-muted); font-size: 12px; }
|
|
|
|
/* ── CodeMirror wrapper ── */
|
|
.editor-wrap .cm-wrap {
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.editor-wrap .cm-wrap:focus-within { border-color: var(--accent); }
|
|
|
|
/* CM6 theme overrides */
|
|
.editor-wrap .cm-editor {
|
|
background: var(--surface) !important;
|
|
color: var(--text) !important;
|
|
font-family: var(--font-mono) !important;
|
|
font-size: 13.5px !important;
|
|
min-height: 44px;
|
|
max-height: 600px;
|
|
}
|
|
.editor-wrap .cm-editor.cm-focused { outline: none !important; }
|
|
.editor-wrap .cm-scroller { overflow: auto !important; }
|
|
.editor-wrap .cm-gutters {
|
|
background: var(--surface2) !important;
|
|
border-right: 1px solid var(--border) !important;
|
|
color: var(--text-muted) !important;
|
|
min-width: 46px;
|
|
}
|
|
.editor-wrap .cm-activeLineGutter { background: rgba(47,129,247,.08) !important; color: var(--text) !important; }
|
|
.editor-wrap .cm-activeLine { background: rgba(47,129,247,.06) !important; }
|
|
.editor-wrap .cm-selectionBackground { background: rgba(47,129,247,.25) !important; }
|
|
.editor-wrap .cm-cursor { border-left-color: var(--accent) !important; border-left-width: 2px !important; }
|
|
.editor-wrap .cm-matchingBracket { background: rgba(63,185,80,.2) !important; color: var(--green) !important; outline: 1px solid var(--green) !important; }
|
|
.editor-wrap .cm-tooltip { background: var(--surface2) !important; border: 1px solid var(--border2) !important; border-radius: 4px !important; color: var(--text) !important; }
|
|
.editor-wrap .cm-tooltip-autocomplete > ul > li { padding: 3px 10px !important; }
|
|
.editor-wrap .cm-tooltip-autocomplete > ul > li[aria-selected] { background: var(--accent-bg) !important; color: var(--text) !important; }
|
|
.editor-wrap .cm-searchMatch { background: rgba(210,153,34,.3) !important; outline: 1px solid var(--yellow) !important; }
|
|
.editor-wrap .cm-searchMatch-selected { background: rgba(210,153,34,.6) !important; }
|
|
.editor-wrap .cm-panels { background: var(--surface2) !important; border-color: var(--border) !important; }
|
|
.editor-wrap .cm-panels input { background: var(--surface) !important; border: 1px solid var(--border2) !important; color: var(--text) !important; border-radius: 3px; padding: 2px 6px; outline: none; }
|
|
.editor-wrap .cm-panels button { background: var(--surface3) !important; border: 1px solid var(--border) !important; color: var(--text) !important; border-radius: 3px; padding: 2px 8px; cursor: pointer; }
|
|
|
|
/* Syntax highlighting */
|
|
.editor-wrap .tok-keyword { color: #ff7b72; }
|
|
.editor-wrap .tok-operator { color: #ff7b72; }
|
|
.editor-wrap .tok-string { color: #a5d6ff; }
|
|
.editor-wrap .tok-number { color: #79c0ff; }
|
|
.editor-wrap .tok-comment { color: #8b949e; font-style: italic; }
|
|
.editor-wrap .tok-function { color: #d2a8ff; }
|
|
.editor-wrap .tok-typeName { color: #ffa657; }
|
|
.editor-wrap .tok-className { color: #ffa657; }
|
|
.editor-wrap .tok-propertyName { color: #79c0ff; }
|
|
.editor-wrap .tok-variableName { color: #e6edf3; }
|
|
.editor-wrap .tok-bool { color: #79c0ff; }
|
|
.editor-wrap .tok-null { color: #79c0ff; }
|
|
.editor-wrap .tok-meta { color: #8b949e; }
|
|
.editor-wrap .tok-emphasis { font-style: italic; }
|
|
.editor-wrap .tok-strong { font-weight: bold; }
|
|
.editor-wrap .tok-heading { color: #79c0ff; font-weight: bold; }
|
|
.editor-wrap .tok-link { color: #a5d6ff; text-decoration: underline; }
|
|
.editor-wrap .tok-url { color: #a5d6ff; }
|
|
.editor-wrap .tok-tagName { color: #7ee787; }
|
|
.editor-wrap .tok-attributeName { color: #79c0ff; }
|
|
.editor-wrap .tok-attributeValue { color: #a5d6ff; }
|
|
.editor-wrap .tok-punctuation { color: #c9d1d9; }
|
|
|
|
/* ── Remote cursors ── */
|
|
.editor-wrap .remote-cursor-widget {
|
|
border-left: 2px solid;
|
|
border-radius: 1px;
|
|
margin-left: -1px;
|
|
pointer-events: none;
|
|
position: relative;
|
|
}
|
|
.editor-wrap .remote-cursor-label {
|
|
position: absolute;
|
|
top: -18px;
|
|
left: -1px;
|
|
font-size: 10px;
|
|
font-family: var(--font-sans);
|
|
padding: 1px 5px;
|
|
border-radius: 3px 3px 3px 0;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
color: #fff;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* ── Preview Panel ── */
|
|
.editor-wrap .preview-panel {
|
|
background: var(--surface); border: 1px solid var(--border); border-top: none;
|
|
min-height: 120px; padding: 16px 20px;
|
|
}
|
|
.editor-wrap .md-preview { line-height: 1.7; color: var(--text); }
|
|
.editor-wrap .md-preview h1, .editor-wrap .md-preview h2, .editor-wrap .md-preview h3, .editor-wrap .md-preview h4, .editor-wrap .md-preview h5, .editor-wrap .md-preview h6 {
|
|
border-bottom: 1px solid var(--border); font-weight: 600; margin: 20px 0 12px; padding-bottom: 6px;
|
|
}
|
|
.editor-wrap .md-preview h1 { font-size: 1.8em; } .md-preview h2 { font-size: 1.4em; }
|
|
.editor-wrap .md-preview h3 { font-size: 1.15em; border-bottom: none; }
|
|
.editor-wrap .md-preview p { margin: 0 0 14px; }
|
|
.editor-wrap .md-preview a { color: var(--accent); text-decoration: none; }
|
|
.editor-wrap .md-preview a:hover { text-decoration: underline; }
|
|
.editor-wrap .md-preview code { background: rgba(110,118,129,.2); border-radius: 3px; font-family: var(--font-mono); font-size: 85%; padding: 2px 5px; }
|
|
.editor-wrap .md-preview pre { background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 14px; overflow: auto; padding: 14px 16px; }
|
|
.editor-wrap .md-preview pre code { background: none; font-size: 13px; padding: 0; }
|
|
.editor-wrap .md-preview blockquote { border-left: 4px solid var(--border2); color: var(--text-muted); margin: 0 0 14px; padding: 4px 14px; }
|
|
.editor-wrap .md-preview ul, .editor-wrap .md-preview ol { margin: 0 0 14px; padding-left: 24px; }
|
|
.editor-wrap .md-preview li { margin-bottom: 4px; }
|
|
.editor-wrap .md-preview hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
|
.editor-wrap .md-preview table { border-collapse: collapse; margin-bottom: 14px; width: 100%; }
|
|
.editor-wrap .md-preview th, .editor-wrap .md-preview td { border: 1px solid var(--border); padding: 6px 12px; text-align: left; }
|
|
.editor-wrap .md-preview th { background: var(--surface2); font-weight: 600; }
|
|
.editor-wrap .md-preview strong { font-weight: 600; } .md-preview em { font-style: italic; }
|
|
.editor-wrap .md-preview del { text-decoration: line-through; color: var(--text-muted); }
|
|
.editor-wrap .md-preview img { max-width: 100%; border-radius: var(--radius); }
|
|
.editor-wrap .md-preview .task-item { list-style: none; margin-left: -20px; }
|
|
.editor-wrap .md-preview .task-item input { margin-right: 6px; accent-color: var(--accent); }
|
|
.editor-wrap .md-preview .mention { color: var(--accent); background: rgba(47,129,247,.12); border-radius: 3px; padding: 1px 3px; font-weight: 500; }
|
|
.editor-wrap .preview-empty { color: var(--text-muted); font-style: italic; padding: 20px 0; }
|
|
|
|
/* ── Mention dropdown ── */
|
|
.editor-wrap .mention-dropdown {
|
|
position: absolute;
|
|
background: var(--surface2); border: 1px solid var(--border2);
|
|
border-radius: var(--radius); box-shadow: 0 8px 24px rgba(1,4,9,.6);
|
|
min-width: 220px; max-height: 220px; overflow-y: auto; z-index: 200;
|
|
}
|
|
.editor-wrap .mention-item { align-items: center; cursor: pointer; display: flex; gap: 10px; padding: 8px 12px; transition: background .1s; }
|
|
.editor-wrap .mention-item:hover, .editor-wrap .mention-item.active { background: var(--accent-bg); }
|
|
.editor-wrap .mention-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--surface3); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: var(--text); flex-shrink: 0; }
|
|
.editor-wrap .mention-info { display: flex; flex-direction: column; min-width: 0; }
|
|
.editor-wrap .mention-name { font-weight: 500; font-size: 13px; color: var(--text); }
|
|
.editor-wrap .mention-handle { font-size: 12px; color: var(--text-muted); }
|
|
|
|
/* ── Emoji picker ── */
|
|
.editor-wrap .emoji-picker {
|
|
position: absolute; background: var(--surface2); border: 1px solid var(--border2);
|
|
border-radius: var(--radius); box-shadow: 0 8px 24px rgba(1,4,9,.6); width: 300px; z-index: 200; overflow: hidden;
|
|
}
|
|
.editor-wrap .emoji-search-wrap { padding: 8px 10px; border-bottom: 1px solid var(--border); }
|
|
.editor-wrap .emoji-search { background: var(--surface); border: 1px solid var(--border2); border-radius: 4px; color: var(--text); font-size: 13px; outline: none; padding: 5px 10px; width: 100%; }
|
|
.editor-wrap .emoji-search::placeholder { color: var(--text-muted); }
|
|
.editor-wrap .emoji-category { font-size: 11px; color: var(--text-muted); padding: 6px 10px 2px; text-transform: uppercase; letter-spacing: .05em; }
|
|
.editor-wrap .emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); padding: 4px 6px 10px; max-height: 180px; overflow-y: auto; }
|
|
.editor-wrap .emoji-btn { background: none; border: none; border-radius: 4px; cursor: pointer; font-size: 18px; padding: 4px; transition: background .1s; text-align: center; }
|
|
.editor-wrap .emoji-btn:hover { background: var(--border); }
|
|
|
|
/* ── Footer ── */
|
|
.editor-wrap .footer-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px 0 0; gap: 8px; flex-wrap: wrap; }
|
|
.editor-wrap .md-hint { align-items: center; color: var(--text-muted); display: inline-flex; font-size: 12px; gap: 5px; text-decoration: none; }
|
|
.editor-wrap .md-hint:hover { color: var(--text); }
|
|
.editor-wrap .btn-row { display: flex; gap: 8px; }
|
|
.editor-wrap .btn { border-radius: var(--radius); border: 1px solid transparent; cursor: pointer; font-family: var(--font-sans); font-size: 14px; font-weight: 500; padding: 6px 16px; transition: opacity .15s, background .15s; }
|
|
.editor-wrap .btn-ghost { background: var(--surface2); border-color: var(--border); color: var(--text); }
|
|
.editor-wrap .btn-ghost:hover { background: var(--border); }
|
|
.editor-wrap .btn-primary { background: #238636; border-color: #2ea043; color: #fff; }
|
|
.editor-wrap .btn-primary:hover { background: #2ea043; }
|
|
.editor-wrap .btn-primary:disabled { opacity: .4; cursor: not-allowed; }
|
|
|
|
/* ── Toast ── */
|
|
.editor-wrap-toast {
|
|
position: fixed; bottom: 24px; left: 50%;
|
|
transform: translateX(-50%) translateY(8px);
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: var(--radius); color: var(--text); font-size: 13px; padding: 8px 18px;
|
|
opacity: 0; transition: opacity .2s, transform .2s; pointer-events: none; z-index: 999;
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.editor-wrap .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
|
|
/* ── Status bar ── */
|
|
.editor-wrap .status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 5px 12px;
|
|
background: var(--surface2);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.editor-wrap .status-bar span { display: flex; align-items: center; gap: 4px; }
|
|
.editor-wrap .status-bar .sep { color: var(--border2); }
|
|
|
|
.editor-wrap ::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
.editor-wrap ::-webkit-scrollbar-track { background: transparent; }
|
|
.editor-wrap ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
|
|
|
|
</style>
|
|
|
|
|
|
<script>
|
|
// ── Fake users ──
|
|
const USERS = [
|
|
{ name: 'Sankalp Sharma', handle: 'sankalp', color: '#2f81f7' },
|
|
{ name: 'Alice Johnson', handle: 'alice', color: '#3fb950' },
|
|
{ name: 'Bob Ramirez', handle: 'bob', color: '#a371f7' },
|
|
{ name: 'Carol Chen', handle: 'carol', color: '#ffa657' },
|
|
{ name: 'Dave Kumar', handle: 'dave', color: '#f85149' },
|
|
{ name: 'Eva Müller', handle: 'eva', color: '#58a6ff' },
|
|
{ name: 'Frank Osei', handle: 'frank', color: '#3fb950' },
|
|
{ name: 'Grace Park', handle: 'grace', color: '#d2a8ff' },
|
|
];
|
|
|
|
const PEER_NAMES = ['Alice Johnson','Bob Ramirez','Carol Chen','Dave Kumar','Eva Müller','Frank Osei'];
|
|
const PEER_COLORS = ['#3fb950','#a371f7','#ffa657','#f85149','#58a6ff','#d2a8ff'];
|
|
|
|
const EMOJI_CATS = [
|
|
{ name: 'Smileys', emojis: ['😀','😃','😄','😁','😆','😅','😂','🤣','😊','😇','🙂','😉','😌','😍','🥰','😘','😋','😛','😝','😜','🤪','😎','🤩','😏','😒','😔','😟','😕','😢','😭','😤','😠','🤬','🤯','😳','😱','😨','😓'] },
|
|
{ name: 'Gestures', emojis: ['👋','✋','👌','✌️','🤞','👍','👎','✊','👊','👏','🙌','🙏','💪','🤳','💅','☝️','👆','👇','👈','👉'] },
|
|
{ name: 'Objects', emojis: ['💻','🖥','⌨️','🖱','📱','📷','📹','🔋','💡','🔦','📡','💾','💿','📀','🔑','🔧','⚙️','🛠','🔩','📎','✏️','📝','📋'] },
|
|
{ name: 'Nature', emojis: ['🌱','🌿','🍀','🌲','🌳','🌴','🌵','🌾','🌷','🌹','🌺','🌸','🌼','🌻','⭐','✨','⚡','🔥','🌈','☀️','🌤','⛅','🌧','❄️','💧','💦'] },
|
|
{ name: 'Food', emojis: ['🍕','🍔','🌮','🥙','🍳','🍲','🍞','🧀','🥩','🍗','🍿','🧁','🎂','🍰','🍩','🍪','🍫','☕','🍵','🍺','🥂','🍷'] },
|
|
{ name: 'Symbols', emojis: ['❤️','🧡','💛','💚','💙','💜','🖤','💔','💕','💞','💓','💗','💖','💘','✅','❌','⚠️','🔴','🟡','🟢','🔵','⚡','💯','🎯','🔥','💡','🚀','⭐','🏆','🎉','🎊','🎈'] },
|
|
];
|
|
|
|
// Shared across all editor instances on the same page so that same-page
|
|
// editors can identify and ignore each other's BroadcastChannel messages.
|
|
const PAGE_ID = Math.random().toString(36).slice(2);
|
|
|
|
function editor(initialValue = '') {
|
|
return {
|
|
// Mode
|
|
mode: 'markdown',
|
|
|
|
// Markdown state
|
|
tab: 'write',
|
|
content: initialValue,
|
|
codeDoc: initialValue,
|
|
preview: '',
|
|
uploads: [],
|
|
isDragging: false,
|
|
|
|
// Code editor state
|
|
codeLang: 'javascript',
|
|
|
|
cmLine: 1, cmCol: 1, cmSelLen: 0,
|
|
wordWrap: false,
|
|
_cmView: null,
|
|
|
|
// Collab state
|
|
// Unique per-page identifier so that editors on the same page don't
|
|
// interfere with each other (the BroadcastChannel is shared globally
|
|
// across tabs, but same-page instances should stay independent).
|
|
pageId: PAGE_ID,
|
|
myId: Math.random().toString(36).slice(2),
|
|
myName: 'You',
|
|
myColor: '#2f81f7',
|
|
peers: [],
|
|
collabActive: false,
|
|
_channel: null,
|
|
_heartbeatInterval: null,
|
|
_remoteCursors: {},
|
|
_knownPeers: [],
|
|
|
|
// Mention
|
|
showMention: false,
|
|
mentionQuery: '',
|
|
mentionStart: -1,
|
|
mentionIndex: 0,
|
|
mentionPos: { top: 0, left: 0 },
|
|
allUsers: USERS,
|
|
filteredUsers: [],
|
|
|
|
// Emoji
|
|
showEmoji: false,
|
|
emojiSearch: '',
|
|
emojiPos: { top: 0, left: 0 },
|
|
emojiCats: EMOJI_CATS,
|
|
filteredEmojiCats: EMOJI_CATS,
|
|
|
|
init() {
|
|
// Pick a random name/color for this session
|
|
const idx = Math.floor(Math.random() * PEER_NAMES.length);
|
|
this.myName = PEER_NAMES[idx];
|
|
this.myColor = PEER_COLORS[idx];
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', e => {
|
|
if ((e.ctrlKey || e.metaKey) && this.mode === 'markdown') {
|
|
if (e.key === 'b') { e.preventDefault(); this.wrap('**','**'); }
|
|
if (e.key === 'i') { e.preventDefault(); this.wrap('_','_'); }
|
|
if (e.key === 'k') { e.preventDefault(); this.insertLink(); }
|
|
}
|
|
});
|
|
|
|
// BroadcastChannel collab
|
|
this._channel = new BroadcastChannel('rte_collab_v1');
|
|
this._channel.onmessage = e => this._onCollab(e.data);
|
|
this._broadcast({ type: 'join', pageId: this.pageId, id: this.myId, name: this.myName, color: this.myColor });
|
|
this._heartbeatInterval = setInterval(() => {
|
|
this._broadcast({ type: 'heartbeat', pageId: this.pageId, id: this.myId, name: this.myName, color: this.myColor });
|
|
// Prune stale peers
|
|
const now = Date.now();
|
|
this.peers = this.peers.filter(p => now - p.lastSeen < 4000);
|
|
this.collabActive = this.peers.length > 0;
|
|
}, 1500);
|
|
},
|
|
|
|
_broadcast(msg) { this._channel && this._channel.postMessage(msg); },
|
|
|
|
_onCollab(msg) {
|
|
if (msg.id === this.myId) return;
|
|
if (msg.type === 'join' || msg.type === 'heartbeat') {
|
|
const existing = this.peers.find(p => p.id === msg.id);
|
|
if (existing) { existing.lastSeen = Date.now(); }
|
|
else { this.peers.push({ id: msg.id, name: msg.name, color: msg.color, lastSeen: Date.now() }); }
|
|
this.collabActive = this.peers.length > 0;
|
|
// Send current doc to new peer (only cross-page, skip same-page instances)
|
|
if (msg.type === 'join' && msg.pageId !== this.pageId && !this._knownPeers.includes(msg.id)) {
|
|
this._broadcast({ type: 'sync', pageId: this.pageId, id: this.myId, name: this.myName, color: this.myColor, content: this.content, codeDoc: this.codeDoc, mode: this.mode });
|
|
this._knownPeers.push(msg.id);
|
|
}
|
|
}
|
|
if (msg.type === 'sync') {
|
|
// Ignore syncs from editors on the same page (same-page isolation)
|
|
if (msg.pageId === this.pageId) return;
|
|
// Only apply cross-peer content when both editors share the same mode
|
|
// (prevents a wysiwyg description editor from pre-filling a markdown reply editor)
|
|
if (!this.content && !this.codeDoc && this.mode === msg.mode) {
|
|
this.content = msg.content || '';
|
|
this.codeDoc = msg.codeDoc || '';
|
|
if (this._cmView && msg.codeDoc) {
|
|
this._cmView.dispatch({ changes: { from: 0, to: this._cmView.state.doc.length, insert: msg.codeDoc } });
|
|
}
|
|
}
|
|
}
|
|
if (msg.type === 'content_change') {
|
|
if (msg.pageId !== this.pageId) this.content = msg.content;
|
|
}
|
|
if (msg.type === 'code_change') {
|
|
if (msg.pageId !== this.pageId && this._cmView) {
|
|
const cur = this._cmView.state.doc.toString();
|
|
if (cur !== msg.code) {
|
|
this._cmView.dispatch({ changes: { from: 0, to: cur.length, insert: msg.code } });
|
|
}
|
|
}
|
|
if (msg.pageId !== this.pageId) this.codeDoc = msg.code;
|
|
}
|
|
if (msg.type === 'cursor_move') {
|
|
this._remoteCursors[msg.id] = { pos: msg.pos, name: msg.name, color: msg.color };
|
|
this._updateRemoteCursors();
|
|
}
|
|
if (msg.type === 'leave') {
|
|
this.peers = this.peers.filter(p => p.id !== msg.id);
|
|
delete this._remoteCursors[msg.id];
|
|
this._updateRemoteCursors();
|
|
this.collabActive = this.peers.length > 0;
|
|
}
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
copyCollabHint() {
|
|
navigator.clipboard.writeText(window.location.href).then(() =>
|
|
this.showToast('🔗', 'URL copied — open in another tab to collaborate!')
|
|
);
|
|
},
|
|
|
|
// ── Markdown helpers ──
|
|
getTA() { return this.$refs.ta; },
|
|
getSelection() {
|
|
const ta = this.getTA();
|
|
if (!ta) return { start: 0, end: 0, text: '' };
|
|
return { start: ta.selectionStart, end: ta.selectionEnd, text: ta.value.slice(ta.selectionStart, ta.selectionEnd) };
|
|
},
|
|
setSelection(start, end) {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
ta.focus(); ta.setSelectionRange(start, end);
|
|
},
|
|
wrap(before, after) {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end, text } = this.getSelection();
|
|
const placeholder = text || 'text';
|
|
const newText = before + placeholder + after;
|
|
this.content = this.content.slice(0, start) + newText + this.content.slice(end);
|
|
this.$nextTick(() => { const s = start + before.length; this.setSelection(s, s + placeholder.length); });
|
|
},
|
|
insertAtLine(prefix) {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const pos = ta.selectionStart;
|
|
const lineStart = this.content.lastIndexOf('\n', pos - 1) + 1;
|
|
this.content = this.content.slice(0, lineStart) + prefix + this.content.slice(lineStart);
|
|
this.$nextTick(() => this.setSelection(pos + prefix.length, pos + prefix.length));
|
|
},
|
|
insertList(prefix) {
|
|
const { start, end, text } = this.getSelection();
|
|
if (text) {
|
|
const lined = text.split('\n').map(l => prefix + l).join('\n');
|
|
this.content = this.content.slice(0, start) + lined + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start, start + lined.length));
|
|
} else { this.insertAtLine(prefix); }
|
|
},
|
|
insertLink() {
|
|
const { text } = this.getSelection();
|
|
const label = text || 'link text';
|
|
const snippet = `[${label}](url)`;
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end } = this.getSelection();
|
|
this.content = this.content.slice(0, start) + snippet + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + label.length + 3, start + snippet.length - 1));
|
|
},
|
|
insertCodeBlock() {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end, text } = this.getSelection();
|
|
const inner = text || 'code here';
|
|
const snippet = '```\n' + inner + '\n```';
|
|
this.content = this.content.slice(0, start) + snippet + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + 4, start + 4 + inner.length));
|
|
},
|
|
insertTable() {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end } = this.getSelection();
|
|
const snippet = '\n| Header | Header |\n| --- | --- |\n| Cell | Cell |\n| Cell | Cell |\n';
|
|
this.content = this.content.slice(0, start) + snippet + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + snippet.length, start + snippet.length));
|
|
},
|
|
insertHR() {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end } = this.getSelection();
|
|
const snippet = '\n---\n';
|
|
this.content = this.content.slice(0, start) + snippet + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + snippet.length, start + snippet.length));
|
|
},
|
|
|
|
autoResize() {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
ta.style.height = 'auto';
|
|
ta.style.height = Math.min(ta.scrollHeight, 600) + 'px';
|
|
},
|
|
|
|
// ── Mention ──
|
|
insertMentionTrigger() {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end } = this.getSelection();
|
|
this.content = this.content.slice(0, start) + '@' + this.content.slice(end);
|
|
this.$nextTick(() => { this.setSelection(start + 1, start + 1); this.openMentionDropdown(start); });
|
|
},
|
|
openMentionDropdown(atPos) {
|
|
this.mentionStart = atPos;
|
|
this.mentionQuery = '';
|
|
this.filteredUsers = this.allUsers.slice(0, 8);
|
|
this.mentionIndex = 0;
|
|
this.showMention = true;
|
|
this.$nextTick(() => this.updateMentionPos(atPos));
|
|
},
|
|
updateMentionPos(atPos) {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const rect = ta.getBoundingClientRect();
|
|
const wRect = ta.closest('.editor-wrap').getBoundingClientRect();
|
|
this.mentionPos = { top: rect.bottom - wRect.top + 4, left: rect.left - wRect.left };
|
|
},
|
|
handleMentionSearch(query) {
|
|
this.mentionQuery = query;
|
|
const q = query.toLowerCase();
|
|
this.filteredUsers = this.allUsers.filter(u => u.name.toLowerCase().includes(q) || u.handle.toLowerCase().includes(q)).slice(0, 8);
|
|
this.mentionIndex = 0;
|
|
},
|
|
selectMention(user) {
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const before = this.content.slice(0, this.mentionStart);
|
|
const after = this.content.slice(ta.selectionStart);
|
|
const insert = `@${user.handle} `;
|
|
this.content = before + insert + after;
|
|
this.showMention = false;
|
|
this.$nextTick(() => { const pos = this.mentionStart + insert.length; this.setSelection(pos, pos); });
|
|
},
|
|
|
|
// ── Emoji ──
|
|
toggleEmojiPicker(evt) {
|
|
if (this.showEmoji) { this.showEmoji = false; return; }
|
|
const btn = evt.currentTarget;
|
|
const wrapRect = btn.closest('.editor-wrap').getBoundingClientRect();
|
|
const btnRect = btn.getBoundingClientRect();
|
|
this.emojiPos = { top: btnRect.bottom - wrapRect.top + 6, left: Math.min(btnRect.left - wrapRect.left, wrapRect.width - 310) };
|
|
this.emojiSearch = '';
|
|
this.filteredEmojiCats = this.emojiCats;
|
|
this.showEmoji = true;
|
|
this.$nextTick(() => this.$refs.emojiInput && this.$refs.emojiInput.focus());
|
|
},
|
|
filterEmoji() {
|
|
const q = this.emojiSearch.toLowerCase();
|
|
if (!q) { this.filteredEmojiCats = this.emojiCats; return; }
|
|
this.filteredEmojiCats = this.emojiCats.filter(c => c.name.toLowerCase().includes(q));
|
|
},
|
|
insertEmoji(emoji) {
|
|
if (this.mode === 'code' && this._cmView) {
|
|
const sel = this._cmView.state.selection.main;
|
|
this._cmView.dispatch({ changes: { from: sel.from, to: sel.to, insert: emoji } });
|
|
} else {
|
|
const ta = this.getTA();
|
|
const { start, end } = this.getSelection();
|
|
this.content = this.content.slice(0, start) + emoji + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + emoji.length, start + emoji.length));
|
|
ta && ta.focus();
|
|
}
|
|
this.showEmoji = false;
|
|
},
|
|
|
|
closePickers() { this.showMention = false; this.showEmoji = false; },
|
|
|
|
// ── Input handler ──
|
|
handleInput(e) {
|
|
this.autoResize();
|
|
this._broadcast({ type: 'content_change', id: this.myId, content: this.content });
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const pos = ta.selectionStart;
|
|
const textBefore = this.content.slice(0, pos);
|
|
const lineStart = textBefore.lastIndexOf('\n') + 1;
|
|
const lineText = textBefore.slice(lineStart);
|
|
const atIdx = lineText.lastIndexOf('@');
|
|
if (atIdx !== -1) {
|
|
const query = lineText.slice(atIdx + 1);
|
|
if (!query.includes(' ') && !query.includes('\n')) {
|
|
const absAt = lineStart + atIdx;
|
|
if (!this.showMention || this.mentionStart !== absAt) {
|
|
this.mentionStart = absAt;
|
|
this.updateMentionPos(absAt);
|
|
this.showMention = true;
|
|
}
|
|
this.handleMentionSearch(query);
|
|
return;
|
|
}
|
|
}
|
|
this.showMention = false;
|
|
},
|
|
|
|
handleKeydown(e) {
|
|
if (this.showMention) {
|
|
if (e.key === 'ArrowDown') { e.preventDefault(); this.mentionIndex = Math.min(this.mentionIndex + 1, this.filteredUsers.length - 1); return; }
|
|
if (e.key === 'ArrowUp') { e.preventDefault(); this.mentionIndex = Math.max(this.mentionIndex - 1, 0); return; }
|
|
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); if (this.filteredUsers[this.mentionIndex]) this.selectMention(this.filteredUsers[this.mentionIndex]); return; }
|
|
if (e.key === 'Escape') { this.showMention = false; return; }
|
|
}
|
|
if (this.showEmoji && e.key === 'Escape') { this.showEmoji = false; return; }
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const ta = this.getTA(); if (!ta) return;
|
|
const { start, end } = this.getSelection();
|
|
this.content = this.content.slice(0, start) + ' ' + this.content.slice(end);
|
|
this.$nextTick(() => this.setSelection(start + 2, start + 2));
|
|
}
|
|
},
|
|
|
|
// ── Image upload ──
|
|
handleDrop(e) {
|
|
this.isDragging = false;
|
|
[...e.dataTransfer.files].filter(f => f.type.startsWith('image/')).forEach(f => this.processImageFile(f));
|
|
},
|
|
handlePaste(e) {
|
|
const items = [...(e.clipboardData?.items || [])].filter(i => i.type.startsWith('image/'));
|
|
if (!items.length) return;
|
|
e.preventDefault();
|
|
items.forEach(item => { const f = item.getAsFile(); if (f) this.processImageFile(f); });
|
|
},
|
|
handleFileInput(e) {
|
|
[...e.target.files].forEach(f => this.processImageFile(f));
|
|
e.target.value = '';
|
|
},
|
|
processImageFile(file) {
|
|
const id = Date.now() + Math.random();
|
|
const entry = { id, name: file.name, dataUrl: '', uploading: true, progress: 0 };
|
|
this.uploads.push(entry);
|
|
const reader = new FileReader();
|
|
reader.onload = ev => {
|
|
const idx = this.uploads.findIndex(u => u.id === id);
|
|
if (idx === -1) return;
|
|
this.uploads[idx].dataUrl = ev.target.result;
|
|
let p = 0;
|
|
const interval = setInterval(() => {
|
|
p += Math.random() * 25 + 10;
|
|
if (p >= 100) {
|
|
p = 100; clearInterval(interval);
|
|
this.uploads[idx].uploading = false;
|
|
const safeName = file.name.replace(/[^\w.-]/g, '_');
|
|
this.content += `\n}...)\n`;
|
|
this.showToast('✓', `Image attached: ${file.name}`);
|
|
}
|
|
this.uploads[idx].progress = p;
|
|
}, 120);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
removeUpload(idx) { this.uploads.splice(idx, 1); },
|
|
|
|
// ── Markdown renderer ──
|
|
renderPreview() {
|
|
if (!this.content.trim()) { this.preview = ''; return; }
|
|
let html = this.content;
|
|
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
const codeBlocks = [];
|
|
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => { codeBlocks.push(`<pre><code>${esc(code.trim())}</code></pre>`); return `\x00cb${codeBlocks.length-1}\x00`; });
|
|
const inlineCodes = [];
|
|
html = html.replace(/`([^`]+)`/g, (_, code) => { inlineCodes.push(`<code>${esc(code)}</code>`); return `\x00ic${inlineCodes.length-1}\x00`; });
|
|
html = html.replace(/^######\s+(.+)$/gm,'<h6>$1</h6>').replace(/^#####\s+(.+)$/gm,'<h5>$1</h5>')
|
|
.replace(/^####\s+(.+)$/gm,'<h4>$1</h4>').replace(/^###\s+(.+)$/gm,'<h3>$1</h3>')
|
|
.replace(/^##\s+(.+)$/gm,'<h2>$1</h2>').replace(/^#\s+(.+)$/gm,'<h1>$1</h1>');
|
|
html = html.replace(/^(?:---|\*\*\*|___)\s*$/gm,'<hr>');
|
|
html = html.replace(/^>\s?(.*)$/gm,'<blockquote>$1</blockquote>');
|
|
html = html.replace(/^\|(.+)\|\n\|[-| :]+\|\n((?:\|.+\|\n?)+)/gm, (_, head, body) => {
|
|
const th = head.split('|').filter(c=>c.trim()).map(c=>`<th>${c.trim()}</th>`).join('');
|
|
const rows = body.trim().split('\n').map(r => `<tr>${r.split('|').filter(c=>c.trim()).map(c=>`<td>${c.trim()}</td>`).join('')}</tr>`).join('');
|
|
return `<table><thead><tr>${th}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
});
|
|
html = html.replace(/^- \[x\]\s+(.+)$/gm,'<li class="task-item"><input type="checkbox" checked disabled> $1</li>');
|
|
html = html.replace(/^- \[ \]\s+(.+)$/gm,'<li class="task-item"><input type="checkbox" disabled> $1</li>');
|
|
html = html.replace(/((?:^- .+\n?)+)/gm, m => `<ul>${m.trim().split('\n').map(l=>`<li>${l.replace(/^- /,'')}</li>`).join('')}</ul>`);
|
|
html = html.replace(/((?:^\d+\. .+\n?)+)/gm, m => `<ol>${m.trim().split('\n').map(l=>`<li>${l.replace(/^\d+\. /,'')}</li>`).join('')}</ol>`);
|
|
html = html.replace(/\*\*\*(.+?)\*\*\*/g,'<strong><em>$1</em></strong>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
|
.replace(/__(.+?)__/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>')
|
|
.replace(/_(.+?)_/g,'<em>$1</em>').replace(/~~(.+?)~~/g,'<del>$1</del>');
|
|
html = html.replace(/@([\w-]+)/g, (_, handle) => USERS.find(u=>u.handle===handle) ? `<span class="mention">@${handle}</span>` : `@${handle}`);
|
|
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,'<img src="$2" alt="$1">');
|
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
|
const lines = html.split('\n');
|
|
const result = [];
|
|
const blockTags = /^<(h[1-6]|ul|ol|li|blockquote|hr|pre|table|thead|tbody|tr|th|td)/;
|
|
for (const line of lines) {
|
|
if (blockTags.test(line.trim()) || line.trim() === '') { result.push(line); }
|
|
else { result.push(line); }
|
|
}
|
|
html = result.join('\n');
|
|
html = html.replace(/\x00ic(\d+)\x00/g,(_,i)=>inlineCodes[+i]).replace(/\x00cb(\d+)\x00/g,(_,i)=>codeBlocks[+i]);
|
|
this.preview = html;
|
|
},
|
|
|
|
clearContent() {
|
|
this.content = ''; this.preview = ''; this.uploads = ''; this.tab = 'write';
|
|
|
|
this.$nextTick(() => { this.getTA()?.focus(); this.autoResize(); });
|
|
},
|
|
|
|
submit() { this.showToast('🚀', 'Submitted!'); },
|
|
|
|
showToast(icon, msg) {
|
|
const t = this.$refs.toast;
|
|
this.$refs.toastIcon.textContent = icon;
|
|
this.$refs.toastMsg.textContent = msg;
|
|
t.classList.add('show');
|
|
setTimeout(() => t.classList.remove('show'), 2400);
|
|
},
|
|
};
|
|
}
|
|
</script> |