composer.html (5163B) - raw
1 <!DOCTYPE html> 2 <!-- 3 Composer by Oscar Benedito <oscar@oscarbenedito.com> 4 5 Minimalistic interface to write without distractions. It is a standalone HTML 6 file. It will save your progress as long as you don't delete browser data. 7 --> 8 <html lang="en"> 9 <head> 10 <meta charset="utf-8"> 11 <meta name="viewport" content="width=device-width, initial-scale=1"> 12 <meta name="author" content="Oscar Benedito"> 13 <title>Composer</title> 14 <style> 15 :root { 16 --font-family: 'serif'; 17 --nav-height: 3em; 18 --text-color: #000; 19 --bg-color: #fff; 20 --nav-color: hsl(0, 0%, 46%); 21 } 22 .toggled { 23 --text-color: hsl(0, 0%, 93%); 24 --bg-color: hsl(0, 0%, 13%); 25 --nav-color: hsl(0, 0%, 56%); 26 } 27 @media (prefers-color-scheme: dark) { 28 :root { 29 --text-color: hsl(0, 0%, 93%); 30 --bg-color: hsl(0, 0%, 13%); 31 --nav-color: hsl(0, 0%, 56%); 32 } 33 } 34 @media (prefers-color-scheme: dark) { 35 .toggled { 36 --text-color: #000; 37 --bg-color: #fff; 38 --nav-color: hsl(0, 0%, 46%); 39 } 40 } 41 .mono { 42 --font-family: 'mono'; 43 } 44 html { 45 background-color: var(--bg-color); 46 } 47 textarea { 48 line-height: 1.4em; 49 font-family: var(--font-family); 50 padding: 1em calc((100% - 720px)/2); 51 background-color: var(--bg-color); 52 color: var(--text-color); 53 margin: 0; 54 height: calc(100% - var(--nav-height)); 55 font-size: 1.2em; 56 box-sizing: border-box; 57 resize: none; 58 right: 0; 59 top: var(--nav-height); 60 bottom: 0; 61 left: 0; 62 width: 100%; 63 position: fixed; 64 border: 0; 65 outline: 0; 66 } 67 nav { 68 text-align: center; 69 height: var(--nav-height); 70 line-height: var(--nav-height); 71 font-size: 1.2em; 72 opacity: 0; 73 color: var(--nav-color); 74 background-color: var(--bg-color); 75 position: fixed; 76 top: 0; 77 right: 0; 78 left: 0; 79 z-index: 1; 80 } 81 nav a { 82 color: var(--nav-color); 83 text-decoration: none; 84 cursor: pointer; 85 } 86 nav:hover { 87 opacity: 1; 88 } 89 </style> 90 </head> 91 <body> 92 <nav> 93 <span id="word-count">0 words · 0 minutes</span> · <a onclick="toggleTheme()">Toggle theme</a> · <a onclick="toggleFont()">Toggle font</a> 94 </nav> 95 <textarea id="composer" placeholder="Start writing..." autofocus></textarea> 96 <script type="text/javascript"> 97 // @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 98 var $composer = document.getElementById('composer'); 99 var $wordcount = document.getElementById('word-count'); 100 var key; 101 var typingTimer; 102 var typingInterval = 200; 103 function updateWordCount() { 104 var words = 0; 105 var content = $composer.value.trim(); 106 if (content != '') { 107 words = content.replace(/\s+/gi, ' ').split(' ').length; 108 } 109 minutes = Math.floor(words/140); 110 $wordcount.textContent = words + ' word' + (words != 1 ? 's' : '') + ' ยท ' + minutes + ' minute' + (minutes != 1 ? 's' : ''); 111 } 112 function loadContents() { 113 var content = localStorage.getItem(key); 114 if (content != null) { 115 $composer.value = content; 116 } 117 } 118 var updateContents = function() { 119 if ($composer.value == '') { 120 localStorage.removeItem(key); 121 } 122 else { 123 localStorage.setItem(key, $composer.value); 124 } 125 updateWordCount(); 126 } 127 var resetTimer = function() { 128 clearTimeout(typingTimer); 129 typingTimer = setTimeout(updateContents, typingInterval); 130 } 131 function downloadData(filename, text) { 132 var text = localStorage.getItem(key); 133 if (text != null && text != '') { 134 var tmpElement = document.createElement('a'); 135 tmpElement.setAttribute('href', 'data:text/markdown;charset=utf-8,' + encodeURIComponent(text)); 136 tmpElement.setAttribute('download', key + '.md'); 137 tmpElement.style.display = 'none'; 138 139 document.body.appendChild(tmpElement); 140 tmpElement.click(); 141 document.body.removeChild(tmpElement); 142 } 143 } 144 var saveEvent = function(event) { 145 if (event.keyCode == 83 && (event.metaKey || event.ctrlKey)) { 146 clearTimeout(typingTimer); 147 event.preventDefault(); 148 updateContents(); 149 if (event.shiftKey) 150 downloadData(); 151 } 152 } 153 function toggleTheme() { 154 if (localStorage && localStorage.getItem('theme') == 'toggled') { 155 localStorage.removeItem('theme'); 156 } else if (localStorage) { 157 localStorage.setItem('theme', 'toggled'); 158 } 159 document.documentElement.classList.toggle('toggled'); 160 } 161 function toggleFont() { 162 if (localStorage && localStorage.getItem('font') == 'mono') { 163 localStorage.removeItem('font'); 164 } else if (localStorage) { 165 localStorage.setItem('font', 'mono'); 166 } 167 document.documentElement.classList.toggle('mono'); 168 } 169 key = (new URLSearchParams(window.location.search)).get('key'); 170 if (key == '' || key == null) { 171 key = 'null'; 172 } 173 key = 'k-' + key; 174 if (localStorage && localStorage.getItem('theme') == 'toggled') { 175 document.documentElement.classList.toggle('toggled'); 176 } 177 if (localStorage && localStorage.getItem('font') == 'mono') { 178 document.documentElement.classList.toggle('mono'); 179 } 180 $composer.addEventListener('keyup input', resetTimer); 181 $composer.addEventListener('keydown', resetTimer); 182 $composer.addEventListener('input', resetTimer); 183 window.addEventListener('beforeunload', updateContents); 184 window.addEventListener('keydown', saveEvent); 185 loadContents(); 186 updateWordCount(); 187 // @license-end 188 </script> 189 </body> 190 </html>