diff --git a/pages/jotpad.html b/pages/jotpad.html index 116ed094..a10b9fcf 100644 --- a/pages/jotpad.html +++ b/pages/jotpad.html @@ -38,6 +38,7 @@

JotPad

+
@@ -70,9 +71,12 @@

JotPad

+ +
+
@@ -81,5 +85,6 @@

JotPad

+ diff --git a/scripts/jotpad.js b/scripts/jotpad.js index f476d4bb..92d11f76 100644 --- a/scripts/jotpad.js +++ b/scripts/jotpad.js @@ -48,7 +48,17 @@ document.addEventListener('DOMContentLoaded', function () { const theme = document.documentElement.getAttribute('data-theme') - ctx.strokeStyle = theme === 'dark' ? '#ffffff' : '#000000' + // Use selectedColor from highlight palette if chosen, otherwise fallback + if (highlightEnabled) { + ctx.strokeStyle = selectedColor; + } else { + // Default based on theme + ctx.strokeStyle = + document.documentElement.getAttribute("data-theme") === "dark" + ? "#ffffff" + : "#000000"; + } + ctx.lineWidth = 2 ctx.lineJoin = 'round' ctx.lineCap = 'round' @@ -187,56 +197,258 @@ document.addEventListener('DOMContentLoaded', function () { togglePlaceholder() }) -// Download PDF -async function downloadPDF() { - const { jsPDF } = window.jspdf - const content = document.getElementById('noteArea').innerHTML - const doc = new jsPDF() - doc.setFont('helvetica', 'normal') - doc.setFontSize(14) - - const tempDiv = document.createElement('div') - tempDiv.innerHTML = content - - let textContent = '' - const walker = document.createTreeWalker( - tempDiv, - NodeFilter.SHOW_TEXT, - null, - false - ) - let node - while ((node = walker.nextNode())) { - textContent += node.nodeValue + '\n' +const highlightToggleBtn = document.getElementById('highlightToggleBtn'); +const highlightPalette = document.getElementById('highlightPalette'); + +// Highlight Colors Circular Palette +const highlightColors = ['yellow', 'lightgreen', 'lightblue', 'pink', 'orange']; +let selectedColor = highlightColors[0]; +let highlightEnabled = false; + +// --- Helper Functions --- +function insertNodeAtRange(node, range) { + const container = range.startContainer; + if (container.nodeType === 3) { + container.parentNode.insertBefore(node, container.nextSibling); + } else { + range.insertNode(node); } +} - const images = tempDiv.getElementsByTagName('img') - let yPosition = 20 - const textLines = doc.splitTextToSize(textContent, 180) - doc.text(textLines, 10, yPosition) - yPosition += textLines.length * 7 +function setCaretAfterNode(node) { + const selection = window.getSelection(); + const range = document.createRange(); + range.setStartAfter(node); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); +} - for (let i = 0; i < images.length; i++) { - if (yPosition > 250) { - doc.addPage() - yPosition = 20 - } +function splitSpanAtCaret() { + const selection = window.getSelection(); + if (!selection.rangeCount) return; - try { - const imgData = await getImageData(images[i].src) - const imgProps = doc.getImageProperties(imgData) - const width = 180 - const height = (imgProps.height * width) / imgProps.width - doc.addImage(imgData, 'PNG', 10, yPosition, width, height) - yPosition += height + 10 - } catch (error) { - console.error('Error adding image to PDF:', error) - } + const range = selection.getRangeAt(0); + const container = range.startContainer; + const offset = range.startOffset; + + let parentSpan = container.nodeType === 3 ? container.parentNode : container.closest('span'); + if (!parentSpan || parentSpan.tagName !== 'SPAN') return; + + const text = container.textContent; + + // Caret at start → move before span + if (offset === 0) { + const newRange = document.createRange(); + newRange.setStartBefore(parentSpan); + newRange.collapse(true); + selection.removeAllRanges(); + selection.addRange(newRange); + return; + } + + // Caret at end → move after span + if (offset >= text.length) { + const newRange = document.createRange(); + newRange.setStartAfter(parentSpan); + newRange.collapse(true); + selection.removeAllRanges(); + selection.addRange(newRange); + return; + } + + // Mid-span → split into two spans + const spanStyle = parentSpan.style.cssText; + + const beforeSpan = document.createElement('span'); + beforeSpan.style.cssText = spanStyle; + beforeSpan.textContent = text.slice(0, offset); + + const afterSpan = document.createElement('span'); + afterSpan.style.cssText = spanStyle; + afterSpan.textContent = text.slice(offset); + + parentSpan.parentNode.insertBefore(beforeSpan, parentSpan); + parentSpan.parentNode.insertBefore(afterSpan, parentSpan.nextSibling); + parentSpan.remove(); + + setCaretAfterNode(beforeSpan.nextSibling); // move caret to after split +} + +// --- Create color swatches --- +highlightColors.forEach((color, i) => { + const swatch = document.createElement('div'); + swatch.className = 'color-swatch'; + swatch.style.backgroundColor = color; + if (i === 0) swatch.classList.add('active-selected'); + highlightPalette.appendChild(swatch); + + swatch.addEventListener('click', () => { + selectedColor = color; + highlightPalette.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active-selected')); + swatch.classList.add('active-selected'); + }); +}); + +// --- Toggle button click --- +highlightToggleBtn.addEventListener('click', (e) => { + e.stopPropagation(); + highlightEnabled = !highlightEnabled; + highlightToggleBtn.classList.toggle('active', highlightEnabled); + + const swatches = highlightPalette.querySelectorAll('.color-swatch'); + + if (highlightEnabled) { + highlightPalette.classList.add('active'); + highlightPalette.style.display = 'flex'; + const btnRect = highlightToggleBtn.getBoundingClientRect(); + const centerX = btnRect.left + btnRect.width / 2 + window.scrollX; + const centerY = btnRect.top + btnRect.height / 2 + window.scrollY; + highlightPalette.style.left = `${centerX - 60}px`; + highlightPalette.style.top = `${centerY - 60}px`; + + const radius = 50; + const arcStart = Math.PI; + const arcEnd = 2 * Math.PI; + const angleStep = (arcEnd - arcStart) / (swatches.length - 1); + swatches.forEach((swatch, i) => { + const angle = arcStart + i * angleStep; + const x = 60 + radius * Math.cos(angle) - 14; + const y = 60 + radius * Math.sin(angle) - 14; + swatch.style.transition = 'all 0.3s ease'; + setTimeout(() => { + swatch.style.left = `${x}px`; + swatch.style.top = `${y}px`; + swatch.style.transform = 'scale(1)'; + }, i * 50); + }); + } else { + swatches.forEach((swatch, i) => { + swatch.style.transition = 'all 0.2s ease'; + setTimeout(() => { + swatch.style.left = '60px'; + swatch.style.top = '60px'; + swatch.style.transform = 'scale(0)'; + }, i * 30); + }); + + setTimeout(() => { + highlightPalette.style.display = 'none'; + highlightPalette.classList.remove('active'); + }, swatches.length * 30 + 150); + } +}); + +// --- Apply highlight to selection --- +function applyHighlight(color) { + const selection = window.getSelection(); + if (selection.isCollapsed) return; + + const range = selection.getRangeAt(0); + const span = document.createElement('span'); + const isDarkMode = document.body.classList.contains('dark-mode'); + span.style.color = isDarkMode ? 'white' : 'black'; + span.style.backgroundColor = color; + span.textContent = range.toString(); + range.deleteContents(); + range.insertNode(span); + + setCaretAfterNode(span); + saveNoteContent(); +} + +// --- Mouseup listener --- +noteArea.addEventListener('mouseup', () => { + if (!highlightEnabled) return; + const selection = window.getSelection(); + if (!selection.isCollapsed) applyHighlight(selectedColor); +}); + +// --- Handle typing and input --- +noteArea.addEventListener('beforeinput', (e) => { + const selection = window.getSelection(); + if (!selection.rangeCount) return; + + // Remove placeholder if present + if (noteArea.classList.contains('empty')) { + noteArea.innerHTML = ''; + noteArea.classList.remove('empty'); + } + + const range = selection.getRangeAt(0); + + // Handle Enter + if (e.inputType === "insertParagraph") { + e.preventDefault(); + const br = document.createElement('br'); + insertNodeAtRange(br, range); + setCaretAfterNode(br); + saveNoteContent(); + return; + } + + // Only handle text insertion + if (e.inputType !== "insertText") return; + e.preventDefault(); + const text = e.data; + + splitSpanAtCaret(); + + const rangeAfterSplit = selection.getRangeAt(0); + if (highlightEnabled) { + const span = document.createElement('span'); + span.style.backgroundColor = selectedColor; + const isDarkMode = document.body.classList.contains('dark-mode'); + span.style.color = isDarkMode ? 'white' : 'black'; + span.textContent = text; + insertNodeAtRange(span, rangeAfterSplit); + setCaretAfterNode(span); + } else { + const textNode = document.createTextNode(text); + insertNodeAtRange(textNode, rangeAfterSplit); + setCaretAfterNode(textNode); } - doc.save('My_Notes.pdf') + saveNoteContent(); +}); + +async function downloadPDF() { + const { jsPDF } = window.jspdf; + const doc = new jsPDF("p", "mm", "a4"); + + const noteArea = document.getElementById("noteArea"); + const drawingCanvas = document.getElementById("drawingCanvas"); + + const noteCanvas = await html2canvas(noteArea, { + backgroundColor: "#ffffff", + scale: 2 + }); + + const combinedCanvas = document.createElement("canvas"); + combinedCanvas.width = noteCanvas.width; + combinedCanvas.height = noteCanvas.height; + const ctx = combinedCanvas.getContext("2d"); + + ctx.drawImage(noteCanvas, 0, 0); + + ctx.drawImage( + drawingCanvas, + 0, + 0, + combinedCanvas.width, + combinedCanvas.height + ); + + const imgData = combinedCanvas.toDataURL("image/png"); + const imgProps = doc.getImageProperties(imgData); + const pdfWidth = doc.internal.pageSize.getWidth(); + const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; + + doc.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight); + doc.save("My_Notes.pdf"); } + function getImageData(url) { return new Promise((resolve, reject) => { const img = new Image() @@ -261,4 +473,4 @@ function deleteAll() { noteArea.classList.add('empty') } -} +} \ No newline at end of file diff --git a/styling/jotpad.css b/styling/jotpad.css index 6786dc47..0e9e92aa 100644 --- a/styling/jotpad.css +++ b/styling/jotpad.css @@ -1,3 +1,4 @@ + /* JotPad (CSS) */ /* Hero Section */ @@ -83,6 +84,64 @@ .note-box.empty::before { content: 'Start typing here...'; color: var(--text-secondary); + pointer-events: none; +} + +.note-meta { + display: flex; + flex-wrap: wrap; + gap: 1rem 1.5rem; + align-items: center; + margin: 1.5rem 0; +} + +.note-meta label { + font-weight: 600; + color: var(--text-primary); +} + +.note-meta select, +.note-meta input { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.25rem; + background-color: var(--card-bg); + color: var(--text-primary); + font-size: 0.95rem; + outline: none; + transition: border 0.2s, box-shadow 0.2s; + appearance: none; + width: 200px; + min-width: 150px; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; +} + +.note-meta select:focus, +.note-meta input:focus { + border-color: var(--primary-500); + box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2); +} + +.note-meta select { + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20'%3E%3Cpolygon fill='%23666' points='0,0 20,0 10,10'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 0.65rem; +} + +.note-meta select option { + background-color: var(--card-bg); + color: var(--text-primary); +} + +.note-meta input::placeholder { + color: var(--text-secondary); +} + +#noteArea{ + white-space: pre-wrap; } #drawingCanvas { @@ -99,6 +158,8 @@ display: flex; justify-content: space-between; padding: 0.625rem; + align-items: center; + gap: 0.5rem; background-color: var(--bg-secondary); } @@ -120,3 +181,95 @@ background-color: var(--error); color: var(--white); } + +.highlight-toolbar { + position: absolute; + display: flex; + gap: 0.5rem; + padding: 0.5rem; + background: #fff; + border: 1px solid #ccc; + border-radius: 0.25rem; + box-shadow: 0 0 5px rgba(0,0,0,0.2); + z-index: 100; +} + +.highlight-btn { + width: 24px; + height: 24px; + border: none; + cursor: pointer; + border-radius: 3px; +} + +.bookmark-btn { + background-color: #f0f0f0; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + font-weight: bold; + border-radius: 0.25rem; +} + +.bookmark-btn.bookmarked { + background-color: #ffd700; +} + +.icon-btn { + background-color: var(--bg-secondary); + border: none; + padding: 0.5rem; + border-radius: 0.25rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + transition: background-color 0.2s, color 0.2s; +} + +.icon-btn:hover { + background-color: var(--primary-100); +} + +.icon-btn.active { + color: var(--primary-500); + background-color: var(--primary-50); +} +.circular-palette { + position: absolute; + width: 120px; + height: 120px; + border-radius: 50%; + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + transition: all 0.3s ease; + pointer-events: none; +} + +.circular-palette .color-swatch { + position: absolute; + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transform: scale(0); + transition: all 0.3s ease; + pointer-events: auto; +} + +.circular-palette.active { + display: flex; +} + +.circular-palette.active .color-swatch { + transform: scale(1); +} + +.circular-palette .color-swatch.active-selected { + border-color: #000; + transform: scale(1.2); +} \ No newline at end of file