generated from coulomb/repo-seed
Implemented all cleanup items from CLEANUP_REPORT.md: Legacy Code Removal: - Removed document-controls-legacy.js wrapper - Updated 4 test files to use DocumentControls directly - Updated scripts/list_components.py acronym mappings - Updated tests/test_component_listing.py expectations Archive and Organization: - Moved relicts/ to docs/prototypes/ with README explaining history - Moved MIGRATION_STATUS.md to docs/migration/ - Removed IMPLEMENTATION_NOTES.md legacy references Test Verification: - All 68 JavaScript tests passing (Jest) - All 3 Python component tests passing - No breaking changes to functionality The codebase is now cleaner with no legacy wrappers or empty directories. Migration is complete and documented. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2316 lines
91 KiB
HTML
Executable File
2316 lines
91 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Changelog</title>
|
||
|
||
<style>
|
||
body {
|
||
font-family: Georgia, Times New Roman, serif;
|
||
max-width: 650px;
|
||
margin: 0 auto;
|
||
padding: 2rem;
|
||
line-height: 1.6;
|
||
color: #333333;
|
||
background-color: #ffffff;
|
||
}
|
||
#markdown-content {
|
||
min-height: 200px;
|
||
}
|
||
h1, h2, h3, h4, h5, h6 {
|
||
color: #333333;
|
||
margin-top: 2rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
h1 {
|
||
text-align: center;
|
||
font-size: 2.2em;
|
||
border-bottom: 2px solid #333333;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
p {
|
||
text-align: justify;
|
||
margin-bottom: 1.2rem;
|
||
}
|
||
pre {
|
||
background-color: #f6f8fa;
|
||
color: #333333;
|
||
padding: 1rem;
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
border: 1px solid #d0d7de;
|
||
}
|
||
code {
|
||
background-color: #f6f8fa;
|
||
color: #333333;
|
||
padding: 0.2em 0.4em;
|
||
border-radius: 3px;
|
||
font-size: 0.9em;
|
||
}
|
||
pre code {
|
||
background: none;
|
||
padding: 0;
|
||
}
|
||
blockquote {
|
||
border-left: 4px solid #dfe2e5;
|
||
margin: 0;
|
||
padding-left: 1rem;
|
||
color: #6a737d;
|
||
}
|
||
table {
|
||
font-size: 0.85em;
|
||
border-collapse: collapse;
|
||
margin: 1rem 0;
|
||
width: 100%;
|
||
border: 1px solid #d0d7de;
|
||
}
|
||
th, td {
|
||
font-size: inherit;
|
||
border: 1px solid #d0d7de;
|
||
padding: 0.5rem;
|
||
text-align: left;
|
||
}
|
||
th {
|
||
background-color: #f6f8fa;
|
||
font-weight: 600;
|
||
}
|
||
a {
|
||
color: #777777;
|
||
text-decoration: underline;
|
||
}
|
||
a:hover {
|
||
color: #999999;
|
||
}
|
||
.markitect-edit-mode .ui-edit-floater-panel {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 4px 12px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-floater-header {
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-floater-header h3 {
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-inline-panel {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 2px 8px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin: 8px 0;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button {
|
||
background: rgba(255,255,255,0.2);
|
||
color: #ffffff;
|
||
border: 1px solid #ff1493;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
min-width: 70px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button:hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button:active,
|
||
.markitect-edit-mode .ui-edit-button.active {
|
||
background: rgba(255,20,147,0.5);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-accept {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-accept:hover {
|
||
background: #388e3c;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel:hover {
|
||
background: #d32f2f;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-reset {
|
||
background: #ff9800;
|
||
color: white;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-reset:hover {
|
||
background: #f57c00;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-secondary:hover {
|
||
background: #545b62;
|
||
}
|
||
.markitect-edit-mode .ui-edit-section-frame {
|
||
border: 2px solid #ff1493;
|
||
box-shadow: 0 0 0 3px #ff149333;
|
||
}
|
||
.markitect-edit-mode .ui-edit-textarea {
|
||
border: 1px solid #ff1493;
|
||
color: #ffffff;
|
||
background: rgba(255,255,255,0.2);
|
||
}
|
||
.markitect-edit-mode .ui-edit-textarea:focus {
|
||
border-color: #ff1493;
|
||
box-shadow: 0 0 0 2px #ff149333;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.3s, visibility 0.3s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-overlay.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 8px 32px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
border-radius: 8px;
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
width: 90%;
|
||
overflow: hidden;
|
||
transform: scale(0.9) translateY(-20px);
|
||
transition: transform 0.3s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-overlay.active .ui-edit-modal {
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-header {
|
||
padding: 20px 24px 16px;
|
||
border-bottom: 1px solid #ff1493;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #ffffff;
|
||
padding: 4px;
|
||
border-radius: 4px;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background-color 0.2s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-close:hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-body {
|
||
padding: 20px 24px;
|
||
overflow-y: auto;
|
||
max-height: 60vh;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-content {
|
||
white-space: pre-line;
|
||
line-height: 1.5;
|
||
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
font-size: 14px;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-section:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-section-title {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-modal-footer {
|
||
padding: 16px 24px 20px;
|
||
border-top: 1px solid #ff1493;
|
||
text-align: right;
|
||
}
|
||
|
||
/* Confirmation Dialog Styles */
|
||
.markitect-edit-mode .ui-edit-confirmation-modal {
|
||
max-width: 500px;
|
||
}
|
||
.markitect-edit-mode .ui-edit-confirmation-content {
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
margin-bottom: 24px;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-edit-mode .ui-edit-confirmation-warning {
|
||
background: linear-gradient(45deg, #ffa500, #ff8c00);
|
||
border: 1px solid #ff1493;
|
||
color: #ffffff;
|
||
padding: 12px 16px;
|
||
border-radius: 6px;
|
||
margin: 16px 0;
|
||
font-size: 14px;
|
||
}
|
||
.markitect-edit-mode .ui-edit-confirmation-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-confirm {
|
||
background: linear-gradient(45deg, #ff0066, #cc0044);
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, transform 0.1s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-confirm:hover {
|
||
background: linear-gradient(45deg, #ff3388, #dd1155);
|
||
transform: translateY(-1px);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-confirm:active {
|
||
transform: translateY(0);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-confirm:focus {
|
||
outline: 2px solid #ff1493;
|
||
outline-offset: 2px;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel {
|
||
background: linear-gradient(45deg, #8a2be2, #4b0082);
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, transform 0.1s;
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel:hover {
|
||
background: linear-gradient(45deg, #9932cc, #6a1a9a);
|
||
transform: translateY(-1px);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel:active {
|
||
transform: translateY(0);
|
||
}
|
||
.markitect-edit-mode .ui-edit-button-cancel:focus {
|
||
outline: 2px solid #ff1493;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
/* Document Scroll Indicators */
|
||
.ui-scroll-indicator {
|
||
position: fixed;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60px;
|
||
height: 30px;
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
border-radius: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.2s ease, background-color 0.2s ease;
|
||
z-index: 1000;
|
||
box-shadow: 0 4px 12px rgba(255,20,147,0.4);
|
||
}
|
||
.ui-scroll-indicator:hover {
|
||
transform: translateX(-50%) scale(1.05);
|
||
}
|
||
.ui-scroll-indicator:not(.disabled):hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
.ui-scroll-indicator.active {
|
||
opacity: 0.9;
|
||
visibility: visible;
|
||
}
|
||
.ui-scroll-indicator.disabled {
|
||
background: rgba(255,20,147,0.5);
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
.ui-scroll-indicator.disabled:hover {
|
||
transform: translateX(-50%);
|
||
background: rgba(255,20,147,0.5);
|
||
}
|
||
.ui-scroll-indicator-up {
|
||
top: 20px;
|
||
}
|
||
.ui-scroll-indicator-down {
|
||
bottom: 20px;
|
||
}
|
||
.ui-scroll-indicator::before {
|
||
content: '';
|
||
width: 0;
|
||
height: 0;
|
||
border-style: solid;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
.ui-scroll-indicator-up::before {
|
||
border-left: 8px solid transparent;
|
||
border-right: 8px solid transparent;
|
||
border-bottom: 12px solid #ffffff;
|
||
}
|
||
.ui-scroll-indicator-down::before {
|
||
border-left: 8px solid transparent;
|
||
border-right: 8px solid transparent;
|
||
border-top: 12px solid #ffffff;
|
||
}
|
||
.ui-scroll-indicator.disabled.ui-scroll-indicator-up::before {
|
||
border-bottom-color: linear-gradient(45deg, #8a2be2, #4b0082);
|
||
}
|
||
.ui-scroll-indicator.disabled.ui-scroll-indicator-down::before {
|
||
border-top-color: linear-gradient(45deg, #8a2be2, #4b0082);
|
||
}
|
||
|
||
/* Insert Mode Specific Styles */
|
||
.markitect-insert-mode .ui-edit-floater-panel {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 4px 12px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-floater-header {
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-floater-header h3 {
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-inline-panel {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 2px 8px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin: 8px 0;
|
||
}
|
||
.markitect-insert-mode .ui-insert-protected-panel {
|
||
border-left: 4px solid #ff9800;
|
||
}
|
||
.markitect-insert-mode .ui-insert-heading-display {
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
padding: 8px 12px;
|
||
background: linear-gradient(45deg, #ffa500, #ff8c00);
|
||
border: 1px solid #ff1493;
|
||
border-radius: 4px;
|
||
border-left: 4px solid #007bff;
|
||
color: #ffffff;
|
||
margin-bottom: 8px;
|
||
}
|
||
.markitect-insert-mode .ui-insert-content-editor {
|
||
border: 1px solid #ff1493;
|
||
color: #ffffff;
|
||
background: rgba(255,255,255,0.2);
|
||
}
|
||
.markitect-insert-mode .ui-insert-content-editor:focus {
|
||
border-color: #ff1493;
|
||
box-shadow: 0 0 0 2px #ff149333;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button {
|
||
background: rgba(255,255,255,0.2);
|
||
color: #ffffff;
|
||
border: 1px solid #ff1493;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
min-width: 70px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button:hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
.markitect-insert-mode .ui-edit-button:active,
|
||
.markitect-insert-mode .ui-edit-button.active {
|
||
background: rgba(255,20,147,0.5);
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-accept {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-accept:hover {
|
||
background: #388e3c;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-cancel {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-cancel:hover {
|
||
background: #d32f2f;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-reset {
|
||
background: #ff9800;
|
||
color: white;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-reset:hover {
|
||
background: #f57c00;
|
||
}
|
||
.markitect-insert-mode .ui-edit-section-frame {
|
||
border: 2px solid #ff1493;
|
||
box-shadow: 0 0 0 3px #ff149333;
|
||
}
|
||
|
||
/* Modal Overlay and Dialog Styles for Insert Mode */
|
||
.markitect-insert-mode .ui-edit-modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.3s, visibility 0.3s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-overlay.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal {
|
||
background: linear-gradient(45deg, #ff6b35, #f7931e, #ffd23f, #06ffa5);
|
||
border: 1px solid #ff1493;
|
||
box-shadow: 0 8px 32px rgba(255,20,147,0.4);
|
||
color: #ffffff;
|
||
border-radius: 8px;
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
width: 90%;
|
||
overflow: hidden;
|
||
transform: scale(0.9) translateY(-20px);
|
||
transition: transform 0.3s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-overlay.active .ui-edit-modal {
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-header {
|
||
padding: 20px 24px 16px;
|
||
border-bottom: 1px solid #ff1493;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-close {
|
||
background: transparent;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #ffffff;
|
||
padding: 0;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-close:hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-body {
|
||
padding: 20px 24px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-section {
|
||
margin-bottom: 8px;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-modal-footer {
|
||
padding: 16px 24px 20px;
|
||
border-top: 1px solid #ff1493;
|
||
text-align: right;
|
||
}
|
||
|
||
/* Confirmation Dialog Styles for Insert Mode */
|
||
.markitect-insert-mode .ui-edit-confirmation-modal {
|
||
max-width: 500px;
|
||
}
|
||
.markitect-insert-mode .ui-edit-confirmation-content {
|
||
font-size: 16px;
|
||
line-height: 1.5;
|
||
margin-bottom: 24px;
|
||
color: #ffffff;
|
||
}
|
||
.markitect-insert-mode .ui-edit-confirmation-warning {
|
||
background: linear-gradient(45deg, #ffa500, #ff8c00);
|
||
color: #ffffff;
|
||
border: 1px solid #ff1493;
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
margin: 16px 0;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
.markitect-insert-mode .ui-edit-confirmation-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 24px;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-confirm {
|
||
background: #dc3545;
|
||
color: white;
|
||
border: 1px solid #dc3545;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-confirm:hover {
|
||
background: #c82333;
|
||
border-color: #bd2130;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-cancel {
|
||
background: rgba(255,255,255,0.2);
|
||
color: #ffffff;
|
||
border: 1px solid #ff1493;
|
||
padding: 10px 20px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.markitect-insert-mode .ui-edit-button-cancel:hover {
|
||
background: rgba(255,20,147,0.3);
|
||
}
|
||
</style>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"
|
||
onload="window.markitectMarkedLoaded = true"
|
||
onerror="window.markitectMarkedError = true"></script>
|
||
</head>
|
||
<body class="markitect-edit-mode">
|
||
|
||
<div id="markdown-content"></div>
|
||
|
||
<script>
|
||
const markdownContent = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n## [0.6.0] - 2025-10-28\n\n### Added\n- **Custom Status Modal System**: Professional theme-consistent status dialogs replacing browser alerts with proper branding and accessibility\n- **HTML Generation Dogtag**: Automatic attribution with timestamp and username linking for generated HTML documents\n- **Enhanced Link Navigation**: All document links now open in new tabs without triggering edit mode for improved user experience\n- **Comprehensive UI Framework Documentation**: Complete guide (UserInterfaceFramework.md) for consistent UI development patterns\n- **Database Integration**: Added store_document method to CleanDocumentManager with proper front matter parsing\n- **Enhanced AST Processing**: Improved title extraction from front matter and heading detection with cache file generation\n\n### Changed\n- **Complete Document Manager Cleanup**: Removed 2000+ lines of legacy code while maintaining full backward compatibility\n- **Clean Architecture Implementation**: DocumentManager now extends CleanDocumentManager with clean wrapper pattern\n- **Improved Error Handling**: Enhanced validation and graceful error recovery throughout the system\n- **Standardized CSS Naming**: Consistent class naming conventions across all UI components\n\n### Fixed\n- **Test Suite Compatibility**: Updated all tests to work with clean implementation architecture\n- **JavaScript Syntax Issues**: Resolved template literal and string escaping problems in generated HTML\n- **Link Behavior**: Fixed issue where document links were incorrectly triggering edit mode\n- **Front Matter Parsing**: Proper integration with FrontMatterParser for metadata extraction\n\n### Technical\n- Added --nodogtag CLI option for clean output when attribution is not desired\n- Enhanced ingest_file method with proper title extraction from front matter and headings\n- Implemented theme-aware modal overlay patterns with proper CSS styling\n- Fixed CSS escape sequences and JavaScript syntax validation issues\n\n## [0.5.0] - 2025-10-26\n\n### Added\n- **Clean TDD-Driven Editor Architecture**: Complete rewrite with object-oriented JavaScript architecture featuring Section, SectionManager, and DOMRenderer classes\n- **Enhanced Test Framework**: Comprehensive testing framework with clean separation of concerns for robust development\n- **Multiple Concurrent Section Editing**: Support for editing multiple sections simultaneously with intelligent management\n- **Intelligent Section Splitting**: Advanced heading detection and section management capabilities\n- **Four-Layer Content Management**: Sophisticated content state management (original, current, pending, editing layers)\n- **Enhanced Status Dialog**: Repository info display showing version, git commit status, and actual save filename\n- **Elegant Slide-in Control Panel**: Floating control panel for edit mode with improved UX\n- **Intelligent Auto-sizing Textarea**: Optimal editing experience with smart textarea resizing\n- **Enhanced Empty Line Preservation**: Better markdown structure preservation with automatic paragraph separation\n\n### Fixed\n- **Textarea Sizing and Font Preservation**: Resolved sizing issues and maintained consistent font rendering\n- **Markdown Structure Preservation**: Fixed roundtrip formatting issues in save functionality\n- **Section Duplication Prevention**: Eliminated duplicate sections when saving edited content\n- **Section Position Preservation**: Prevented unwanted section jumping during editing\n- **CSS Embedding Issues**: Resolved import errors in HTML template generation\n- **Control Panel UX**: Hidden control ribbon when panel is expanded for cleaner interface\n\n### Changed\n- **Action Semantics**: Proper implementation of Accept, Cancel, and Reset operations\n- **Global Reset Functionality**: Enhanced reset capabilities across the editor\n- **Makefile Organization**: Reorganized installation targets for better user experience\n\n### Technical Improvements\n- Complete legacy editor system replacement\n- Test-driven development approach implementation\n- Enhanced UI/UX with better section positioning\n- Improved content management workflow\n\n## [0.4.0] - 2025-10-25\n\n### Added\n- feat: add comprehensive testing and error tracking for edit mode\n\n### Fixed\n- fix: resolve md-render --edit functionality and add enhanced version tracking\n- fix: resolve critical JavaScript syntax errors in md-render --edit\n- fix: resolve md-ingest Path object conversion error\n\n### Other\n- chore: clean up repository documentation files for release\n\n## [0.3.0] - 2025-10-25\n\n### Added\n- **Kaizen-agentic Framework Integration**: Integrated capability submodule for enhanced development workflow\n- **Test Reorganization System**: Reorganized tests by capability with improved modularity\n- **Capability Inclusion Management**: Comprehensive system for managing capability inclusions\n- **Todofile System**: Implemented todofile system to replace NEXT.md for better task tracking\n\n### Changed\n- **Directory Organization**: Logical separation and reorganization of project structure\n- **Historical File Organization**: Cleaner structure with better file organization\n\n## [0.2.0] - 2025-10-20\n\n### Added\n- **GraphQL Interface**: Advanced querying capabilities with full GraphQL implementation\n- **Full-text Search**: FTS5 backend integration for powerful search functionality\n- **Plugin Architecture**: Extensible framework with comprehensive plugin support\n- **Query Paradigms**: 14 different query paradigms for flexible data access\n- **Cost Management**: Activity tracking and resource cost management\n- **Template Rendering**: Template system with validation capabilities\n- **CLI Consolidation**: Unified command-line interface\n- **Production Asset Management**: Content-addressable storage system\n- **17 Kaizen-agentic Agents**: Integrated development agent ecosystem\n\n### Changed\n- **Performance Optimization**: 60-85% performance improvement through system optimization\n- **Error Handling**: Enterprise-grade error handling and recovery mechanisms\n- **Resource Management**: Memory-efficient and scalable architecture\n\n### Fixed\n- **Cross-platform Validation**: Comprehensive validation for Unix/Windows/macOS\n- **Type Safety**: Enhanced type safety and security validation\n- **Test Coverage**: 1983/1983 tests passing (100% success rate)\n\n## [0.1.0] - 2025-10-15\n\n### Added\n- **Development Infrastructure**: Comprehensive Makefile for development workflow\n- **Project Documentation**: ProjectStatusDigest.md and ProjectDiary.md for tracking\n- **TDD Workspace System**: Structured Test-Driven Development workflow implementation\n- **Issue Management**: Gitea integration for issue tracking and management\n- **Virtual Environment Management**: Enhanced venv detection and shell activation\n- **Wiki Integration**: Submodule tracking for project documentation\n- **Core Repository Setup**: Initial project structure and configuration\n\n### Changed\n- **Build System**: Enhanced build targets with venv Python and PYTHONPATH support\n- **Target Naming**: Renamed workspace targets to TDD Workspace with tdd- prefix\n\nxxx\n\n\n---\n*-- html from markdown by <a href=\"https://coulomb.social/open/MarkiTect\" target=\"_blank\">MarkiTect</a> on 2025-10-28 23:52:58 by <a href=\"https://coulomb.social/open/worsch\" target=\"_blank\">worsch</a>*";
|
||
|
||
const MARKITECT_EDIT_MODE = true;
|
||
const MARKITECT_EDITOR_CONFIG = {
|
||
mode: 'edit',
|
||
theme: 'github',
|
||
keyboardShortcuts: true,
|
||
autosave: false,
|
||
sections: true,
|
||
originalFilename: 'CHANGELOG',
|
||
version: 'Markitect v0.5.0.dev+dd3a000',
|
||
repoName: 'Markitect'
|
||
};
|
||
|
||
// Make config available globally
|
||
window.editorConfig = MARKITECT_EDITOR_CONFIG;
|
||
|
||
|
||
// Clean Editor Architecture
|
||
/**
|
||
* Test-Driven Section Editor Implementation
|
||
*
|
||
* A clean, object-oriented approach to handling section editing
|
||
* that can be tested independently of the DOM.
|
||
*/
|
||
|
||
// Enums for clear state management
|
||
const EditState = Object.freeze({
|
||
ORIGINAL: 'original',
|
||
EDITING: 'editing',
|
||
MODIFIED: 'modified',
|
||
SAVED: 'saved'
|
||
});
|
||
|
||
const SectionType = Object.freeze({
|
||
HEADING: 'heading',
|
||
PARAGRAPH: 'paragraph',
|
||
LIST: 'list',
|
||
CODE: 'code',
|
||
BLOCKQUOTE: 'blockquote'
|
||
});
|
||
|
||
/**
|
||
* Section class - Core business logic for a single editable section
|
||
*/
|
||
class Section {
|
||
constructor(id, originalMarkdown, sectionType = SectionType.PARAGRAPH) {
|
||
this.id = id;
|
||
this.originalMarkdown = originalMarkdown;
|
||
this.currentMarkdown = originalMarkdown;
|
||
this.editingMarkdown = null;
|
||
this.pendingMarkdown = null;
|
||
this.sectionType = sectionType;
|
||
this.headingLevel = Section.detectHeadingLevel(originalMarkdown);
|
||
this.state = EditState.ORIGINAL;
|
||
this.domElement = null;
|
||
this.lastSaved = null;
|
||
this.created = new Date();
|
||
}
|
||
|
||
startEdit() {
|
||
if (this.state === EditState.EDITING) {
|
||
throw new Error(`Section ${this.id} is already being edited`);
|
||
}
|
||
this.editingMarkdown = this.pendingMarkdown || this.currentMarkdown;
|
||
this.state = EditState.EDITING;
|
||
return this.editingMarkdown;
|
||
}
|
||
|
||
updateContent(markdown) {
|
||
if (this.state !== EditState.EDITING) {
|
||
throw new Error(`Section ${this.id} is not in editing state`);
|
||
}
|
||
this.editingMarkdown = markdown;
|
||
}
|
||
|
||
acceptChanges() {
|
||
if (this.state !== EditState.EDITING) {
|
||
throw new Error(`Section ${this.id} is not in editing state`);
|
||
}
|
||
this.currentMarkdown = this.editingMarkdown;
|
||
this.editingMarkdown = null;
|
||
this.pendingMarkdown = null;
|
||
this.state = EditState.SAVED;
|
||
this.lastSaved = new Date();
|
||
return this.currentMarkdown;
|
||
}
|
||
|
||
cancelChanges() {
|
||
if (this.state !== EditState.EDITING) {
|
||
throw new Error(`Section ${this.id} is not in editing state`);
|
||
}
|
||
this.editingMarkdown = null;
|
||
if (this.pendingMarkdown !== null) {
|
||
this.state = EditState.MODIFIED;
|
||
return this.pendingMarkdown;
|
||
} else if (this.lastSaved !== null) {
|
||
this.state = EditState.SAVED;
|
||
return this.currentMarkdown;
|
||
} else {
|
||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||
return this.currentMarkdown;
|
||
}
|
||
}
|
||
|
||
resetToOriginal() {
|
||
this.currentMarkdown = this.originalMarkdown;
|
||
this.editingMarkdown = null;
|
||
this.pendingMarkdown = null;
|
||
this.lastSaved = null;
|
||
this.state = EditState.ORIGINAL;
|
||
return this.originalMarkdown;
|
||
}
|
||
|
||
stopEditing() {
|
||
if (this.state !== EditState.EDITING) {
|
||
return this.state;
|
||
}
|
||
|
||
// If we have editing changes that differ from current content, preserve them as pending
|
||
if (this.editingMarkdown && this.editingMarkdown !== this.currentMarkdown) {
|
||
this.pendingMarkdown = this.editingMarkdown;
|
||
this.state = EditState.MODIFIED; // Has pending changes
|
||
} else {
|
||
// No changes made during this edit session
|
||
this.pendingMarkdown = null;
|
||
if (this.lastSaved !== null) {
|
||
this.state = EditState.SAVED;
|
||
} else {
|
||
this.state = this.hasChanges() ? EditState.MODIFIED : EditState.ORIGINAL;
|
||
}
|
||
}
|
||
|
||
this.editingMarkdown = null;
|
||
return this.state;
|
||
}
|
||
|
||
hasChanges() {
|
||
return this.currentMarkdown !== this.originalMarkdown;
|
||
}
|
||
|
||
isEditing() {
|
||
return this.state === EditState.EDITING;
|
||
}
|
||
|
||
getStatus() {
|
||
return {
|
||
id: this.id,
|
||
state: this.state,
|
||
hasChanges: this.hasChanges(),
|
||
isEditing: this.isEditing(),
|
||
contentLength: this.currentMarkdown.length,
|
||
lastSaved: this.lastSaved,
|
||
sectionType: this.sectionType
|
||
};
|
||
}
|
||
|
||
static generateId(content, position) {
|
||
const str = content.substring(0, 100) + position.toString();
|
||
let hash = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
const char = str.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash;
|
||
}
|
||
return `section_${Math.abs(hash)}_${position}`;
|
||
}
|
||
|
||
static detectType(markdown) {
|
||
const trimmed = markdown.trim();
|
||
if (trimmed.startsWith('#')) return SectionType.HEADING;
|
||
if (trimmed.startsWith('```')) return SectionType.CODE;
|
||
if (trimmed.startsWith('>')) return SectionType.BLOCKQUOTE;
|
||
if (trimmed.startsWith('-') || trimmed.startsWith('*') || /^\d+\./.test(trimmed)) {
|
||
return SectionType.LIST;
|
||
}
|
||
return SectionType.PARAGRAPH;
|
||
}
|
||
|
||
static detectHeadingLevel(markdown) {
|
||
const trimmed = markdown.trim();
|
||
const match = trimmed.match(/^(#{1,6})\s/);
|
||
return match ? match[1].length : null;
|
||
}
|
||
|
||
isHeading() {
|
||
return this.sectionType === SectionType.HEADING;
|
||
}
|
||
|
||
isProtectedHeading() {
|
||
if (!this.isHeading()) return false;
|
||
// Check if we're in insert mode and if this heading level is protected
|
||
const config = window.editorConfig || {};
|
||
const restrictedLevels = config.restrictedHeadingLevels || [];
|
||
return config.mode === 'insert' && restrictedLevels.includes(this.headingLevel);
|
||
}
|
||
|
||
getHeadingText() {
|
||
if (!this.isHeading()) return null;
|
||
// Extract first line for heading text
|
||
const firstLine = this.originalMarkdown.trim().split('\n')[0];
|
||
const match = firstLine.match(/^(#{1,6})\s+(.+)$/);
|
||
return match ? match[2] : null;
|
||
}
|
||
|
||
getHeadingContent() {
|
||
if (!this.isHeading()) return this.currentMarkdown;
|
||
const lines = this.currentMarkdown.split('\n');
|
||
// Return content after the heading line
|
||
return lines.slice(1).join('\n');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SectionManager class - Manages the collection of sections
|
||
*/
|
||
class SectionManager {
|
||
constructor() {
|
||
this.sections = new Map();
|
||
// Note: Removed single editingSection tracking to allow multiple concurrent edits
|
||
this.listeners = new Map();
|
||
}
|
||
|
||
on(event, callback) {
|
||
if (!this.listeners.has(event)) {
|
||
this.listeners.set(event, []);
|
||
}
|
||
this.listeners.get(event).push(callback);
|
||
}
|
||
|
||
emit(event, data) {
|
||
if (this.listeners.has(event)) {
|
||
this.listeners.get(event).forEach(callback => callback(data));
|
||
}
|
||
}
|
||
|
||
createSectionsFromMarkdown(markdownContent) {
|
||
const lines = markdownContent.split('\n');
|
||
const sections = [];
|
||
let currentSection = '';
|
||
let position = 0;
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i];
|
||
const isHeading = /^#{1,6}\s/.test(line);
|
||
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
|
||
const isNewSection = isHeading || isNewParagraph;
|
||
|
||
if (isNewSection && currentSection.trim()) {
|
||
const sectionId = Section.generateId(currentSection, position);
|
||
const sectionType = Section.detectType(currentSection);
|
||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||
sections.push(section);
|
||
this.sections.set(sectionId, section);
|
||
position++;
|
||
currentSection = line;
|
||
} else {
|
||
if (currentSection) currentSection += '\n';
|
||
currentSection += line;
|
||
}
|
||
}
|
||
|
||
if (currentSection.trim()) {
|
||
const sectionId = Section.generateId(currentSection, position);
|
||
const sectionType = Section.detectType(currentSection);
|
||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||
sections.push(section);
|
||
this.sections.set(sectionId, section);
|
||
}
|
||
|
||
this.emit('sections-created', { sections, count: sections.length });
|
||
return sections;
|
||
}
|
||
|
||
startEditing(sectionId) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
|
||
// Check if section is already being edited
|
||
if (section.isEditing()) {
|
||
console.log('Section already in editing state:', sectionId);
|
||
return section.editingMarkdown;
|
||
}
|
||
|
||
const content = section.startEdit();
|
||
// Note: No longer tracking single editingSection - allowing multiple
|
||
this.emit('edit-started', { sectionId, content, section: section.getStatus() });
|
||
return content;
|
||
}
|
||
|
||
updateContent(sectionId, markdown) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
section.updateContent(markdown);
|
||
this.emit('content-updated', { sectionId, markdown, section: section.getStatus() });
|
||
}
|
||
|
||
acceptChanges(sectionId) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
|
||
// For protected headings in insert mode, validate that heading hasn't changed
|
||
if (section.isProtectedHeading()) {
|
||
const originalHeadingLine = section.originalMarkdown.split('\n')[0];
|
||
const newHeadingLine = section.editingMarkdown.split('\n')[0];
|
||
if (originalHeadingLine !== newHeadingLine) {
|
||
throw new Error(`Cannot modify protected heading in insert mode. Heading level ${section.headingLevel} is read-only.`);
|
||
}
|
||
}
|
||
|
||
// Check if the edited content contains new headings that would create splits
|
||
const newContent = section.editingMarkdown;
|
||
const originalContent = section.originalMarkdown;
|
||
const shouldSplit = this.checkForSectionSplits(newContent, originalContent);
|
||
|
||
if (shouldSplit) {
|
||
// Handle section splitting
|
||
this.handleSectionSplit(sectionId, newContent);
|
||
} else {
|
||
// Normal accept without splitting
|
||
const content = section.acceptChanges();
|
||
// Note: No longer tracking single editingSection
|
||
this.emit('changes-accepted', { sectionId, content, section: section.getStatus() });
|
||
}
|
||
|
||
return section.currentMarkdown;
|
||
}
|
||
|
||
checkForSectionSplits(content, originalContent) {
|
||
if (!content) return false;
|
||
|
||
// Split by lines and check for headings
|
||
const lines = content.split('\n');
|
||
const originalLines = originalContent ? originalContent.split('\n') : [];
|
||
|
||
let newHeadingCount = 0;
|
||
let originalHeadingCount = 0;
|
||
|
||
// Count headings in new content
|
||
for (const line of lines) {
|
||
if (/^#{1,6}\s/.test(line.trim())) {
|
||
newHeadingCount++;
|
||
}
|
||
}
|
||
|
||
// Count headings in original content
|
||
for (const line of originalLines) {
|
||
if (/^#{1,6}\s/.test(line.trim())) {
|
||
originalHeadingCount++;
|
||
}
|
||
}
|
||
|
||
// Split if:
|
||
// 1. We have multiple headings now, OR
|
||
// 2. We added headings where there were none before, OR
|
||
// 3. We have more headings than we started with
|
||
return newHeadingCount > 1 ||
|
||
(originalHeadingCount === 0 && newHeadingCount > 0) ||
|
||
newHeadingCount > originalHeadingCount;
|
||
}
|
||
|
||
handleSectionSplit(originalSectionId, content) {
|
||
console.log('Splitting section:', originalSectionId);
|
||
|
||
const originalSection = this.sections.get(originalSectionId);
|
||
if (!originalSection) return;
|
||
|
||
// Accept the current changes first
|
||
originalSection.acceptChanges();
|
||
|
||
// Split the content into new sections
|
||
const newSections = this.createSectionsFromContent(content, originalSectionId);
|
||
|
||
// Get all sections as an ordered array to maintain document order
|
||
const allSectionsArray = Array.from(this.sections.values());
|
||
const originalIndex = allSectionsArray.findIndex(s => s.id === originalSectionId);
|
||
|
||
// Clear the sections map and rebuild it with proper order
|
||
this.sections.clear();
|
||
|
||
// Add sections before the original
|
||
for (let i = 0; i < originalIndex; i++) {
|
||
const section = allSectionsArray[i];
|
||
this.sections.set(section.id, section);
|
||
}
|
||
|
||
// Add the new split sections
|
||
newSections.forEach(section => {
|
||
this.sections.set(section.id, section);
|
||
});
|
||
|
||
// Add sections after the original
|
||
for (let i = originalIndex + 1; i < allSectionsArray.length; i++) {
|
||
const section = allSectionsArray[i];
|
||
this.sections.set(section.id, section);
|
||
}
|
||
|
||
// Note: No longer tracking single editingSection
|
||
|
||
// Emit event to trigger UI re-render
|
||
this.emit('section-split', {
|
||
originalSectionId,
|
||
newSections: newSections.map(s => s.getStatus()),
|
||
allSections: Array.from(this.sections.values())
|
||
});
|
||
}
|
||
|
||
createSectionsFromContent(content, baseSectionId) {
|
||
const lines = content.split('\n');
|
||
const sections = [];
|
||
let currentSection = '';
|
||
let position = 0;
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i];
|
||
const isHeading = /^#{1,6}\s/.test(line.trim());
|
||
|
||
if (isHeading) {
|
||
// When we encounter a heading, complete any previous section
|
||
if (currentSection.trim()) {
|
||
const sectionId = `${baseSectionId}_split_${position}`;
|
||
const sectionType = Section.detectType(currentSection);
|
||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||
sections.push(section);
|
||
position++;
|
||
}
|
||
// Start new section with this heading
|
||
currentSection = line;
|
||
} else {
|
||
// Add content to current section
|
||
if (currentSection) currentSection += '\n';
|
||
currentSection += line;
|
||
}
|
||
}
|
||
|
||
// Add the final section if it has content
|
||
if (currentSection.trim()) {
|
||
const sectionId = `${baseSectionId}_split_${position}`;
|
||
const sectionType = Section.detectType(currentSection);
|
||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||
sections.push(section);
|
||
}
|
||
|
||
return sections;
|
||
}
|
||
|
||
cancelChanges(sectionId) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
const content = section.cancelChanges();
|
||
// Note: No longer tracking single editingSection
|
||
this.emit('changes-cancelled', { sectionId, content, section: section.getStatus() });
|
||
return content;
|
||
}
|
||
|
||
resetToOriginal(sectionId) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
const content = section.resetToOriginal();
|
||
this.emit('section-reset', { sectionId, content, section: section.getStatus() });
|
||
return content;
|
||
}
|
||
|
||
stopEditing(sectionId) {
|
||
const section = this.sections.get(sectionId);
|
||
if (!section) {
|
||
throw new Error(`Section ${sectionId} not found`);
|
||
}
|
||
|
||
const newState = section.stopEditing();
|
||
// Note: No longer tracking single editingSection
|
||
|
||
this.emit('edit-stopped', { sectionId, newState, section: section.getStatus() });
|
||
return newState;
|
||
}
|
||
|
||
getAllSections() {
|
||
return Array.from(this.sections.values());
|
||
}
|
||
|
||
getDocumentMarkdown() {
|
||
return this.getAllSections()
|
||
.map(section => section.currentMarkdown)
|
||
.join('\n\n');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* DOM Renderer - Handles DOM interactions
|
||
*/
|
||
class DOMRenderer {
|
||
constructor(sectionManager, containerElement) {
|
||
this.sectionManager = sectionManager;
|
||
this.container = containerElement;
|
||
// Note: Removed single currentSection tracking to allow multiple concurrent edits
|
||
this.editingSections = new Set(); // Track multiple editing sections
|
||
|
||
this.handleSectionClick = this.handleSectionClick.bind(this);
|
||
this.handleAccept = this.handleAccept.bind(this);
|
||
this.handleCancel = this.handleCancel.bind(this);
|
||
this.handleReset = this.handleReset.bind(this);
|
||
this.handleKeydown = this.handleKeydown.bind(this);
|
||
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
setupEventListeners() {
|
||
this.sectionManager.on('sections-created', (data) => {
|
||
this.renderAllSections(data.sections);
|
||
});
|
||
this.sectionManager.on('edit-started', (data) => {
|
||
this.showEditor(data.sectionId, data.content);
|
||
});
|
||
this.sectionManager.on('edit-stopped', (data) => {
|
||
this.hideEditor(data.sectionId);
|
||
// Don't update content - let pending changes remain
|
||
});
|
||
this.sectionManager.on('changes-accepted', (data) => {
|
||
this.hideEditor(data.sectionId);
|
||
this.updateSectionContent(data.sectionId, data.content);
|
||
});
|
||
this.sectionManager.on('changes-cancelled', (data) => {
|
||
this.hideEditor(data.sectionId);
|
||
this.updateSectionContent(data.sectionId, data.content);
|
||
});
|
||
this.sectionManager.on('section-reset', (data) => {
|
||
this.updateTextareaContent(data.content, data.sectionId);
|
||
});
|
||
this.sectionManager.on('section-split', (data) => {
|
||
console.log('Handling section split in UI');
|
||
this.handleSectionSplit(data);
|
||
});
|
||
}
|
||
|
||
renderAllSections(sections) {
|
||
this.container.innerHTML = '';
|
||
sections.forEach(section => {
|
||
const element = this.createSectionElement(section);
|
||
section.domElement = element;
|
||
this.container.appendChild(element);
|
||
});
|
||
this.container.addEventListener('click', this.handleSectionClick);
|
||
}
|
||
|
||
createSectionElement(section) {
|
||
const element = document.createElement('div');
|
||
element.setAttribute('data-section-id', section.id);
|
||
|
||
if (typeof marked !== 'undefined') {
|
||
const html = marked.parse(section.currentMarkdown);
|
||
// Add target="_blank" to all links
|
||
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
||
element.innerHTML = htmlWithTargetBlank;
|
||
} else {
|
||
element.innerHTML = `<p>${section.currentMarkdown}</p>`;
|
||
}
|
||
|
||
// Setup styling and event handlers
|
||
this.setupSectionElement(element);
|
||
|
||
return element;
|
||
}
|
||
|
||
handleSectionClick(event) {
|
||
// Don't handle clicks on form elements, buttons, or links
|
||
if (event.target.closest('textarea, button, input, a')) {
|
||
return;
|
||
}
|
||
|
||
const sectionElement = event.target.closest('.ui-edit-section');
|
||
if (!sectionElement) return;
|
||
|
||
const sectionId = sectionElement.getAttribute('data-section-id');
|
||
if (!sectionId) return;
|
||
|
||
// Check if this section is already being edited
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
if (section && section.isEditing()) {
|
||
console.log('Section already being edited:', sectionId);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('Starting edit for section:', sectionId);
|
||
this.sectionManager.startEditing(sectionId);
|
||
} catch (error) {
|
||
console.error('Failed to start editing:', error);
|
||
}
|
||
}
|
||
|
||
showEditor(sectionId, content) {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (!element) return;
|
||
|
||
this.hideCurrentEditor();
|
||
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
const isProtectedHeading = section && section.isProtectedHeading();
|
||
|
||
const editorContainer = document.createElement('div');
|
||
editorContainer.className = isProtectedHeading ? 'ui-edit-inline-panel ui-insert-protected-panel' : 'ui-edit-inline-panel';
|
||
editorContainer.style.cssText = `
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
width: 100%;
|
||
`;
|
||
|
||
// If this is a protected heading, show the heading display
|
||
if (isProtectedHeading) {
|
||
const headingDisplay = document.createElement('div');
|
||
headingDisplay.className = 'ui-insert-heading-display';
|
||
const headingText = section.getHeadingText();
|
||
const headingLevel = section.headingLevel;
|
||
const headingMarkdown = '#'.repeat(headingLevel) + ' ' + headingText;
|
||
headingDisplay.textContent = headingMarkdown;
|
||
headingDisplay.style.cssText = `
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
padding: 8px 12px;
|
||
background: rgba(0, 0, 0, 0.05);
|
||
border-radius: 4px;
|
||
border-left: 4px solid #007bff;
|
||
color: #333;
|
||
`;
|
||
editorContainer.appendChild(headingDisplay);
|
||
}
|
||
|
||
// Create content editing area
|
||
const editingArea = document.createElement('div');
|
||
editingArea.style.cssText = `
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
`;
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = isProtectedHeading ? 'ui-edit-textarea ui-insert-content-editor' : 'ui-edit-textarea ui-edit-textarea-main';
|
||
|
||
// For protected headings, only show content after the heading
|
||
const textareaContent = isProtectedHeading ? section.getHeadingContent() : content;
|
||
textarea.value = textareaContent;
|
||
|
||
textarea.style.cssText = `
|
||
flex: 1;
|
||
min-height: 100px;
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
resize: vertical;
|
||
`;
|
||
|
||
textarea.addEventListener('input', () => {
|
||
if (isProtectedHeading) {
|
||
// Reconstruct full content with protected heading
|
||
const headingLine = section.originalMarkdown.split('\n')[0];
|
||
const fullContent = headingLine + '\n' + textarea.value;
|
||
this.sectionManager.updateContent(sectionId, fullContent);
|
||
} else {
|
||
this.sectionManager.updateContent(sectionId, textarea.value);
|
||
}
|
||
});
|
||
textarea.addEventListener('keydown', this.handleKeydown);
|
||
|
||
const controls = document.createElement('div');
|
||
controls.style.cssText = `
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
`;
|
||
|
||
const createButton = (text, className, handler) => {
|
||
const btn = document.createElement('button');
|
||
btn.textContent = text;
|
||
btn.className = className;
|
||
btn.addEventListener('click', handler);
|
||
return btn;
|
||
};
|
||
|
||
controls.appendChild(createButton('✓ Accept', 'ui-edit-button ui-edit-button-accept', () => this.handleAccept(sectionId)));
|
||
controls.appendChild(createButton('✗ Cancel', 'ui-edit-button ui-edit-button-cancel', () => this.handleCancel(sectionId)));
|
||
controls.appendChild(createButton('🔄 Reset', 'ui-edit-button ui-edit-button-reset', () => this.handleReset(sectionId)));
|
||
|
||
editingArea.appendChild(textarea);
|
||
editingArea.appendChild(controls);
|
||
editorContainer.appendChild(editingArea);
|
||
|
||
element.innerHTML = '';
|
||
element.appendChild(editorContainer);
|
||
|
||
textarea.focus();
|
||
// Track this section as being edited
|
||
this.editingSections.add(sectionId);
|
||
}
|
||
|
||
hideCurrentEditor() {
|
||
// This method is no longer needed since we support multiple editors
|
||
// Individual editors are hidden via hideEditor(sectionId)
|
||
}
|
||
|
||
hideEditor(sectionId) {
|
||
// Remove from editing sections set
|
||
this.editingSections.delete(sectionId);
|
||
|
||
// Force re-render the section to ensure it displays correctly
|
||
const section = this.sectionManager.sections.get(sectionId);
|
||
if (section) {
|
||
this.updateSectionContent(sectionId, section.currentMarkdown);
|
||
}
|
||
}
|
||
|
||
updateSectionContent(sectionId, content) {
|
||
const element = this.findSectionElement(sectionId);
|
||
if (!element) return;
|
||
|
||
if (typeof marked !== 'undefined') {
|
||
const html = marked.parse(content);
|
||
// Add target="_blank" to all links
|
||
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
||
element.innerHTML = htmlWithTargetBlank;
|
||
} else {
|
||
element.innerHTML = `<p>${content}</p>`;
|
||
}
|
||
|
||
// Restore the section styling and click behavior
|
||
this.setupSectionElement(element);
|
||
}
|
||
|
||
setupSectionElement(element) {
|
||
element.className = 'ui-edit-section';
|
||
element.style.cssText = `
|
||
margin: 16px 0;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: 2px solid transparent;
|
||
`;
|
||
|
||
// Remove any existing event listeners to avoid duplicates
|
||
element.removeEventListener('mouseenter', element._mouseenterHandler);
|
||
element.removeEventListener('mouseleave', element._mouseleaveHandler);
|
||
|
||
// Create new handlers and store references
|
||
element._mouseenterHandler = () => {
|
||
element.style.backgroundColor = 'rgba(0, 0, 0, 0.02)';
|
||
element.style.borderColor = 'rgba(0, 0, 0, 0.1)';
|
||
};
|
||
|
||
element._mouseleaveHandler = () => {
|
||
element.style.backgroundColor = '';
|
||
element.style.borderColor = 'transparent';
|
||
};
|
||
|
||
element.addEventListener('mouseenter', element._mouseenterHandler);
|
||
element.addEventListener('mouseleave', element._mouseleaveHandler);
|
||
}
|
||
|
||
updateTextareaContent(content, sectionId) {
|
||
// Find the specific textarea for this section
|
||
const element = this.findSectionElement(sectionId);
|
||
if (element) {
|
||
const textarea = element.querySelector('textarea');
|
||
if (textarea) {
|
||
textarea.value = content;
|
||
}
|
||
}
|
||
}
|
||
|
||
handleSectionSplit(data) {
|
||
// Clear the editor state for the original section
|
||
this.editingSections.delete(data.originalSectionId);
|
||
|
||
// Find the original section element and its position
|
||
const originalElement = this.findSectionElement(data.originalSectionId);
|
||
if (!originalElement) {
|
||
console.error('Original section element not found');
|
||
return;
|
||
}
|
||
|
||
// Get the position of the original element
|
||
const originalPosition = Array.from(this.container.children).indexOf(originalElement);
|
||
|
||
// Create new section elements for the split sections
|
||
const newElements = [];
|
||
data.newSections.forEach(sectionData => {
|
||
const section = this.sectionManager.sections.get(sectionData.id);
|
||
if (section) {
|
||
const element = this.createSectionElement(section);
|
||
section.domElement = element;
|
||
newElements.push(element);
|
||
}
|
||
});
|
||
|
||
// Remove the original element
|
||
originalElement.remove();
|
||
|
||
// Insert new elements at the original position
|
||
if (originalPosition < this.container.children.length) {
|
||
const nextElement = this.container.children[originalPosition];
|
||
newElements.forEach(element => {
|
||
this.container.insertBefore(element, nextElement);
|
||
});
|
||
} else {
|
||
// If original was at the end, just append
|
||
newElements.forEach(element => {
|
||
this.container.appendChild(element);
|
||
});
|
||
}
|
||
|
||
// Show success message
|
||
console.log(`Section split into ${data.newSections.length} sections`);
|
||
|
||
// Notify the main editor about the split
|
||
if (window.markitectCleanEditor) {
|
||
window.markitectCleanEditor.showMessage(
|
||
`✂️ Section split into ${data.newSections.length} sections!`,
|
||
'success'
|
||
);
|
||
}
|
||
}
|
||
|
||
findSectionElement(sectionId) {
|
||
return this.container.querySelector(`[data-section-id="${sectionId}"]`);
|
||
}
|
||
|
||
handleAccept(sectionId) {
|
||
try {
|
||
console.log('Accepting changes for section:', sectionId);
|
||
this.sectionManager.acceptChanges(sectionId);
|
||
console.log('Changes accepted successfully');
|
||
} catch (error) {
|
||
console.error('Failed to accept changes:', error);
|
||
}
|
||
}
|
||
|
||
handleCancel(sectionId) {
|
||
try {
|
||
console.log('Canceling changes for section:', sectionId);
|
||
this.sectionManager.cancelChanges(sectionId);
|
||
console.log('Changes canceled successfully');
|
||
} catch (error) {
|
||
console.error('Failed to cancel changes:', error);
|
||
}
|
||
}
|
||
|
||
handleReset(sectionId) {
|
||
try {
|
||
this.sectionManager.resetToOriginal(sectionId);
|
||
} catch (error) {
|
||
console.error('Failed to reset section:', error);
|
||
}
|
||
}
|
||
|
||
handleKeydown(event) {
|
||
if (!this.currentSection) return;
|
||
if (event.ctrlKey || event.metaKey) {
|
||
switch (event.key) {
|
||
case 'Enter':
|
||
event.preventDefault();
|
||
this.handleAccept(this.currentSection);
|
||
break;
|
||
case 'Escape':
|
||
event.preventDefault();
|
||
this.handleCancel(this.currentSection);
|
||
break;
|
||
}
|
||
}
|
||
if (event.key === 'Escape') {
|
||
event.preventDefault();
|
||
this.handleCancel(this.currentSection);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Main Editor Integration
|
||
*/
|
||
class MarkitectCleanEditor {
|
||
constructor(markdownContent, containerElement, options = {}) {
|
||
this.options = {
|
||
theme: 'github',
|
||
keyboardShortcuts: true,
|
||
autosave: false,
|
||
...options
|
||
};
|
||
|
||
this.sectionManager = new SectionManager();
|
||
this.domRenderer = new DOMRenderer(this.sectionManager, containerElement);
|
||
this.originalMarkdown = markdownContent;
|
||
this.initialize();
|
||
}
|
||
|
||
initialize() {
|
||
try {
|
||
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
|
||
console.log(`✓ Initialized clean editor with ${sections.length} sections`);
|
||
|
||
// Add global control panel
|
||
this.addGlobalControls();
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Failed to initialize clean editor:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
addGlobalControls() {
|
||
// Create floating control panel
|
||
const panel = document.createElement('div');
|
||
panel.id = 'ui-edit-floater';
|
||
panel.className = 'ui-edit-floater-panel';
|
||
panel.innerHTML = `
|
||
<div class="ui-edit-floater-header">
|
||
<h3>📝 Editor</h3>
|
||
<div class="ui-edit-floater-status" id="editor-status">Ready</div>
|
||
</div>
|
||
<div class="ui-edit-floater-actions">
|
||
<button id="save-document" class="ui-edit-button ui-edit-button-accept">💾 Save Document</button>
|
||
<button id="reset-all" class="ui-edit-button ui-edit-button-reset">🔄 Reset All</button>
|
||
<button id="show-status" class="ui-edit-button ui-edit-button-secondary">📊 Show Status</button>
|
||
</div>
|
||
`;
|
||
|
||
// Style the panel
|
||
panel.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
z-index: 1000;
|
||
font-family: system-ui, -apple-system, sans-serif;
|
||
min-width: 200px;
|
||
max-width: 250px;
|
||
`;
|
||
|
||
// Add internal styling for structural layout (theme colors come from CSS)
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.ui-edit-floater-header h3 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 16px;
|
||
}
|
||
.ui-edit-floater-status {
|
||
font-size: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.ui-edit-button {
|
||
display: block;
|
||
width: 100%;
|
||
margin: 6px 0;
|
||
padding: 10px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
border: 1px solid transparent;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
document.body.appendChild(panel);
|
||
|
||
// Add event listeners
|
||
document.getElementById('save-document').addEventListener('click', () => this.saveDocument());
|
||
document.getElementById('reset-all').addEventListener('click', () => this.resetAllSections());
|
||
document.getElementById('show-status').addEventListener('click', () => this.showStatus());
|
||
|
||
// Update status periodically
|
||
this.statusInterval = setInterval(() => this.updateGlobalStatus(), 2000);
|
||
}
|
||
|
||
updateGlobalStatus() {
|
||
const statusEl = document.getElementById('editor-status');
|
||
if (!statusEl) return;
|
||
|
||
const sections = this.sectionManager.getAllSections();
|
||
const modified = sections.filter(s => s.hasChanges()).length;
|
||
const editing = sections.filter(s => s.isEditing()).length;
|
||
|
||
if (editing > 0) {
|
||
statusEl.textContent = `Editing ${editing} section${editing !== 1 ? 's' : ''}`;
|
||
statusEl.className = 'ui-edit-floater-status editing';
|
||
} else if (modified > 0) {
|
||
statusEl.textContent = `${modified} section${modified !== 1 ? 's' : ''} modified`;
|
||
statusEl.style.color = '';
|
||
} else {
|
||
statusEl.textContent = 'All sections saved ✓';
|
||
statusEl.style.color = '';
|
||
}
|
||
}
|
||
|
||
saveDocument() {
|
||
const markdown = this.getDocumentMarkdown();
|
||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
// Generate intelligent filename
|
||
const filename = this.generateSaveFilename();
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.style.display = 'none';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
console.log('📄 Document saved as:', filename);
|
||
this.showMessage(`Document saved as: ${filename}`, 'success');
|
||
}
|
||
|
||
generateSaveFilename() {
|
||
// Try to get original filename from config
|
||
let baseName = 'document';
|
||
|
||
// Method 1: Use original filename from config if available
|
||
if (typeof MARKITECT_EDITOR_CONFIG !== 'undefined' && MARKITECT_EDITOR_CONFIG.originalFilename) {
|
||
baseName = MARKITECT_EDITOR_CONFIG.originalFilename;
|
||
}
|
||
|
||
// Method 2: Try to extract from page title
|
||
if (baseName === 'document') {
|
||
const title = document.title;
|
||
if (title && title !== 'Markdown Document' && !title.includes('Debug') && !title.includes('Test')) {
|
||
baseName = title.toLowerCase()
|
||
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
||
.replace(/\s+/g, '-') // Replace spaces with dashes
|
||
.replace(/-+/g, '-') // Collapse multiple dashes
|
||
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
||
}
|
||
}
|
||
|
||
// Method 3: Try to extract from URL pathname
|
||
if (baseName === 'document') {
|
||
const urlPath = window.location.pathname;
|
||
const match = urlPath.match(/\/([^\/]+)\.html?$/);
|
||
if (match) {
|
||
const urlBaseName = match[1];
|
||
if (!urlBaseName.includes('debug') && !urlBaseName.includes('test')) {
|
||
baseName = urlBaseName.replace(/_/g, '-');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method 4: Try to extract from first heading
|
||
if (baseName === 'document') {
|
||
const firstHeading = this.sectionManager.getAllSections()
|
||
.find(section => section.sectionType === 'heading');
|
||
if (firstHeading) {
|
||
baseName = firstHeading.originalMarkdown
|
||
.replace(/^#+\s*/, '') // Remove markdown heading syntax
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9\s-]/g, '')
|
||
.replace(/\s+/g, '-')
|
||
.replace(/-+/g, '-')
|
||
.replace(/^-|-$/g, '')
|
||
.substring(0, 30); // Limit length
|
||
}
|
||
}
|
||
|
||
// Generate timestamp
|
||
const now = new Date();
|
||
const timestamp = now.toISOString()
|
||
.replace(/T/, '-')
|
||
.replace(/:/g, '-')
|
||
.replace(/\.\d{3}Z$/, '');
|
||
|
||
// Check if there are modifications
|
||
const hasModifications = this.sectionManager.getAllSections()
|
||
.some(section => section.hasChanges());
|
||
|
||
if (hasModifications) {
|
||
return `${baseName}-edited-${timestamp}.md`;
|
||
} else {
|
||
return `${baseName}-${timestamp}.md`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show custom confirmation dialog with theme-consistent styling
|
||
* @param {string} message - The confirmation message
|
||
* @param {string} confirmText - Text for confirm button (default: "Confirm")
|
||
* @param {string} cancelText - Text for cancel button (default: "Cancel")
|
||
* @param {string} warningText - Optional warning text to highlight consequences
|
||
* @returns {Promise<boolean>} - True if confirmed, false if cancelled
|
||
*/
|
||
showConfirmation(message, confirmText = "Confirm", cancelText = "Cancel", warningText = null) {
|
||
return new Promise((resolve) => {
|
||
// Remove any existing modal
|
||
const existingModal = document.querySelector('.ui-edit-modal-overlay');
|
||
if (existingModal) {
|
||
existingModal.remove();
|
||
}
|
||
|
||
// Create modal overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'ui-edit-modal-overlay';
|
||
|
||
// Create modal content
|
||
const modal = document.createElement('div');
|
||
modal.className = 'ui-edit-modal ui-edit-confirmation-modal';
|
||
|
||
// Create header
|
||
const header = document.createElement('div');
|
||
header.className = 'ui-edit-modal-header';
|
||
|
||
const title = document.createElement('h3');
|
||
title.className = 'ui-edit-modal-title';
|
||
title.textContent = 'Confirm Action';
|
||
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.className = 'ui-edit-modal-close';
|
||
closeBtn.innerHTML = '×';
|
||
closeBtn.setAttribute('aria-label', 'Close');
|
||
|
||
header.appendChild(title);
|
||
header.appendChild(closeBtn);
|
||
|
||
// Create body
|
||
const body = document.createElement('div');
|
||
body.className = 'ui-edit-modal-body';
|
||
|
||
const content = document.createElement('div');
|
||
content.className = 'ui-edit-confirmation-content';
|
||
content.textContent = message;
|
||
body.appendChild(content);
|
||
|
||
// Add warning section if provided
|
||
if (warningText) {
|
||
const warning = document.createElement('div');
|
||
warning.className = 'ui-edit-confirmation-warning';
|
||
warning.textContent = warningText;
|
||
body.appendChild(warning);
|
||
}
|
||
|
||
// Create footer with action buttons
|
||
const footer = document.createElement('div');
|
||
footer.className = 'ui-edit-modal-footer';
|
||
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.className = 'ui-edit-confirmation-buttons';
|
||
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.className = 'ui-edit-button-cancel';
|
||
cancelBtn.textContent = cancelText;
|
||
|
||
const confirmBtn = document.createElement('button');
|
||
confirmBtn.className = 'ui-edit-button-confirm';
|
||
confirmBtn.textContent = confirmText;
|
||
|
||
buttonContainer.appendChild(cancelBtn);
|
||
buttonContainer.appendChild(confirmBtn);
|
||
footer.appendChild(buttonContainer);
|
||
|
||
// Assemble modal
|
||
modal.appendChild(header);
|
||
modal.appendChild(body);
|
||
modal.appendChild(footer);
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
// Function to close modal and resolve
|
||
const closeModal = (result) => {
|
||
overlay.remove();
|
||
resolve(result);
|
||
};
|
||
|
||
// Event listeners
|
||
closeBtn.addEventListener('click', () => closeModal(false));
|
||
cancelBtn.addEventListener('click', () => closeModal(false));
|
||
confirmBtn.addEventListener('click', () => closeModal(true));
|
||
|
||
// Close on overlay click
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
closeModal(false);
|
||
}
|
||
});
|
||
|
||
// Keyboard support
|
||
const handleKeyDown = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeModal(false);
|
||
} else if (e.key === 'Enter') {
|
||
closeModal(true);
|
||
}
|
||
};
|
||
|
||
document.addEventListener('keydown', handleKeyDown);
|
||
|
||
// Clean up event listener when modal is closed
|
||
const originalResolve = resolve;
|
||
resolve = (result) => {
|
||
document.removeEventListener('keydown', handleKeyDown);
|
||
originalResolve(result);
|
||
};
|
||
|
||
// Show modal with animation
|
||
setTimeout(() => {
|
||
overlay.classList.add('active');
|
||
// Focus the confirm button for accessibility
|
||
confirmBtn.focus();
|
||
}, 10);
|
||
});
|
||
}
|
||
|
||
async resetAllSections() {
|
||
const confirmed = await this.showConfirmation(
|
||
'Reset all content to original markdown?',
|
||
'Reset Document',
|
||
'Keep Changes',
|
||
'This will permanently lose all edits and remove any split sections. This action cannot be undone.'
|
||
);
|
||
|
||
if (confirmed) {
|
||
// Clear the section manager completely
|
||
this.sectionManager.sections.clear();
|
||
// Note: No longer tracking single editingSection
|
||
|
||
// Recreate sections from original markdown
|
||
const sections = this.sectionManager.createSectionsFromMarkdown(this.originalMarkdown);
|
||
|
||
console.log('🔄 All sections reset to original structure');
|
||
this.showMessage('Document reset to original structure', 'info');
|
||
}
|
||
}
|
||
|
||
showStatus() {
|
||
const sections = this.sectionManager.getAllSections();
|
||
const total = sections.length;
|
||
const modified = sections.filter(s => s.hasChanges()).length;
|
||
const editing = sections.filter(s => s.isEditing()).length;
|
||
|
||
// Get the actual save filename that will be used
|
||
const saveFilename = this.generateSaveFilename();
|
||
|
||
// Create structured content for the modal
|
||
const modalContent = {
|
||
title: `📊 ${window.editorConfig.repoName} Status`,
|
||
sections: [
|
||
{
|
||
title: 'Application Information',
|
||
content: `${window.editorConfig.version}`
|
||
},
|
||
{
|
||
title: 'File Information',
|
||
content: `Save file: ${saveFilename}
|
||
Source: ${window.editorConfig.originalFilename}
|
||
URL: ${window.location.protocol}//${window.location.host}${window.location.pathname}`
|
||
},
|
||
{
|
||
title: 'Document Status',
|
||
content: `• Total sections: ${total}
|
||
• Modified sections: ${modified}
|
||
• Currently editing: ${editing}
|
||
• Unsaved changes: ${modified > 0 || editing > 0 ? 'Yes' : 'No'}`
|
||
},
|
||
{
|
||
title: 'Section Behavior',
|
||
content: `• Each section is a logical unit (heading + content until next heading)
|
||
• Content with line breaks stays in one section
|
||
• To split content: Create new headings (# ## ###)
|
||
• Sections don't auto-split on line breaks`
|
||
},
|
||
{
|
||
title: 'Editing Controls',
|
||
content: `• Click any section to edit its content
|
||
• Accept (✓) - Save changes to that section
|
||
• Cancel (✗) - Discard changes, return to previous state
|
||
• Reset (🔄) - Restore original content for that section
|
||
• Save Document - Download all current content
|
||
• Reset All - Restore entire document to original state`
|
||
}
|
||
]
|
||
};
|
||
|
||
this.showModal(modalContent);
|
||
}
|
||
|
||
showModal(content) {
|
||
// Remove any existing modal
|
||
const existingModal = document.querySelector('.ui-edit-modal-overlay');
|
||
if (existingModal) {
|
||
existingModal.remove();
|
||
}
|
||
|
||
// Create modal overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'ui-edit-modal-overlay';
|
||
|
||
// Create modal content
|
||
const modal = document.createElement('div');
|
||
modal.className = 'ui-edit-modal';
|
||
|
||
// Create header
|
||
const header = document.createElement('div');
|
||
header.className = 'ui-edit-modal-header';
|
||
|
||
const title = document.createElement('h3');
|
||
title.className = 'ui-edit-modal-title';
|
||
title.textContent = content.title;
|
||
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.className = 'ui-edit-modal-close';
|
||
closeBtn.innerHTML = '×';
|
||
closeBtn.setAttribute('aria-label', 'Close');
|
||
|
||
header.appendChild(title);
|
||
header.appendChild(closeBtn);
|
||
|
||
// Create body
|
||
const body = document.createElement('div');
|
||
body.className = 'ui-edit-modal-body';
|
||
|
||
// Add sections
|
||
content.sections.forEach(section => {
|
||
const sectionDiv = document.createElement('div');
|
||
sectionDiv.className = 'ui-edit-modal-section';
|
||
|
||
const sectionTitle = document.createElement('div');
|
||
sectionTitle.className = 'ui-edit-modal-section-title';
|
||
sectionTitle.textContent = section.title;
|
||
|
||
const sectionContent = document.createElement('div');
|
||
sectionContent.className = 'ui-edit-modal-content';
|
||
sectionContent.textContent = section.content;
|
||
|
||
sectionDiv.appendChild(sectionTitle);
|
||
sectionDiv.appendChild(sectionContent);
|
||
body.appendChild(sectionDiv);
|
||
});
|
||
|
||
// Create footer with close button
|
||
const footer = document.createElement('div');
|
||
footer.className = 'ui-edit-modal-footer';
|
||
|
||
const footerCloseBtn = document.createElement('button');
|
||
footerCloseBtn.className = 'ui-edit-button ui-edit-button-accept';
|
||
footerCloseBtn.textContent = 'Close';
|
||
footer.appendChild(footerCloseBtn);
|
||
|
||
// Assemble modal
|
||
modal.appendChild(header);
|
||
modal.appendChild(body);
|
||
modal.appendChild(footer);
|
||
overlay.appendChild(modal);
|
||
|
||
// Add to page
|
||
document.body.appendChild(overlay);
|
||
|
||
// Close handlers
|
||
const closeModal = () => {
|
||
overlay.classList.remove('active');
|
||
setTimeout(() => {
|
||
if (overlay.parentNode) {
|
||
overlay.parentNode.removeChild(overlay);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
closeBtn.addEventListener('click', closeModal);
|
||
footerCloseBtn.addEventListener('click', closeModal);
|
||
|
||
// Close on overlay click (but not modal content)
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
closeModal();
|
||
}
|
||
});
|
||
|
||
// Close on Escape key
|
||
const handleKeydown = (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeModal();
|
||
document.removeEventListener('keydown', handleKeydown);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleKeydown);
|
||
|
||
// Show modal with animation
|
||
requestAnimationFrame(() => {
|
||
overlay.classList.add('active');
|
||
});
|
||
|
||
// Focus management
|
||
setTimeout(() => {
|
||
closeBtn.focus();
|
||
}, 100);
|
||
}
|
||
|
||
showMessage(message, type = 'info') {
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.textContent = message;
|
||
|
||
const colors = {
|
||
'success': '#28a745',
|
||
'error': '#dc3545',
|
||
'info': '#007acc'
|
||
};
|
||
|
||
messageDiv.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: ${colors[type] || colors.info};
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
z-index: 10001;
|
||
font-size: 14px;
|
||
max-width: 400px;
|
||
text-align: center;
|
||
`;
|
||
|
||
document.body.appendChild(messageDiv);
|
||
|
||
setTimeout(() => {
|
||
if (messageDiv.parentNode) {
|
||
messageDiv.parentNode.removeChild(messageDiv);
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
getDocumentMarkdown() {
|
||
return this.sectionManager.getDocumentMarkdown();
|
||
}
|
||
}
|
||
|
||
// Initialize the clean editor system
|
||
let markitectCleanEditor;
|
||
|
||
function initializeCleanEditor() {
|
||
const container = document.getElementById('markdown-content');
|
||
if (!container) {
|
||
console.error('Markdown content container not found');
|
||
return;
|
||
}
|
||
|
||
if (typeof window.MarkitectEditor === 'undefined') {
|
||
console.error('MarkitectEditor not found');
|
||
return;
|
||
}
|
||
|
||
markitectCleanEditor = new window.MarkitectEditor.MarkitectCleanEditor(markdownContent, container);
|
||
window.markitectCleanEditor = markitectCleanEditor; // Make globally available
|
||
console.log('✅ Clean section editor initialized successfully');
|
||
}
|
||
|
||
// Document scroll indicators
|
||
function initializeScrollIndicators() {
|
||
// Create scroll indicators
|
||
const scrollUpIndicator = document.createElement('div');
|
||
scrollUpIndicator.className = 'ui-scroll-indicator ui-scroll-indicator-up';
|
||
scrollUpIndicator.setAttribute('aria-label', 'Scroll to top');
|
||
scrollUpIndicator.setAttribute('title', 'Scroll up');
|
||
|
||
const scrollDownIndicator = document.createElement('div');
|
||
scrollDownIndicator.className = 'ui-scroll-indicator ui-scroll-indicator-down';
|
||
scrollDownIndicator.setAttribute('aria-label', 'Scroll to bottom');
|
||
scrollDownIndicator.setAttribute('title', 'Scroll down');
|
||
|
||
document.body.appendChild(scrollUpIndicator);
|
||
document.body.appendChild(scrollDownIndicator);
|
||
|
||
let scrollIndicatorTimeout = null;
|
||
|
||
// Function to show/hide indicators based on scroll position and mouse position
|
||
function updateScrollIndicators(mouseY = null) {
|
||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||
const windowHeight = window.innerHeight;
|
||
const documentHeight = document.documentElement.scrollHeight;
|
||
|
||
// Determine if scrolling is possible in each direction
|
||
const canScrollUp = scrollTop > 0;
|
||
const canScrollDown = scrollTop < documentHeight - windowHeight;
|
||
|
||
// Only show indicators if there's any scroll possibility or if document is short
|
||
let showUp = false;
|
||
let showDown = false;
|
||
|
||
// Show indicators on mouseover near top/bottom of viewport
|
||
if (mouseY !== null) {
|
||
const topZone = 100; // pixels from top
|
||
const bottomZone = windowHeight - 100; // pixels from bottom
|
||
|
||
if (mouseY <= topZone) {
|
||
showUp = true;
|
||
}
|
||
if (mouseY >= bottomZone) {
|
||
showDown = true;
|
||
}
|
||
}
|
||
|
||
// Update indicator visibility and state
|
||
if (showUp) {
|
||
scrollUpIndicator.classList.add('active');
|
||
if (canScrollUp) {
|
||
scrollUpIndicator.classList.remove('disabled');
|
||
} else {
|
||
scrollUpIndicator.classList.add('disabled');
|
||
}
|
||
} else {
|
||
scrollUpIndicator.classList.remove('active');
|
||
}
|
||
|
||
if (showDown) {
|
||
scrollDownIndicator.classList.add('active');
|
||
if (canScrollDown) {
|
||
scrollDownIndicator.classList.remove('disabled');
|
||
} else {
|
||
scrollDownIndicator.classList.add('disabled');
|
||
}
|
||
} else {
|
||
scrollDownIndicator.classList.remove('active');
|
||
}
|
||
|
||
// Auto-hide after a delay when mouse moves away
|
||
if (scrollIndicatorTimeout) {
|
||
clearTimeout(scrollIndicatorTimeout);
|
||
}
|
||
scrollIndicatorTimeout = setTimeout(() => {
|
||
scrollUpIndicator.classList.remove('active');
|
||
scrollDownIndicator.classList.remove('active');
|
||
}, 2000);
|
||
}
|
||
|
||
// Mouse move handler
|
||
function handleMouseMove(e) {
|
||
updateScrollIndicators(e.clientY);
|
||
}
|
||
|
||
// Smooth scroll function
|
||
function smoothScroll(targetY, duration = 500) {
|
||
const startY = window.pageYOffset;
|
||
const difference = targetY - startY;
|
||
const startTime = performance.now();
|
||
|
||
function step(currentTime) {
|
||
const elapsed = currentTime - startTime;
|
||
const progress = Math.min(elapsed / duration, 1);
|
||
|
||
// Easing function (ease-out)
|
||
const easeOut = 1 - Math.pow(1 - progress, 3);
|
||
|
||
window.scrollTo(0, startY + difference * easeOut);
|
||
|
||
if (progress < 1) {
|
||
requestAnimationFrame(step);
|
||
}
|
||
}
|
||
|
||
requestAnimationFrame(step);
|
||
}
|
||
|
||
// Click handlers for smooth scrolling
|
||
scrollUpIndicator.addEventListener('click', () => {
|
||
const currentScroll = window.pageYOffset;
|
||
const targetScroll = Math.max(0, currentScroll - window.innerHeight * 0.8);
|
||
smoothScroll(targetScroll);
|
||
});
|
||
|
||
scrollDownIndicator.addEventListener('click', () => {
|
||
const currentScroll = window.pageYOffset;
|
||
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
|
||
const targetScroll = Math.min(maxScroll, currentScroll + window.innerHeight * 0.8);
|
||
smoothScroll(targetScroll);
|
||
});
|
||
|
||
// Event listeners
|
||
document.addEventListener('mousemove', handleMouseMove);
|
||
document.addEventListener('scroll', () => updateScrollIndicators());
|
||
|
||
// Initial check
|
||
updateScrollIndicators();
|
||
|
||
console.log('✅ Document scroll indicators initialized');
|
||
}
|
||
|
||
// Export for testing and usage
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
||
} else {
|
||
window.MarkitectEditor = { Section, SectionManager, DOMRenderer, MarkitectCleanEditor };
|
||
}
|
||
|
||
|
||
// Always render content first (graceful degradation)
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log("Rendering content...");
|
||
|
||
const contentDiv = document.getElementById('markdown-content');
|
||
|
||
// Step 1: Ensure content is always displayed
|
||
if (contentDiv) {
|
||
if (typeof marked !== 'undefined') {
|
||
try {
|
||
const html = marked.parse(markdownContent);
|
||
// Add target="_blank" to all links
|
||
const htmlWithTargetBlank = html.replace(/<a href="([^"]*)"([^>]*)>/g, '<a href="$1" target="_blank"$2>');
|
||
contentDiv.innerHTML = htmlWithTargetBlank;
|
||
console.log("✓ Content rendered successfully");
|
||
console.log('✓ Markdown rendered successfully');
|
||
} catch (error) {
|
||
contentDiv.innerHTML = '<p>Error rendering markdown: ' + error.message + '</p>';
|
||
console.error("Content rendered with errors");
|
||
console.error("Markdown parsing failed:", error.message);
|
||
}
|
||
} else {
|
||
// Fallback: display raw markdown with basic formatting
|
||
const fallbackHtml = markdownContent
|
||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||
.replace(/^- (.*$)/gim, '<li>$1</li>')
|
||
.replace(/\n\n/g, '<br><br>')
|
||
.replace(/\n/g, '<br>');
|
||
contentDiv.innerHTML = '<div style="white-space: pre-wrap;">' + fallbackHtml + '</div>';
|
||
console.warn("Content rendered with fallback parser");
|
||
console.warn("CDN library failed to load - using basic fallback rendering");
|
||
}
|
||
}
|
||
|
||
// Step 2: Initialize edit/insert capabilities if enabled
|
||
if ((typeof MARKITECT_EDIT_MODE !== 'undefined' && MARKITECT_EDIT_MODE) ||
|
||
(typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE)) {
|
||
const mode = (typeof MARKITECT_INSERT_MODE !== 'undefined' && MARKITECT_INSERT_MODE) ? 'insert' : 'edit';
|
||
console.log(`Initializing clean ${mode} capabilities...`);
|
||
try {
|
||
console.log("Creating clean editor instance...");
|
||
initializeCleanEditor();
|
||
if (mode === 'insert') {
|
||
console.log("✓ Clean insert mode active - click any section to edit (headings 1-3 protected)");
|
||
} else {
|
||
console.log("✓ Clean edit mode active - click any section to edit");
|
||
}
|
||
} catch (error) {
|
||
console.error(`Clean ${mode} mode failed to initialize:`, error);
|
||
}
|
||
}
|
||
|
||
// Step 3: Initialize document scroll indicators (always available)
|
||
try {
|
||
initializeScrollIndicators();
|
||
} catch (error) {
|
||
console.error("Scroll indicators failed to initialize:", error);
|
||
}
|
||
});
|
||
|
||
// Handle CDN loading errors
|
||
window.addEventListener('load', function() {
|
||
if (window.markitectMarkedError) {
|
||
console.error("CDN library failed to load - network or firewall blocking marked.js");
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |