|
370 | 370 | justify-content: space-between; |
371 | 371 | } |
372 | 372 | } |
| 373 | + |
| 374 | + .wiki-link { |
| 375 | + color: var(--primary); |
| 376 | + text-decoration: none; |
| 377 | + border-bottom: 1px dashed var(--primary-light); |
| 378 | + cursor: pointer; |
| 379 | + } |
| 380 | + |
| 381 | + .wiki-link:hover { |
| 382 | + background-color: rgba(0, 130, 201, 0.1); |
| 383 | + border-bottom: 1px solid var(--primary); |
| 384 | + } |
| 385 | + |
| 386 | + /* Add a special style for non-existent pages */ |
| 387 | + .wiki-link.new-page { |
| 388 | + color: #e67e22; |
| 389 | + border-bottom: 1px dashed #e67e22; |
| 390 | + } |
| 391 | + |
| 392 | + .wiki-link.new-page:hover { |
| 393 | + background-color: rgba(230, 126, 34, 0.1); |
| 394 | + } |
373 | 395 | </style> |
374 | 396 | </head> |
375 | 397 | <body> |
|
393 | 415 | // Make secp256k1 available globally (required by nosdav-shim.js) |
394 | 416 | window.secp256k1 = secp256k1 |
395 | 417 |
|
| 418 | + // Configure Marked to recognize wiki-links [[foo]] |
| 419 | + const wikiLinkExtension = { |
| 420 | + name: 'wikiLink', |
| 421 | + level: 'inline', |
| 422 | + start(src) { |
| 423 | + return src.match(/\[\[/)?.index |
| 424 | + }, |
| 425 | + tokenizer(src) { |
| 426 | + const rule = /^\[\[([^\[\]]+)\]\]/ |
| 427 | + const match = rule.exec(src) |
| 428 | + if (match) { |
| 429 | + return { |
| 430 | + type: 'wikiLink', |
| 431 | + raw: match[0], |
| 432 | + text: match[1].trim(), |
| 433 | + tokens: [] |
| 434 | + } |
| 435 | + } |
| 436 | + return undefined |
| 437 | + }, |
| 438 | + renderer(token) { |
| 439 | + const pageName = token.text |
| 440 | + const safePageName = pageName |
| 441 | + .replace(/[^a-zA-Z0-9-_]/g, '-') |
| 442 | + .toLowerCase() |
| 443 | + return `<a href="#" class="wiki-link" data-page="${pageName}" data-link-type="page">[[${pageName}]]</a>` |
| 444 | + } |
| 445 | + } |
| 446 | + |
| 447 | + // Add the extension to marked |
| 448 | + marked.use({ extensions: [wikiLinkExtension] }) |
| 449 | + |
396 | 450 | // Initialize the navbar |
397 | 451 | const navbarContainer = document.getElementById('navbar') |
398 | 452 | if (navbarContainer) { |
|
563 | 617 | const [storageType, setStorageType] = useState('') |
564 | 618 | const [viewMode, setViewMode] = useState(VIEW_MODES.SPLIT) |
565 | 619 | const editorRef = useRef(null) |
| 620 | + const previewContainerRef = useRef(null) |
566 | 621 | const saveTimeoutRef = useRef(null) |
567 | 622 |
|
568 | 623 | // Load journal entry for the current date |
|
594 | 649 | } |
595 | 650 | }, []) |
596 | 651 |
|
| 652 | + // Set up event listener for wiki links |
| 653 | + useEffect(() => { |
| 654 | + if (previewContainerRef.current) { |
| 655 | + const handleWikiLinkClick = e => { |
| 656 | + // Check if the clicked element is a wiki link |
| 657 | + if (e.target.classList.contains('wiki-link')) { |
| 658 | + e.preventDefault() |
| 659 | + const pageName = e.target.dataset.page |
| 660 | + const linkType = e.target.dataset.linkType |
| 661 | + |
| 662 | + if (pageName) { |
| 663 | + if (linkType === 'page') { |
| 664 | + // Navigate to pages.html with the page name |
| 665 | + window.location.href = `pages.html?page=${encodeURIComponent( |
| 666 | + pageName |
| 667 | + )}` |
| 668 | + } else { |
| 669 | + // Try to extract a date from the link, or use today's date |
| 670 | + let date |
| 671 | + const dateParts = pageName.split('_') |
| 672 | + if (dateParts.length === 3) { |
| 673 | + try { |
| 674 | + date = new Date( |
| 675 | + parseInt(dateParts[0]), |
| 676 | + parseInt(dateParts[1]) - 1, |
| 677 | + parseInt(dateParts[2]) |
| 678 | + ) |
| 679 | + if (isNaN(date.getTime())) { |
| 680 | + date = new Date() |
| 681 | + } |
| 682 | + } catch (e) { |
| 683 | + date = new Date() |
| 684 | + } |
| 685 | + } else { |
| 686 | + date = new Date() |
| 687 | + } |
| 688 | + setCurrentDate(date) |
| 689 | + } |
| 690 | + } |
| 691 | + } |
| 692 | + } |
| 693 | + |
| 694 | + // Add click event listener to the preview container |
| 695 | + previewContainerRef.current.addEventListener( |
| 696 | + 'click', |
| 697 | + handleWikiLinkClick |
| 698 | + ) |
| 699 | + |
| 700 | + // Clean up the event listener |
| 701 | + return () => { |
| 702 | + if (previewContainerRef.current) { |
| 703 | + previewContainerRef.current.removeEventListener( |
| 704 | + 'click', |
| 705 | + handleWikiLinkClick |
| 706 | + ) |
| 707 | + } |
| 708 | + } |
| 709 | + } |
| 710 | + }, [content, viewMode]) |
| 711 | + |
597 | 712 | // Load all journal entries |
598 | 713 | const loadJournalEntries = async () => { |
599 | 714 | const entries = await JournalStorage.getJournalIndex() |
|
684 | 799 | } |
685 | 800 | } |
686 | 801 |
|
| 802 | + // Helper function to insert wiki link at cursor |
| 803 | + const insertWikiLink = () => { |
| 804 | + if (!editorRef.current) return |
| 805 | + |
| 806 | + const textarea = editorRef.current |
| 807 | + const start = textarea.selectionStart |
| 808 | + const end = textarea.selectionEnd |
| 809 | + const selectedText = textarea.value.substring(start, end) |
| 810 | + |
| 811 | + // Prepare the wiki link text - use selection or prompt for page name |
| 812 | + let linkText |
| 813 | + if (selectedText) { |
| 814 | + linkText = selectedText |
| 815 | + } else { |
| 816 | + linkText = prompt('Enter page name:', 'page-name') |
| 817 | + if (!linkText) return // User canceled the prompt |
| 818 | + } |
| 819 | + |
| 820 | + const wikiLink = `[[${linkText}]]` |
| 821 | + |
| 822 | + // Insert the wiki link at the cursor position |
| 823 | + const newContent = |
| 824 | + textarea.value.substring(0, start) + |
| 825 | + wikiLink + |
| 826 | + textarea.value.substring(end) |
| 827 | + |
| 828 | + setContent(newContent) |
| 829 | + |
| 830 | + // Focus and set cursor position after the inserted text |
| 831 | + setTimeout(() => { |
| 832 | + textarea.focus() |
| 833 | + const newPosition = start + wikiLink.length |
| 834 | + textarea.setSelectionRange(newPosition, newPosition) |
| 835 | + }, 0) |
| 836 | + } |
| 837 | + |
687 | 838 | // Main class for the editor/preview container based on view mode |
688 | 839 | const getMainClass = () => { |
689 | 840 | switch (viewMode) { |
|
830 | 981 | </svg> |
831 | 982 | </div> |
832 | 983 | `} |
| 984 | +
|
| 985 | + <!-- Add Wiki Link button --> |
| 986 | + <div |
| 987 | + class="view-option" |
| 988 | + onClick=${insertWikiLink} |
| 989 | + title="Insert Wiki Link" |
| 990 | + > |
| 991 | + <svg |
| 992 | + width="16" |
| 993 | + height="16" |
| 994 | + viewBox="0 0 24 24" |
| 995 | + fill="none" |
| 996 | + stroke="currentColor" |
| 997 | + stroke-width="2" |
| 998 | + stroke-linecap="round" |
| 999 | + stroke-linejoin="round" |
| 1000 | + > |
| 1001 | + <path |
| 1002 | + d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" |
| 1003 | + ></path> |
| 1004 | + <path |
| 1005 | + d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" |
| 1006 | + ></path> |
| 1007 | + </svg> |
| 1008 | + </div> |
833 | 1009 | </div> |
834 | 1010 | </div> |
835 | 1011 | <div class="date-nav"> |
|
904 | 1080 | `} |
905 | 1081 | </div> |
906 | 1082 |
|
907 | | - <div class="preview-container"> |
| 1083 | + <div class="preview-container" ref=${previewContainerRef}> |
908 | 1084 | <div |
909 | 1085 | class="preview" |
910 | 1086 | dangerouslySetInnerHTML=${{ __html: marked.parse(content) }} |
|
0 commit comments