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