localnotes.html 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. <!DOCTYPE html>
  2. <!--
  3. TODO
  4. [x] document json with metadata, title and UID key name
  5. [x] app metadata, last open file
  6. [x] start off with initial document
  7. [x] better system for 'new note' and 'open note'
  8. [ ] assertions about document health
  9. [x] notes have created and edited date
  10. [x] can delete notes
  11. [ ] can duplicate notes
  12. [ ] better in-memory storage of notes (don't have to parse every time i render sidebar or do a search)
  13. [ ] use indexedDB when available
  14. [ ] common interface for both
  15. [ ] mobile styling, probably with expandable side bar
  16. [x] mini Vdom
  17. [x] class and id parsing from tag strings like 'div.foo#bar'
  18. [x] draw sidebar
  19. [x] click file to open
  20. [x] new file button
  21. [x] document title
  22. [ ] contentEditable changes sanitize dom
  23. [ ] greenlist of top level elements, strip everything else
  24. [ ] selection styling (bold, italic). I actually think there's contenteditable commands to apply these things?
  25. [ ] drag and drop restructuring
  26. basically want to say SetUpDrag(el, {start: f, drag: f, end: f}), drop target and insertion could be
  27. genericized as well
  28. [ ] generic system, will use in sidebar and document
  29. [ ] configure hotspots for drop before, inside, after, left, right
  30. [ ] protocol style actions for insertion
  31. [ ] insert context menu
  32. [ ] slash, todo, and bullet list inference at start of top level element
  33. [ ] key commands
  34. [ ] linking
  35. [ ] 'note' links, show page title, have uid
  36. [ ] content editing strategy for links, can't edit the text, can delete (maybe if inside a selection)
  37. [ ] emoji picker
  38. [ ] 'file structure' graph in meta
  39. [ ] notes can have 'child' notes
  40. [ ] simple search
  41. [ ] export all data
  42. [ ] storage space estimator
  43. [ ] handle running out of space (alert user, ???)
  44. [ ] can navigate between title and content with cursor
  45. [ ] history should preserve per document during a session
  46. BUGS
  47. [ ] new block in h1 doesn't preserve indent. Might need to lint new blocks as todo needs it's checked cleared too
  48. [ ] pasting sticks content inside of a block, not even sure of the desired behaviour here, maybe
  49. contents should be 'children' only in so far as having the parent's indentation
  50. [x] <img> are really hard to edit around, should these be wrapped in a div or something?
  51. [ ] 'li' has different positioning in chrome vs firefox
  52. [ ] pasting blocks picks up style as inline, which breaks things
  53. [x] selection/insertion is triggering history states
  54. [ ] todo blocks need mouse events for more than just the checkbox (should check bounds on the rect for toggling it)
  55. [x] chrome - when trying to drag a block that is partially range selected the selection collapses and it fails
  56. [ ] the mouse target can stick around, it should use that nearest neighbor function
  57. [x] emoji encoding isn't surviving file transfers
  58. [ ] deleting the last note can get app into bad state where meta "current_note" points to non existant note
  59. Today's project
  60. [ ] I'm in need of a debug section that shows the current range with live update
  61. [ ] indexedDB & localStorage interface
  62. [x] switch localstorage calls to callbacks
  63. [x] drop images in note
  64. [ ] drag and drop blocks
  65. [x] draw CSS drag handle on every element
  66. [x] detect drag is over handle
  67. [x] handle shows up on hover
  68. [x] and selection
  69. [x] insertion tracking shows up only during handle drag
  70. [x] store 'selected' elements during drag
  71. [x] drag img has all selected elements
  72. [x] dragged elements have low opacity
  73. [x] drag can be cancelled, either dropping outside of note or on top of one of the dragged elements
  74. [/] insertion has 'in', 'before', 'after', 'left', 'right' detection
  75. [x] drop inserts selected elements
  76. [x] collapsed cursor selection shouldn't trigger 'selected'?
  77. [x] support touch events
  78. [ ] mobile needs to update block selection when range changes
  79. [x] change 'target' and 'selection' from classes to a list of elements - this should some bugs with platform dragging and
  80. avoids issues with history saving changes to ephemeral classes
  81. [x] the handles should be their own nodes outside of the note that are positioned
  82. [x] use nth child to do drag styling on selection? or disable history saving during drag?
  83. [x] reimplement the ondrag handler
  84. [x] note should still have it's drag/drop prevented
  85. [x] single mouse target should get a handle too
  86. [x] SELECTION needs to include TARGET and be ordered correctly
  87. -->
  88. <head>
  89. <meta charset="utf-8">
  90. <link rel="shortcut icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='.8 .8 14.4 14.4'><path d='M10 8.99a1 1 0 00-.695 1.717l4.004 4a1 1 0 101.414-1.414l-4.004-4A1 1 0 0010 8.99z' fill='%2380b0ff' stroke='%235D7FDDaa' stroke-width='.3'/><path d='M6.508 1C3.48 1 1.002 3.474 1.002 6.5S3.48 12 6.508 12s5.504-2.474 5.504-5.5S9.536 1 6.508 1zm0 2a3.486 3.486 0 013.504 3.5c0 1.944-1.556 3.5-3.504 3.5a3.488 3.488 0 01-3.506-3.5C3.002 4.556 4.56 3 6.508 3z' fill='%2380b0ff' stroke='%235D7FDDaa' stroke-width='.3'/></svg>">
  91. <style>
  92. body {
  93. margin: 0;
  94. font-family: 'Courier New', Courier, monospace;
  95. }
  96. sidebar, document, note {
  97. box-sizing: border-box;
  98. }
  99. .hidden {
  100. visibility: hidden;
  101. pointer-events: none;
  102. }
  103. sidebar {
  104. position: absolute;
  105. top: 0px;
  106. bottom: 0px;
  107. width: 200px;
  108. border-right: 1px solid black;
  109. padding: 1em;
  110. }
  111. sidebar div.note-list {
  112. font-size: 0.8em;
  113. padding: 12px 0px;
  114. cursor: pointer;
  115. }
  116. sidebar p {
  117. margin-top: 4px;
  118. margin: 0em;
  119. }
  120. sidebar p:hover {
  121. text-decoration: underline;
  122. }
  123. sidebar p button {
  124. cursor: pointer;
  125. margin-left: 1em;
  126. padding: 0px 4px;
  127. font-size: 9px;
  128. }
  129. /* * {outline: 1px solid rgba(255,0,0,0.1);} */
  130. document {
  131. position: absolute;
  132. overflow: auto;
  133. left: 200px;
  134. top: 0px;
  135. bottom: 0px;
  136. right: 0px;
  137. padding: 1em;
  138. }
  139. note, #title {
  140. outline: none
  141. }
  142. #title {
  143. font-size: 4em;
  144. padding: 0px 24px;
  145. }
  146. note {
  147. width: 100%;
  148. min-height: calc(100vh - 200px);
  149. position: relative;
  150. display: block;
  151. padding: 2em;
  152. }
  153. h1,h2,h3,h4,p,li {
  154. min-height: 1em;
  155. }
  156. li {
  157. left: 14px;
  158. position: relative;
  159. }
  160. .todo {
  161. display: block;
  162. margin: 0.2em 0;
  163. line-height: 1em;
  164. text-indent: -1.8em;
  165. padding-left: 1.8em;
  166. pointer-events: none;
  167. }
  168. .todo::before {
  169. content: '';
  170. display: inline-block;
  171. width: 1.2em;
  172. height: 1.2em;
  173. margin-right: 0.5em;
  174. position: relative;
  175. top: 0.25em;
  176. box-sizing: border-box;
  177. border-radius: 2px;
  178. border: 3px solid #3a8dec;
  179. cursor: pointer;
  180. pointer-events: auto;
  181. }
  182. .todo.checked::before {
  183. background: #3a8dec;
  184. }
  185. /* s = "";for (i = 1; i <= 20; i++) {s += `.indent${i} {margin-left: ${i*24}px;}\n`}; console.log(s) */
  186. .indent1 {margin-left: 24px;}
  187. .indent2 {margin-left: 48px;}
  188. .indent3 {margin-left: 72px;}
  189. .indent4 {margin-left: 96px;}
  190. .indent5 {margin-left: 120px;}
  191. .indent6 {margin-left: 144px;}
  192. .indent7 {margin-left: 168px;}
  193. .indent8 {margin-left: 192px;}
  194. .indent9 {margin-left: 216px;}
  195. .indent10 {margin-left: 240px;}
  196. .indent11 {margin-left: 264px;}
  197. .indent12 {margin-left: 288px;}
  198. .indent13 {margin-left: 312px;}
  199. .indent14 {margin-left: 336px;}
  200. .indent15 {margin-left: 360px;}
  201. .indent16 {margin-left: 384px;}
  202. .indent17 {margin-left: 408px;}
  203. .indent18 {margin-left: 432px;}
  204. .indent19 {margin-left: 456px;}
  205. .indent20 {margin-left: 480px;}
  206. /* note * {
  207. outline: 1px solid rgba(255, 0, 0, 0.199);
  208. } */
  209. .insertion::after {
  210. content: '';
  211. display: block;
  212. position: absolute;
  213. width: 100%;
  214. height: 0px;
  215. border: 0px solid #3a8dec;
  216. border-top-width: 4px;
  217. }
  218. .insertion.ibefore::after {
  219. top: -10px;
  220. }
  221. .insertion::after, .insertion.iafter::after {
  222. bottom: -10px;
  223. }
  224. note > * {
  225. position: relative;
  226. }
  227. selection {
  228. position: absolute;
  229. display: block;
  230. cursor: grab;
  231. pointer-events: auto;
  232. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAABbmlDQ1BpY2MAACiRdZG9S0JhFIcftTDKkFAiosHBokEhCqKxbHCREDPIatHrV+DH5V4lpDVoaRAaopa+hv6DWoPWgiAogojG5r6WkNu5Kiih5/Le8/B7z+9w7rlgDeWUvN4zD/lCSYsEA57V2JrH/o4FF26GGYoruroQDofoGj+PUi3x4Dd7da/rGAPJlK6ApU94VlG1krBMQ2irpJq8J+xWsvGk8ImwT5MBhW9NPdHgN5MzDf4yWYtGFsFq9vRk2jjRxkpWywtPCnvzubLSnMf8EkeqsLIseVTOGDoRggTwkKDMJjlK+CUXZGedfVN13xJF8SjyVqmgiSNDVrw+UcvSNSU5LXpKnhwVc+//96mnZ6Yb3R0B6H01jM9xsO9DrWoYv6eGUTsD2wtcF1r+ouxp7lv0akvzHoNzBy5vWlriAK52YeRZjWvxumSTY02n4eMCBmPguof+9caumvecP0F0W37RHRwewYTUOzf+AOECZ/uSmLScAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAKklEQVQIW2P8DwQMSIAJxG5oaGA4cuQIRBikYvv27f/fv38PYv5nRNcCAFvgGrGltVmQAAAAAElFTkSuQmCC);
  233. }
  234. ._dragging {
  235. opacity: 0.3;
  236. }
  237. /* note *::before, note *::after {
  238. content: "<?>";
  239. display: inline-block;
  240. font-family: courier;
  241. font-size: 9px;
  242. color: purple;
  243. }
  244. note p::before {content: "<p>"}
  245. note p::after {content: "</p>"}
  246. note div::before {content: "<div>"}
  247. note div::after {content: "</div>"} */
  248. * { outline: 1px solid rgba(256,0,0,0.2) !important;}
  249. </style>
  250. </head>
  251. <body>
  252. <sidebar>
  253. <h2>localnotes</h2>
  254. <content></content>
  255. </sidebar>
  256. <document>
  257. <h1 id="title" contenteditable="true">title</h1>
  258. <note contenteditable="true" ></note>
  259. </document>
  260. <selection></selection>
  261. <selection></selection>
  262. <!-- https://unicode.org/emoji/charts-12.0/emoji-style.html
  263. res = []; document.querySelectorAll("a.plain").forEach((el)=>res.push(el.textContent)) -->
  264. <script>
  265. emoji = ["☹️","☠️","❣️","❤️","🕳️","🗨️","🗯️","🖐️","✌️","☝️","✍️","👁️","🕵️","🕴️","⛷️","🏌️","⛹️","🏋️","🗣️","🐿️","🕊️","🕷️","🕸️","🏵️","☘️","🌶️","🍽️","🗺️","🏔️","⛰️","🏕️","🏖️","🏜️","🏝️","🏞️","🏟️","🏛️","🏗️","🏘️","🏚️","⛩️","🏙️","♨️","🏎️","🏍️","🛣️","🛤️","🛢️","🛳️","⛴️","🛥️","✈️","🛩️","🛰️","🛎️","⏱️","⏲️","🕰️","🌡️","☀️","☁️","⛈️","🌤️","🌥️","🌦️","🌧️","🌨️","🌩️","🌪️","🌫️","🌬️","☂️","⛱️","❄️","☃️","☄️","🎗️","🎟️","🎖️","⛸️","🕹️","♠️","♥️","♦️","♣️","♟️","🖼️","🕶️","🛍️","⛑️","🎙️","🎚️","🎛️","☎️","🖥️","🖨️","⌨️","🖱️","🖲️","🎞️","📽️","🕯️","🗞️","🏷️","✉️","🗳️","✏️","✒️","🖋️","🖊️","🖌️","🖍️","🗂️","🗒️","🗓️","🖇️","✂️","🗃️","🗄️","🗑️","🗝️","⛏️","⚒️","🛠️","🗡️","⚔️","🛡️","⚙️","🗜️","⚖️","⛓️","⚗️","🛏️","🛋️","⚰️","⚱️","⚠️","☢️","☣️","⬆️","↗️","➡️","↘️","⬇️","↙️","⬅️","↖️","↕️","↔️","↩️","↪️","⤴️","⤵️","⚛️","🕉️","✡️","☸️","☯️","✝️","☦️","☪️","☮️","▶️","⏭️","⏯️","◀️","⏮️","⏸️","⏹️","⏺️","⏏️","♀️","♂️","⚕️","♾️","♻️","⚜️","☑️","✔️","✖️","〽️","✳️","✴️","❇️","‼️","⁉️","〰️","©️","®️","™️","#️⃣","*️⃣","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🅰️","🅱️","ℹ️","Ⓜ️","🅾️","🅿️","🈂️","🈷️","㊗️","㊙️","◼️","◻️","▪️","▫️","🏳️","😀","😃","😄","😁","😆","😅","🤣","😂","🙂","🙃","😉","😊","😇","🥰","😍","🤩","😘","😗","😚","😙","😋","😛","😜","🤪","😝","🤑","🤗","🤭","🤫","🤔","🤐","🤨","😐","😑","😶","😏","😒","🙄","😬","🤥","😌","😔","😪","🤤","😴","😷","🤒","🤕","🤢","🤮","🤧","🥵","🥶","🥴","😵","🤯","🤠","🥳","😎","🤓","🧐","😕","😟","🙁","😮","😯","😲","😳","🥺","😦","😧","😨","😰","😥","😢","😭","😱","😖","😣","😞","😓","😩","😫","🥱","😤","😡","😠","🤬","😈","👿","💀","💩","🤡","👹","👺","👻","👽","👾","🤖","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🙈","🙉","🙊","💋","💌","💘","💝","💖","💗","💓","💞","💕","💟","💔","🧡","💛","💚","💙","💜","🤎","🖤","🤍","💯","💢","💥","💫","💦","💨","💣","💬","💭","💤","👋","🤚","✋","🖖","👌","🤏","🤞","🤟","🤘","🤙","👈","👉","👆","🖕","👇","👍","👎","✊","👊","🤛","🤜","👏","🙌","👐","🤲","🤝","🙏","💅","🤳","💪","🦾","🦿","🦵","🦶","👂","🦻","👃","🧠","🦷","🦴","👀","👅","👄","👶","🧒","👦","👧","🧑","👱","👨","🧔","👩","🧓","👴","👵","🙍","🙎","🙅","🙆","💁","🙋","🧏","🙇","🤦","🤷","👮","💂","👷","🤴","👸","👳","👲","🧕","🤵","👰","🤰","🤱","👼","🎅","🤶","🦸","🦹","🧙","🧚","🧛","🧜","🧝","🧞","🧟","💆","💇","🚶","🧍","🧎","🏃","💃","🕺","👯","🧖","🧗","🤺","🏇","🏂","🏄","🚣","🏊","🚴","🚵","🤸","🤼","🤽","🤾","🤹","🧘","🛀","🛌","👭","👫","👬","💏","💑","👪","👤","👥","👣","🦰","🦱","🦳","🦲","🐵","🐒","🦍","🦧","🐶","🐕","🦮","🐩","🐺","🦊","🦝","🐱","🐈","🦁","🐯","🐅","🐆","🐴","🐎","🦄","🦓","🦌","🐮","🐂","🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🦙","🦒","🐘","🦏","🦛","🐭","🐁","🐀","🐹","🐰","🐇","🦔","🦇","🐻","🐨","🐼","🦥","🦦","🦨","🦘","🦡","🐾","🦃","🐔","🐓","🐣","🐤","🐥","🐦","🐧","🦅","🦆","🦢","🦉","🦩","🦚","🦜","🐸","🐊","🐢","🦎","🐍","🐲","🐉","🦕","🦖","🐳","🐋","🐬","🐟","🐠","🐡","🦈","🐙","🐚","🐌","🦋","🐛","🐜","🐝","🐞","🦗","🦂","🦟","🦠","💐","🌸","💮","🌹","🥀","🌺","🌻","🌼","🌷","🌱","🌲","🌳","🌴","🌵","🌾","🌿","🍀","🍁","🍂","🍃","🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭","🍎","🍏","🍐","🍑","🍒","🍓","🥝","🍅","🥥","🥑","🍆","🥔","🥕","🌽","🥒","🥬","🥦","🧄","🧅","🍄","🥜","🌰","🍞","🥐","🥖","🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩","🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯","🥙","🧆","🥚","🍳","🥘","🍲","🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘","🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣","🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀","🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩","🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭","🍮","🍯","🍼","🥛","☕","🍵","🍶","🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃","🥤","🧃","🧉","🧊","🥢","🍴","🥄","🔪","🏺","🌍","🌎","🌏","🌐","🗾","🧭","🌋","🗻","🧱","🏠","🏡","🏢","🏣","🏤","🏥","🏦","🏨","🏩","🏪","🏫","🏬","🏭","🏯","🏰","💒","🗼","🗽","⛪","🕌","🛕","🕍","🕋","⛲","⛺","🌁","🌃","🌄","🌅","🌆","🌇","🌉","🎠","🎡","🎢","💈","🎪","🚂","🚃","🚄","🚅","🚆","🚇","🚈","🚉","🚊","🚝","🚞","🚋","🚌","🚍","🚎","🚐","🚑","🚒","🚓","🚔","🚕","🚖","🚗","🚘","🚙","🚚","🚛","🚜","🛵","🦽","🦼","🛺","🚲","🛴","🛹","🚏","⛽","🚨","🚥","🚦","🛑","🚧","⚓","⛵","🛶","🚤","🚢","🛫","🛬","🪂","💺","🚁","🚟","🚠","🚡","🚀","🛸","🧳","⌛","⏳","⌚","⏰","🕛","🕧","🕐","🕜","🕑","🕝","🕒","🕞","🕓","🕟","🕔","🕠","🕕","🕡","🕖","🕢","🕗","🕣","🕘","🕤","🕙","🕥","🕚","🕦","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚","🌛","🌜","🌝","🌞","🪐","⭐","🌟","🌠","🌌","⛅","🌀","🌈","🌂","☔","⚡","⛄","🔥","💧","🌊","🎃","🎄","🎆","🎇","🧨","✨","🎈","🎉","🎊","🎋","🎍","🎎","🎏","🎐","🎑","🧧","🎀","🎁","🎫","🏆","🏅","🥇","🥈","🥉","⚽","⚾","🥎","🏀","🏐","🏈","🏉","🎾","🥏","🎳","🏏","🏑","🏒","🥍","🏓","🏸","🥊","🥋","🥅","⛳","🎣","🤿","🎽","🎿","🛷","🥌","🎯","🪀","🪁","🎱","🔮","🧿","🎮","🎰","🎲","🧩","🧸","🃏","🀄","🎴","🎭","🎨","🧵","🧶","👓","🥽","🥼","🦺","👔","👕","👖","🧣","🧤","🧥","🧦","👗","👘","🥻","🩱","🩲","🩳","👙","👚","👛","👜","👝","🎒","👞","👟","🥾","🥿","👠","👡","🩰","👢","👑","👒","🎩","🎓","🧢","📿","💄","💍","💎","🔇","🔈","🔉","🔊","📢","📣","📯","🔔","🔕","🎼","🎵","🎶","🎤","🎧","📻","🎷","🎸","🎹","🎺","🎻","🪕","🥁","📱","📲","📞","📟","📠","🔋","🔌","💻","💽","💾","💿","📀","🧮","🎥","🎬","📺","📷","📸","📹","📼","🔍","🔎","💡","🔦","🏮","🪔","📔","📕","📖","📗","📘","📙","📚","📓","📒","📃","📜","📄","📰","📑","🔖","💰","💴","💵","💶","💷","💸","💳","🧾","💹","💱","💲","📧","📨","📩","📤","📥","📦","📫","📪","📬","📭","📮","📝","💼","📁","📂","📅","📆","📇","📈","📉","📊","📋","📌","📍","📎","📏","📐","🔒","🔓","🔏","🔐","🔑","🔨","🪓","🔫","🏹","🔧","🔩","🦯","🔗","🧰","🧲","🧪","🧫","🧬","🔬","🔭","📡","💉","🩸","💊","🩹","🩺","🚪","🪑","🚽","🚿","🛁","🪒","🧴","🧷","🧹","🧺","🧻","🧼","🧽","🧯","🛒","🚬","🗿","🏧","🚮","🚰","♿","🚹","🚺","🚻","🚼","🚾","🛂","🛃","🛄","🛅","🚸","⛔","🚫","🚳","🚭","🚯","🚱","🚷","📵","🔞","🔃","🔄","🔙","🔚","🔛","🔜","🔝","🛐","🕎","🔯","♈","♉","♊","♋","♌","♍","♎","♏","♐","♑","♒","♓","⛎","🔀","🔁","🔂","⏩","⏪","🔼","⏫","🔽","⏬","🎦","🔅","🔆","📶","📳","📴","🔱","📛","🔰","⭕","✅","❌","❎","➕","➖","➗","➰","➿","❓","❔","❕","❗","🔟","🔠","🔡","🔢","🔣","🔤","🆎","🆑","🆒","🆓","🆔","🆕","🆖","🆗","🆘","🆙","🆚","🈁","🈶","🈯","🉐","🈹","🈚","🈲","🉑","🈸","🈴","🈳","🈺","🈵","🔴","🟠","🟡","🟢","🔵","🟣","🟤","⚫","⚪","🟥","🟧","🟨","🟩","🟦","🟪","🟫","⬛","⬜","◾","◽","🔶","🔷","🔸","🔹","🔺","🔻","💠","🔘","🔳","🔲","🏁","🚩","🎌","🏴","🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲","🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽","🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭","🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷","🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨","🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲","🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽","🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴","🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸","🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷","🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮","🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹","🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹","🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴","🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵","🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷","🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰","🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨","🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲","🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺","🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪","🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺","🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰","🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾","🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧","🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰","🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻","🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬","🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷","🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳","🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮","🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦","🇿🇲","🇿🇼","🏴󠁧󠁢󠁥󠁮󠁧󠁿","🏴󠁧󠁢󠁳󠁣󠁴󠁿","🏴󠁧󠁢󠁷󠁬󠁳󠁿","👋🏻","👋🏼","👋🏽","👋🏾","👋🏿","🤚🏻","🤚🏼","🤚🏽","🤚🏾","🤚🏿","🖐🏻","🖐🏼","🖐🏽","🖐🏾","🖐🏿","✋🏻","✋🏼","✋🏽","✋🏾","✋🏿","🖖🏻","🖖🏼","🖖🏽","🖖🏾","🖖🏿","👌🏻","👌🏼","👌🏽","👌🏾","👌🏿","🤏🏻","🤏🏼","🤏🏽","🤏🏾","🤏🏿","✌🏻","✌🏼","✌🏽","✌🏾","✌🏿","🤞🏻","🤞🏼","🤞🏽","🤞🏾","🤞🏿","🤟🏻","🤟🏼","🤟🏽","🤟🏾","🤟🏿","🤘🏻","🤘🏼","🤘🏽","🤘🏾","🤘🏿","🤙🏻","🤙🏼","🤙🏽","🤙🏾","🤙🏿","👈🏻","👈🏼","👈🏽","👈🏾","👈🏿","👉🏻","👉🏼","👉🏽","👉🏾","👉🏿","👆🏻","👆🏼","👆🏽","👆🏾","👆🏿","🖕🏻","🖕🏼","🖕🏽","🖕🏾","🖕🏿","👇🏻","👇🏼","👇🏽","👇🏾","👇🏿","☝🏻","☝🏼","☝🏽","☝🏾","☝🏿","👍🏻","👍🏼","👍🏽","👍🏾","👍🏿","👎🏻","👎🏼","👎🏽","👎🏾","👎🏿","✊🏻","✊🏼","✊🏽","✊🏾","✊🏿","👊🏻","👊🏼","👊🏽","👊🏾","👊🏿","🤛🏻","🤛🏼","🤛🏽","🤛🏾","🤛🏿","🤜🏻","🤜🏼","🤜🏽","🤜🏾","🤜🏿","👏🏻","👏🏼","👏🏽","👏🏾","👏🏿","🙌🏻","🙌🏼","🙌🏽","🙌🏾","🙌🏿","👐🏻","👐🏼","👐🏽","👐🏾","👐🏿","🤲🏻","🤲🏼","🤲🏽","🤲🏾","🤲🏿","🙏🏻","🙏🏼","🙏🏽","🙏🏾","🙏🏿","✍🏻","✍🏼","✍🏽","✍🏾","✍🏿","💅🏻","💅🏼","💅🏽","💅🏾","💅🏿","🤳🏻","🤳🏼","🤳🏽","🤳🏾","🤳🏿","💪🏻","💪🏼","💪🏽","💪🏾","💪🏿","🦵🏻","🦵🏼","🦵🏽","🦵🏾","🦵🏿","🦶🏻","🦶🏼","🦶🏽","🦶🏾","🦶🏿","👂🏻","👂🏼","👂🏽","👂🏾","👂🏿","🦻🏻","🦻🏼","🦻🏽","🦻🏾","🦻🏿","👃🏻","👃🏼","👃🏽","👃🏾","👃🏿","👶🏻","👶🏼","👶🏽","👶🏾","👶🏿","🧒🏻","🧒🏼","🧒🏽","🧒🏾","🧒🏿","👦🏻","👦🏼","👦🏽","👦🏾","👦🏿","👧🏻","👧🏼","👧🏽","👧🏾","👧🏿","🧑🏻","🧑🏼","🧑🏽","🧑🏾","🧑🏿","👱🏻","👱🏼","👱🏽","👱🏾","👱🏿","👨🏻","👨🏼","👨🏽","👨🏾","👨🏿","🧔🏻","🧔🏼","🧔🏽","🧔🏾","🧔🏿","👩🏻","👩🏼","👩🏽","👩🏾","👩🏿","🧓🏻","🧓🏼","🧓🏽","🧓🏾","🧓🏿","👴🏻","👴🏼","👴🏽","👴🏾","👴🏿","👵🏻","👵🏼","👵🏽","👵🏾","👵🏿","🙍🏻","🙍🏼","🙍🏽","🙍🏾","🙍🏿","🙎🏻","🙎🏼","🙎🏽","🙎🏾","🙎🏿","🙅🏻","🙅🏼","🙅🏽","🙅🏾","🙅🏿","🙆🏻","🙆🏼","🙆🏽","🙆🏾","🙆🏿","💁🏻","💁🏼","💁🏽","💁🏾","💁🏿","🙋🏻","🙋🏼","🙋🏽","🙋🏾","🙋🏿","🧏🏻","🧏🏼","🧏🏽","🧏🏾","🧏🏿","🙇🏻","🙇🏼","🙇🏽","🙇🏾","🙇🏿","🤦🏻","🤦🏼","🤦🏽","🤦🏾","🤦🏿","🤷🏻","🤷🏼","🤷🏽","🤷🏾","🤷🏿","👮🏻","👮🏼","👮🏽","👮🏾","👮🏿","🕵🏻","🕵🏼","🕵🏽","🕵🏾","🕵🏿","💂🏻","💂🏼","💂🏽","💂🏾","💂🏿","👷🏻","👷🏼","👷🏽","👷🏾","👷🏿","🤴🏻","🤴🏼","🤴🏽","🤴🏾","🤴🏿","👸🏻","👸🏼","👸🏽","👸🏾","👸🏿","👳🏻","👳🏼","👳🏽","👳🏾","👳🏿","👲🏻","👲🏼","👲🏽","👲🏾","👲🏿","🧕🏻","🧕🏼","🧕🏽","🧕🏾","🧕🏿","🤵🏻","🤵🏼","🤵🏽","🤵🏾","🤵🏿","👰🏻","👰🏼","👰🏽","👰🏾","👰🏿","🤰🏻","🤰🏼","🤰🏽","🤰🏾","🤰🏿","🤱🏻","🤱🏼","🤱🏽","🤱🏾","🤱🏿","👼🏻","👼🏼","👼🏽","👼🏾","👼🏿","🎅🏻","🎅🏼","🎅🏽","🎅🏾","🎅🏿","🤶🏻","🤶🏼","🤶🏽","🤶🏾","🤶🏿","🦸🏻","🦸🏼","🦸🏽","🦸🏾","🦸🏿","🦹🏻","🦹🏼","🦹🏽","🦹🏾","🦹🏿","🧙🏻","🧙🏼","🧙🏽","🧙🏾","🧙🏿","🧚🏻","🧚🏼","🧚🏽","🧚🏾","🧚🏿","🧛🏻","🧛🏼","🧛🏽","🧛🏾","🧛🏿","🧜🏻","🧜🏼","🧜🏽","🧜🏾","🧜🏿","🧝🏻","🧝🏼","🧝🏽","🧝🏾","🧝🏿","💆🏻","💆🏼","💆🏽","💆🏾","💆🏿","💇🏻","💇🏼","💇🏽","💇🏾","💇🏿","🚶🏻","🚶🏼","🚶🏽","🚶🏾","🚶🏿","🧍🏻","🧍🏼","🧍🏽","🧍🏾","🧍🏿","🧎🏻","🧎🏼","🧎🏽","🧎🏾","🧎🏿","🏃🏻","🏃🏼","🏃🏽","🏃🏾","🏃🏿","💃🏻","💃🏼","💃🏽","💃🏾","💃🏿","🕺🏻","🕺🏼","🕺🏽","🕺🏾","🕺🏿","🕴🏻","🕴🏼","🕴🏽","🕴🏾","🕴🏿","🧖🏻","🧖🏼","🧖🏽","🧖🏾","🧖🏿","🧗🏻","🧗🏼","🧗🏽","🧗🏾","🧗🏿","🏇🏻","🏇🏼","🏇🏽","🏇🏾","🏇🏿","🏂🏻","🏂🏼","🏂🏽","🏂🏾","🏂🏿","🏌🏻","🏌🏼","🏌🏽","🏌🏾","🏌🏿","🏄🏻","🏄🏼","🏄🏽","🏄🏾","🏄🏿","🚣🏻","🚣🏼","🚣🏽","🚣🏾","🚣🏿","🏊🏻","🏊🏼","🏊🏽","🏊🏾","🏊🏿","⛹🏻","⛹🏼","⛹🏽","⛹🏾","⛹🏿","🏋🏻","🏋🏼","🏋🏽","🏋🏾","🏋🏿","🚴🏻","🚴🏼","🚴🏽","🚴🏾","🚴🏿","🚵🏻","🚵🏼","🚵🏽","🚵🏾","🚵🏿","🤸🏻","🤸🏼","🤸🏽","🤸🏾","🤸🏿","🤽🏻","🤽🏼","🤽🏽","🤽🏾","🤽🏿","🤾🏻","🤾🏼","🤾🏽","🤾🏾","🤾🏿","🤹🏻","🤹🏼","🤹🏽","🤹🏾","🤹🏿","🧘🏻","🧘🏼","🧘🏽","🧘🏾","🧘🏿","🛀🏻","🛀🏼","🛀🏽","🛀🏾","🛀🏿","🛌🏻","🛌🏼","🛌🏽","🛌🏾","🛌🏿","👭🏻","👭🏼","👭🏽","👭🏾","👭🏿","👫🏻","👫🏼","👫🏽","👫🏾","👫🏿","👬🏻","👬🏼","👬🏽","👬🏾","👬🏿","🏻","🏼","🏽","🏾","🏿","👁️‍🗨️","👱‍♂️","👱🏻‍♂️","👱🏼‍♂️","👱🏽‍♂️","👱🏾‍♂️","👱🏿‍♂️","👨‍🦰","👨🏻‍🦰","👨🏼‍🦰","👨🏽‍🦰","👨🏾‍🦰","👨🏿‍🦰","👨‍🦱","👨🏻‍🦱","👨🏼‍🦱","👨🏽‍🦱","👨🏾‍🦱","👨🏿‍🦱","👨‍🦳","👨🏻‍🦳","👨🏼‍🦳","👨🏽‍🦳","👨🏾‍🦳","👨🏿‍🦳","👨‍🦲","👨🏻‍🦲","👨🏼‍🦲","👨🏽‍🦲","👨🏾‍🦲","👨🏿‍🦲","👱‍♀️","👱🏻‍♀️","👱🏼‍♀️","👱🏽‍♀️","👱🏾‍♀️","👱🏿‍♀️","👩‍🦰","👩🏻‍🦰","👩🏼‍🦰","👩🏽‍🦰","👩🏾‍🦰","👩🏿‍🦰","👩‍🦱","👩🏻‍🦱","👩🏼‍🦱","👩🏽‍🦱","👩🏾‍🦱","👩🏿‍🦱","👩‍🦳","👩🏻‍🦳","👩🏼‍🦳","👩🏽‍🦳","👩🏾‍🦳","👩🏿‍🦳","👩‍🦲","👩🏻‍🦲","👩🏼‍🦲","👩🏽‍🦲","👩🏾‍🦲","👩🏿‍🦲","🙍‍♂️","🙍🏻‍♂️","🙍🏼‍♂️","🙍🏽‍♂️","🙍🏾‍♂️","🙍🏿‍♂️","🙍‍♀️","🙍🏻‍♀️","🙍🏼‍♀️","🙍🏽‍♀️","🙍🏾‍♀️","🙍🏿‍♀️","🙎‍♂️","🙎🏻‍♂️","🙎🏼‍♂️","🙎🏽‍♂️","🙎🏾‍♂️","🙎🏿‍♂️","🙎‍♀️","🙎🏻‍♀️","🙎🏼‍♀️","🙎🏽‍♀️","🙎🏾‍♀️","🙎🏿‍♀️","🙅‍♂️","🙅🏻‍♂️","🙅🏼‍♂️","🙅🏽‍♂️","🙅🏾‍♂️","🙅🏿‍♂️","🙅‍♀️","🙅🏻‍♀️","🙅🏼‍♀️","🙅🏽‍♀️","🙅🏾‍♀️","🙅🏿‍♀️","🙆‍♂️","🙆🏻‍♂️","🙆🏼‍♂️","🙆🏽‍♂️","🙆🏾‍♂️","🙆🏿‍♂️","🙆‍♀️","🙆🏻‍♀️","🙆🏼‍♀️","🙆🏽‍♀️","🙆🏾‍♀️","🙆🏿‍♀️","💁‍♂️","💁🏻‍♂️","💁🏼‍♂️","💁🏽‍♂️","💁🏾‍♂️","💁🏿‍♂️","💁‍♀️","💁🏻‍♀️","💁🏼‍♀️","💁🏽‍♀️","💁🏾‍♀️","💁🏿‍♀️","🙋‍♂️","🙋🏻‍♂️","🙋🏼‍♂️","🙋🏽‍♂️","🙋🏾‍♂️","🙋🏿‍♂️","🙋‍♀️","🙋🏻‍♀️","🙋🏼‍♀️","🙋🏽‍♀️","🙋🏾‍♀️","🙋🏿‍♀️","🧏‍♂️","🧏🏻‍♂️","🧏🏼‍♂️","🧏🏽‍♂️","🧏🏾‍♂️","🧏🏿‍♂️","🧏‍♀️","🧏🏻‍♀️","🧏🏼‍♀️","🧏🏽‍♀️","🧏🏾‍♀️","🧏🏿‍♀️","🙇‍♂️","🙇🏻‍♂️","🙇🏼‍♂️","🙇🏽‍♂️","🙇🏾‍♂️","🙇🏿‍♂️","🙇‍♀️","🙇🏻‍♀️","🙇🏼‍♀️","🙇🏽‍♀️","🙇🏾‍♀️","🙇🏿‍♀️","🤦‍♂️","🤦🏻‍♂️","🤦🏼‍♂️","🤦🏽‍♂️","🤦🏾‍♂️","🤦🏿‍♂️","🤦‍♀️","🤦🏻‍♀️","🤦🏼‍♀️","🤦🏽‍♀️","🤦🏾‍♀️","🤦🏿‍♀️","🤷‍♂️","🤷🏻‍♂️","🤷🏼‍♂️","🤷🏽‍♂️","🤷🏾‍♂️","🤷🏿‍♂️","🤷‍♀️","🤷🏻‍♀️","🤷🏼‍♀️","🤷🏽‍♀️","🤷🏾‍♀️","🤷🏿‍♀️","👨‍⚕️","👨🏻‍⚕️","👨🏼‍⚕️","👨🏽‍⚕️","👨🏾‍⚕️","👨🏿‍⚕️","👩‍⚕️","👩🏻‍⚕️","👩🏼‍⚕️","👩🏽‍⚕️","👩🏾‍⚕️","👩🏿‍⚕️","👨‍🎓","👨🏻‍🎓","👨🏼‍🎓","👨🏽‍🎓","👨🏾‍🎓","👨🏿‍🎓","👩‍🎓","👩🏻‍🎓","👩🏼‍🎓","👩🏽‍🎓","👩🏾‍🎓","👩🏿‍🎓","👨‍🏫","👨🏻‍🏫","👨🏼‍🏫","👨🏽‍🏫","👨🏾‍🏫","👨🏿‍🏫","👩‍🏫","👩🏻‍🏫","👩🏼‍🏫","👩🏽‍🏫","👩🏾‍🏫","👩🏿‍🏫","👨‍⚖️","👨🏻‍⚖️","👨🏼‍⚖️","👨🏽‍⚖️","👨🏾‍⚖️","👨🏿‍⚖️","👩‍⚖️","👩🏻‍⚖️","👩🏼‍⚖️","👩🏽‍⚖️","👩🏾‍⚖️","👩🏿‍⚖️","👨‍🌾","👨🏻‍🌾","👨🏼‍🌾","👨🏽‍🌾","👨🏾‍🌾","👨🏿‍🌾","👩‍🌾","👩🏻‍🌾","👩🏼‍🌾","👩🏽‍🌾","👩🏾‍🌾","👩🏿‍🌾","👨‍🍳","👨🏻‍🍳","👨🏼‍🍳","👨🏽‍🍳","👨🏾‍🍳","👨🏿‍🍳","👩‍🍳","👩🏻‍🍳","👩🏼‍🍳","👩🏽‍🍳","👩🏾‍🍳","👩🏿‍🍳","👨‍🔧","👨🏻‍🔧","👨🏼‍🔧","👨🏽‍🔧","👨🏾‍🔧","👨🏿‍🔧","👩‍🔧","👩🏻‍🔧","👩🏼‍🔧","👩🏽‍🔧","👩🏾‍🔧","👩🏿‍🔧","👨‍🏭","👨🏻‍🏭","👨🏼‍🏭","👨🏽‍🏭","👨🏾‍🏭","👨🏿‍🏭","👩‍🏭","👩🏻‍🏭","👩🏼‍🏭","👩🏽‍🏭","👩🏾‍🏭","👩🏿‍🏭","👨‍💼","👨🏻‍💼","👨🏼‍💼","👨🏽‍💼","👨🏾‍💼","👨🏿‍💼","👩‍💼","👩🏻‍💼","👩🏼‍💼","👩🏽‍💼","👩🏾‍💼","👩🏿‍💼","👨‍🔬","👨🏻‍🔬","👨🏼‍🔬","👨🏽‍🔬","👨🏾‍🔬","👨🏿‍🔬","👩‍🔬","👩🏻‍🔬","👩🏼‍🔬","👩🏽‍🔬","👩🏾‍🔬","👩🏿‍🔬","👨‍💻","👨🏻‍💻","👨🏼‍💻","👨🏽‍💻","👨🏾‍💻","👨🏿‍💻","👩‍💻","👩🏻‍💻","👩🏼‍💻","👩🏽‍💻","👩🏾‍💻","👩🏿‍💻","👨‍🎤","👨🏻‍🎤","👨🏼‍🎤","👨🏽‍🎤","👨🏾‍🎤","👨🏿‍🎤","👩‍🎤","👩🏻‍🎤","👩🏼‍🎤","👩🏽‍🎤","👩🏾‍🎤","👩🏿‍🎤","👨‍🎨","👨🏻‍🎨","👨🏼‍🎨","👨🏽‍🎨","👨🏾‍🎨","👨🏿‍🎨","👩‍🎨","👩🏻‍🎨","👩🏼‍🎨","👩🏽‍🎨","👩🏾‍🎨","👩🏿‍🎨","👨‍✈️","👨🏻‍✈️","👨🏼‍✈️","👨🏽‍✈️","👨🏾‍✈️","👨🏿‍✈️","👩‍✈️","👩🏻‍✈️","👩🏼‍✈️","👩🏽‍✈️","👩🏾‍✈️","👩🏿‍✈️","👨‍🚀","👨🏻‍🚀","👨🏼‍🚀","👨🏽‍🚀","👨🏾‍🚀","👨🏿‍🚀","👩‍🚀","👩🏻‍🚀","👩🏼‍🚀","👩🏽‍🚀","👩🏾‍🚀","👩🏿‍🚀","👨‍🚒","👨🏻‍🚒","👨🏼‍🚒","👨🏽‍🚒","👨🏾‍🚒","👨🏿‍🚒","👩‍🚒","👩🏻‍🚒","👩🏼‍🚒","👩🏽‍🚒","👩🏾‍🚒","👩🏿‍🚒","👮‍♂️","👮🏻‍♂️","👮🏼‍♂️","👮🏽‍♂️","👮🏾‍♂️","👮🏿‍♂️","👮‍♀️","👮🏻‍♀️","👮🏼‍♀️","👮🏽‍♀️","👮🏾‍♀️","👮🏿‍♀️","🕵️‍♂️","🕵🏻‍♂️","🕵🏼‍♂️","🕵🏽‍♂️","🕵🏾‍♂️","🕵🏿‍♂️","🕵️‍♀️","🕵🏻‍♀️","🕵🏼‍♀️","🕵🏽‍♀️","🕵🏾‍♀️","🕵🏿‍♀️","💂‍♂️","💂🏻‍♂️","💂🏼‍♂️","💂🏽‍♂️","💂🏾‍♂️","💂🏿‍♂️","💂‍♀️","💂🏻‍♀️","💂🏼‍♀️","💂🏽‍♀️","💂🏾‍♀️","💂🏿‍♀️","👷‍♂️","👷🏻‍♂️","👷🏼‍♂️","👷🏽‍♂️","👷🏾‍♂️","👷🏿‍♂️","👷‍♀️","👷🏻‍♀️","👷🏼‍♀️","👷🏽‍♀️","👷🏾‍♀️","👷🏿‍♀️","👳‍♂️","👳🏻‍♂️","👳🏼‍♂️","👳🏽‍♂️","👳🏾‍♂️","👳🏿‍♂️","👳‍♀️","👳🏻‍♀️","👳🏼‍♀️","👳🏽‍♀️","👳🏾‍♀️","👳🏿‍♀️","🦸‍♂️","🦸🏻‍♂️","🦸🏼‍♂️","🦸🏽‍♂️","🦸🏾‍♂️","🦸🏿‍♂️","🦸‍♀️","🦸🏻‍♀️","🦸🏼‍♀️","🦸🏽‍♀️","🦸🏾‍♀️","🦸🏿‍♀️","🦹‍♂️","🦹🏻‍♂️","🦹🏼‍♂️","🦹🏽‍♂️","🦹🏾‍♂️","🦹🏿‍♂️","🦹‍♀️","🦹🏻‍♀️","🦹🏼‍♀️","🦹🏽‍♀️","🦹🏾‍♀️","🦹🏿‍♀️","🧙‍♂️","🧙🏻‍♂️","🧙🏼‍♂️","🧙🏽‍♂️","🧙🏾‍♂️","🧙🏿‍♂️","🧙‍♀️","🧙🏻‍♀️","🧙🏼‍♀️","🧙🏽‍♀️","🧙🏾‍♀️","🧙🏿‍♀️","🧚‍♂️","🧚🏻‍♂️","🧚🏼‍♂️","🧚🏽‍♂️","🧚🏾‍♂️","🧚🏿‍♂️","🧚‍♀️","🧚🏻‍♀️","🧚🏼‍♀️","🧚🏽‍♀️","🧚🏾‍♀️","🧚🏿‍♀️","🧛‍♂️","🧛🏻‍♂️","🧛🏼‍♂️","🧛🏽‍♂️","🧛🏾‍♂️","🧛🏿‍♂️","🧛‍♀️","🧛🏻‍♀️","🧛🏼‍♀️","🧛🏽‍♀️","🧛🏾‍♀️","🧛🏿‍♀️","🧜‍♂️","🧜🏻‍♂️","🧜🏼‍♂️","🧜🏽‍♂️","🧜🏾‍♂️","🧜🏿‍♂️","🧜‍♀️","🧜🏻‍♀️","🧜🏼‍♀️","🧜🏽‍♀️","🧜🏾‍♀️","🧜🏿‍♀️","🧝‍♂️","🧝🏻‍♂️","🧝🏼‍♂️","🧝🏽‍♂️","🧝🏾‍♂️","🧝🏿‍♂️","🧝‍♀️","🧝🏻‍♀️","🧝🏼‍♀️","🧝🏽‍♀️","🧝🏾‍♀️","🧝🏿‍♀️","🧞‍♂️","🧞‍♀️","🧟‍♂️","🧟‍♀️","💆‍♂️","💆🏻‍♂️","💆🏼‍♂️","💆🏽‍♂️","💆🏾‍♂️","💆🏿‍♂️","💆‍♀️","💆🏻‍♀️","💆🏼‍♀️","💆🏽‍♀️","💆🏾‍♀️","💆🏿‍♀️","💇‍♂️","💇🏻‍♂️","💇🏼‍♂️","💇🏽‍♂️","💇🏾‍♂️","💇🏿‍♂️","💇‍♀️","💇🏻‍♀️","💇🏼‍♀️","💇🏽‍♀️","💇🏾‍♀️","💇🏿‍♀️","🚶‍♂️","🚶🏻‍♂️","🚶🏼‍♂️","🚶🏽‍♂️","🚶🏾‍♂️","🚶🏿‍♂️","🚶‍♀️","🚶🏻‍♀️","🚶🏼‍♀️","🚶🏽‍♀️","🚶🏾‍♀️","🚶🏿‍♀️","🧍‍♂️","🧍🏻‍♂️","🧍🏼‍♂️","🧍🏽‍♂️","🧍🏾‍♂️","🧍🏿‍♂️","🧍‍♀️","🧍🏻‍♀️","🧍🏼‍♀️","🧍🏽‍♀️","🧍🏾‍♀️","🧍🏿‍♀️","🧎‍♂️","🧎🏻‍♂️","🧎🏼‍♂️","🧎🏽‍♂️","🧎🏾‍♂️","🧎🏿‍♂️","🧎‍♀️","🧎🏻‍♀️","🧎🏼‍♀️","🧎🏽‍♀️","🧎🏾‍♀️","🧎🏿‍♀️","👨‍🦯","👨🏻‍🦯","👨🏼‍🦯","👨🏽‍🦯","👨🏾‍🦯","👨🏿‍🦯","👩‍🦯","👩🏻‍🦯","👩🏼‍🦯","👩🏽‍🦯","👩🏾‍🦯","👩🏿‍🦯","👨‍🦼","👨🏻‍🦼","👨🏼‍🦼","👨🏽‍🦼","👨🏾‍🦼","👨🏿‍🦼","👩‍🦼","👩🏻‍🦼","👩🏼‍🦼","👩🏽‍🦼","👩🏾‍🦼","👩🏿‍🦼","👨‍🦽","👨🏻‍🦽","👨🏼‍🦽","👨🏽‍🦽","👨🏾‍🦽","👨🏿‍🦽","👩‍🦽","👩🏻‍🦽","👩🏼‍🦽","👩🏽‍🦽","👩🏾‍🦽","👩🏿‍🦽","🏃‍♂️","🏃🏻‍♂️","🏃🏼‍♂️","🏃🏽‍♂️","🏃🏾‍♂️","🏃🏿‍♂️","🏃‍♀️","🏃🏻‍♀️","🏃🏼‍♀️","🏃🏽‍♀️","🏃🏾‍♀️","🏃🏿‍♀️","👯‍♂️","👯‍♀️","🧖‍♂️","🧖🏻‍♂️","🧖🏼‍♂️","🧖🏽‍♂️","🧖🏾‍♂️","🧖🏿‍♂️","🧖‍♀️","🧖🏻‍♀️","🧖🏼‍♀️","🧖🏽‍♀️","🧖🏾‍♀️","🧖🏿‍♀️","🧗‍♂️","🧗🏻‍♂️","🧗🏼‍♂️","🧗🏽‍♂️","🧗🏾‍♂️","🧗🏿‍♂️","🧗‍♀️","🧗🏻‍♀️","🧗🏼‍♀️","🧗🏽‍♀️","🧗🏾‍♀️","🧗🏿‍♀️","🏌️‍♂️","🏌🏻‍♂️","🏌🏼‍♂️","🏌🏽‍♂️","🏌🏾‍♂️","🏌🏿‍♂️","🏌️‍♀️","🏌🏻‍♀️","🏌🏼‍♀️","🏌🏽‍♀️","🏌🏾‍♀️","🏌🏿‍♀️","🏄‍♂️","🏄🏻‍♂️","🏄🏼‍♂️","🏄🏽‍♂️","🏄🏾‍♂️","🏄🏿‍♂️","🏄‍♀️","🏄🏻‍♀️","🏄🏼‍♀️","🏄🏽‍♀️","🏄🏾‍♀️","🏄🏿‍♀️","🚣‍♂️","🚣🏻‍♂️","🚣🏼‍♂️","🚣🏽‍♂️","🚣🏾‍♂️","🚣🏿‍♂️","🚣‍♀️","🚣🏻‍♀️","🚣🏼‍♀️","🚣🏽‍♀️","🚣🏾‍♀️","🚣🏿‍♀️","🏊‍♂️","🏊🏻‍♂️","🏊🏼‍♂️","🏊🏽‍♂️","🏊🏾‍♂️","🏊🏿‍♂️","🏊‍♀️","🏊🏻‍♀️","🏊🏼‍♀️","🏊🏽‍♀️","🏊🏾‍♀️","🏊🏿‍♀️","⛹️‍♂️","⛹🏻‍♂️","⛹🏼‍♂️","⛹🏽‍♂️","⛹🏾‍♂️","⛹🏿‍♂️","⛹️‍♀️","⛹🏻‍♀️","⛹🏼‍♀️","⛹🏽‍♀️","⛹🏾‍♀️","⛹🏿‍♀️","🏋️‍♂️","🏋🏻‍♂️","🏋🏼‍♂️","🏋🏽‍♂️","🏋🏾‍♂️","🏋🏿‍♂️","🏋️‍♀️","🏋🏻‍♀️","🏋🏼‍♀️","🏋🏽‍♀️","🏋🏾‍♀️","🏋🏿‍♀️","🚴‍♂️","🚴🏻‍♂️","🚴🏼‍♂️","🚴🏽‍♂️","🚴🏾‍♂️","🚴🏿‍♂️","🚴‍♀️","🚴🏻‍♀️","🚴🏼‍♀️","🚴🏽‍♀️","🚴🏾‍♀️","🚴🏿‍♀️","🚵‍♂️","🚵🏻‍♂️","🚵🏼‍♂️","🚵🏽‍♂️","🚵🏾‍♂️","🚵🏿‍♂️","🚵‍♀️","🚵🏻‍♀️","🚵🏼‍♀️","🚵🏽‍♀️","🚵🏾‍♀️","🚵🏿‍♀️","🤸‍♂️","🤸🏻‍♂️","🤸🏼‍♂️","🤸🏽‍♂️","🤸🏾‍♂️","🤸🏿‍♂️","🤸‍♀️","🤸🏻‍♀️","🤸🏼‍♀️","🤸🏽‍♀️","🤸🏾‍♀️","🤸🏿‍♀️","🤼‍♂️","🤼‍♀️","🤽‍♂️","🤽🏻‍♂️","🤽🏼‍♂️","🤽🏽‍♂️","🤽🏾‍♂️","🤽🏿‍♂️","🤽‍♀️","🤽🏻‍♀️","🤽🏼‍♀️","🤽🏽‍♀️","🤽🏾‍♀️","🤽🏿‍♀️","🤾‍♂️","🤾🏻‍♂️","🤾🏼‍♂️","🤾🏽‍♂️","🤾🏾‍♂️","🤾🏿‍♂️","🤾‍♀️","🤾🏻‍♀️","🤾🏼‍♀️","🤾🏽‍♀️","🤾🏾‍♀️","🤾🏿‍♀️","🤹‍♂️","🤹🏻‍♂️","🤹🏼‍♂️","🤹🏽‍♂️","🤹🏾‍♂️","🤹🏿‍♂️","🤹‍♀️","🤹🏻‍♀️","🤹🏼‍♀️","🤹🏽‍♀️","🤹🏾‍♀️","🤹🏿‍♀️","🧘‍♂️","🧘🏻‍♂️","🧘🏼‍♂️","🧘🏽‍♂️","🧘🏾‍♂️","🧘🏿‍♂️","🧘‍♀️","🧘🏻‍♀️","🧘🏼‍♀️","🧘🏽‍♀️","🧘🏾‍♀️","🧘🏿‍♀️","🧑‍🤝‍🧑","🧑🏻‍🤝‍🧑🏻","🧑🏼‍🤝‍🧑🏻","🧑🏼‍🤝‍🧑🏼","🧑🏽‍🤝‍🧑🏻","🧑🏽‍🤝‍🧑🏼","🧑🏽‍🤝‍🧑🏽","🧑🏾‍🤝‍🧑🏻","🧑🏾‍🤝‍🧑🏼","🧑🏾‍🤝‍🧑🏽","🧑🏾‍🤝‍🧑🏾","🧑🏿‍🤝‍🧑🏻","🧑🏿‍🤝‍🧑🏼","🧑🏿‍🤝‍🧑🏽","🧑🏿‍🤝‍🧑🏾","🧑🏿‍🤝‍🧑🏿","👩🏼‍🤝‍👩🏻","👩🏽‍🤝‍👩🏻","👩🏽‍🤝‍👩🏼","👩🏾‍🤝‍👩🏻","👩🏾‍🤝‍👩🏼","👩🏾‍🤝‍👩🏽","👩🏿‍🤝‍👩🏻","👩🏿‍🤝‍👩🏼","👩🏿‍🤝‍👩🏽","👩🏿‍🤝‍👩🏾","👩🏻‍🤝‍👨🏼","👩🏻‍🤝‍👨🏽","👩🏻‍🤝‍👨🏾","👩🏻‍🤝‍👨🏿","👩🏼‍🤝‍👨🏻","👩🏼‍🤝‍👨🏽","👩🏼‍🤝‍👨🏾","👩🏼‍🤝‍👨🏿","👩🏽‍🤝‍👨🏻","👩🏽‍🤝‍👨🏼","👩🏽‍🤝‍👨🏾","👩🏽‍🤝‍👨🏿","👩🏾‍🤝‍👨🏻","👩🏾‍🤝‍👨🏼","👩🏾‍🤝‍👨🏽","👩🏾‍🤝‍👨🏿","👩🏿‍🤝‍👨🏻","👩🏿‍🤝‍👨🏼","👩🏿‍🤝‍👨🏽","👩🏿‍🤝‍👨🏾","👨🏼‍🤝‍👨🏻","👨🏽‍🤝‍👨🏻","👨🏽‍🤝‍👨🏼","👨🏾‍🤝‍👨🏻","👨🏾‍🤝‍👨🏼","👨🏾‍🤝‍👨🏽","👨🏿‍🤝‍👨🏻","👨🏿‍🤝‍👨🏼","👨🏿‍🤝‍👨🏽","👨🏿‍🤝‍👨🏾","👩‍❤️‍💋‍👨","👨‍❤️‍💋‍👨","👩‍❤️‍💋‍👩","👩‍❤️‍👨","👨‍❤️‍👨","👩‍❤️‍👩","👨‍👩‍👦","👨‍👩‍👧","👨‍👩‍👧‍👦","👨‍👩‍👦‍👦","👨‍👩‍👧‍👧","👨‍👨‍👦","👨‍👨‍👧","👨‍👨‍👧‍👦","👨‍👨‍👦‍👦","👨‍👨‍👧‍👧","👩‍👩‍👦","👩‍👩‍👧","👩‍👩‍👧‍👦","👩‍👩‍👦‍👦","👩‍👩‍👧‍👧","👨‍👦","👨‍👦‍👦","👨‍👧","👨‍👧‍👦","👨‍👧‍👧","👩‍👦","👩‍👦‍👦","👩‍👧","👩‍👧‍👦","👩‍👧‍👧","🐕‍🦺","🏳️‍🌈","🏴‍☠️","〰︎","‼︎","⁉︎","*︎⃣","#︎⃣","〽︎","©︎","®︎","↔︎","↕︎","↖︎","↗︎","↘︎","↙︎","↩︎","↪︎","⌨︎","⏏︎","⏭︎","⏮︎","⏯︎","⏱︎","⏲︎","⏸︎","⏹︎","⏺︎","▪︎","🖐","✌","☝","✍","☹️","☠️","❣️","❤️","🕳️","🗨️","🗯️","🖐️","✌️","☝️","✍️","👁️","🕵️","🕴️","⛷️","🏌️","⛹️","🏋️","🗣️","🐿️","🕊️","🕷️","🕸️","🏵️","☘️","🌶️","🍽️","🗺️","🏔️","⛰️","🏕️","🏖️","🏜️","🏝️","🏞️","🏟️","🏛️","🏗️","🏘️","🏚️","⛩️","🏙️","♨️","🏎️","🏍️","🛣️","🛤️","🛢️","🛳️","⛴️","🛥️","✈️","🛩️","🛰️","🛎️","⏱️","⏲️","🕰️","🌡️","☀️","☁️","⛈️","🌤️","🌥️","🌦️","🌧️","🌨️","🌩️","🌪️","🌫️","🌬️","☂️","⛱️","❄️","☃️","☄️","🎗️","🎟️","🎖️","⛸️","🕹️","♠️","♥️","♦️","♣️","♟️","🖼️","🕶️","🛍️","⛑️","🎙️","🎚️","🎛️","☎️","🖥️","🖨️","⌨️","🖱️","🖲️","🎞️","📽️","🕯️","🗞️","🏷️","✉️","🗳️","✏️","✒️","🖋️","🖊️","🖌️","🖍️","🗂️","🗒️","🗓️","🖇️","✂️","🗃️","🗄️","🗑️","🗝️","⛏️","⚒️","🛠️","🗡️","⚔️","🛡️","⚙️","🗜️","⚖️","⛓️","⚗️","🛏️","🛋️","⚰️","⚱️","⚠️","☢️","☣️","⬆️","↗️","➡️","↘️","⬇️","↙️","⬅️","↖️","↕️","↔️","↩️","↪️","⤴️","⤵️","⚛️","🕉️","✡️","☸️","☯️","✝️","☦️","☪️","☮️","▶️","⏭️","⏯️","◀️","⏮️","⏸️","⏹️","⏺️","⏏️","♀️","♂️","⚕️","♾️","♻️","⚜️","☑️","✔️","✖️","〽️","✳️","✴️","❇️"]
  266. </script>
  267. <script>
  268. const DOC_VERSION = 0
  269. var note = null
  270. var TARGET = null
  271. var SELECTION = []
  272. INSERTING = false
  273. Array.prototype.distinct = function(){
  274. let res = []
  275. for (let x of this){
  276. if (!res.includes(x)) {
  277. res.push(x)}}
  278. return res}
  279. // construct a dom fragment from nested ['tag', optional {attrs}, ... children]
  280. function Render(node) {
  281. if (!node) {
  282. return
  283. } else if (typeof(node) === 'string') {
  284. return document.createTextNode(node)
  285. } else {
  286. let tag = node[0].match(/^[^\.#]+/)[0]
  287. let classes = node[0].match(/\.[^\.#]+/g) || []
  288. let ids = node[0].match(/\#[^\.#]+/g) || []
  289. let attrs = Object.prototype.toString.call(node[1]) === '[object Object]' ? node[1] : null
  290. let children = node.slice(attrs ? 2 : 1)
  291. let el = document.createElement(tag)
  292. if (attrs) {
  293. for ([k, v] of Object.entries(attrs)) {
  294. if (typeof(v) === 'function') {
  295. el[k] = v
  296. } else {
  297. el.setAttribute(k, v)
  298. }}}
  299. classes.forEach((s)=>el.classList.add(s.slice(1)))
  300. if (ids[0]) {el.setAttribute('id', (ids[0].slice(1)))}
  301. children.forEach((child)=>{
  302. let cnode = Render(child)
  303. if (cnode) {
  304. el.appendChild(cnode)}})
  305. return el}}
  306. // Storage stuff
  307. // will use IndexedDB when available, hence the asyncronicity
  308. function PutItem(k, o){
  309. localStorage.setItem(k, JSON.stringify(o))
  310. }
  311. function GetItem(k, f){
  312. f(JSON.parse(localStorage.getItem(k)))
  313. }
  314. function DeleteItem(k){
  315. localStorage.removeItem(k)
  316. }
  317. function StorageItems(f){
  318. f(Object.entries(localStorage))
  319. }
  320. // Note file stuff
  321. function SaveMeta(){
  322. localStorage.setItem('meta', JSON.stringify(meta))
  323. }
  324. function SaveNote(note) {
  325. note.edited = new Date()
  326. PutItem(note.uid, note)
  327. }
  328. function NewNote() {
  329. random_emoji = emoji[Math.floor(Math.random()*emoji.length)]
  330. res = {
  331. uid: Math.random().toString(16).slice(2),
  332. title: `${random_emoji} Untitled`,
  333. content: "<p></p>",
  334. version: DOC_VERSION,
  335. created: new Date(),
  336. edited: new Date()
  337. }
  338. SaveNote(res)
  339. return res
  340. }
  341. function OpenNote(uid) {
  342. if (uid == null) {
  343. note = null
  344. meta.current_note = null
  345. document.querySelector("document").classList.add("hidden")
  346. } else {
  347. GetItem(uid, (o)=>{
  348. note = o
  349. meta.current_note = note.uid
  350. document.querySelector("document").classList.remove("hidden")
  351. document.querySelector("#title").textContent = note.title
  352. document.querySelector("note").innerHTML = note.content
  353. HistoryRecordState()
  354. SaveMeta()
  355. })
  356. }
  357. HISTORY = []
  358. HIDX = 0
  359. }
  360. function DeleteNote(uid){
  361. if (note.uid == uid) {
  362. OpenNote(null)
  363. }
  364. DeleteItem(uid)
  365. }
  366. function SyncContent() {
  367. note.content = document.querySelector("note").innerHTML
  368. note.title = document.querySelector("#title").innerText
  369. SaveNote(note)
  370. }
  371. /* TODO
  372. [x] undo/redo traversal
  373. [x] key command capture
  374. [x] state change truncates stack
  375. [ ] stack length limit
  376. [ ] cursor preservation
  377. */
  378. var HISTORY = []
  379. var HIDX = 0
  380. function HistoryRecordState(){
  381. if (INSERTING) {return}
  382. let s = document.querySelector("note").innerHTML
  383. if (HISTORY[HIDX] != s) {
  384. HISTORY = HISTORY.slice(0, HIDX+1)
  385. HISTORY.push(s)
  386. HIDX = HISTORY.length-1}
  387. SyncContent()}
  388. function HistoryUndo(){
  389. HistoryRecordState() // due to interval recording there might be changes
  390. if (HIDX > 0) {
  391. HIDX -= 1
  392. document.querySelector("note").innerHTML = HISTORY[HIDX]}
  393. SyncContent()
  394. }
  395. function HistoryRedo(){
  396. if (HIDX < HISTORY.length-1) {
  397. HIDX += 1
  398. document.querySelector("note").innerHTML = HISTORY[HIDX]
  399. SyncContent()}}
  400. document.addEventListener("keydown", (e) => {
  401. if (e.key === "z" && e.ctrlKey === true) {
  402. e.preventDefault()
  403. HistoryUndo()}
  404. if (e.key === "y" && e.ctrlKey === true) {
  405. e.preventDefault()
  406. HistoryRedo()}})
  407. setInterval(()=>HistoryRecordState(), 1000)
  408. const block_defs = {
  409. p: {shortcode: "/p", tagName: "P", vdom: ['p']},
  410. h1: {shortcode: "/h1", tagName: "H1", vdom: ['h1']},
  411. h2: {shortcode: "/h2", tagName: "H2", vdom: ['h2']},
  412. h3: {shortcode: "/h3", tagName: "H3", vdom: ['h3']},
  413. h4: {shortcode: "/h4", tagName: "H4", vdom: ['h4']},
  414. hr: {shortcode: "/hr", tagName: "HR", vdom: ['hr']},
  415. //img: {shortcode: "/img", tagName: "IMG", vdom: ['img']},
  416. li: {shortcode: "* ", tagName: "LI", vdom: ['li']},
  417. todo: {shortcode: "[] ", tagName: "DIV", vdom: ['div.todo', {onclick: 'this.classList.toggle("checked")'}]}
  418. }
  419. const block_tags = Object.entries(block_defs).map(([k,v])=>v.tagName)
  420. function ValidBlockType(node){
  421. if (block_tags.includes(node.tagName)) {
  422. if (node.tagName === "DIV") {
  423. if (node.classList.contains("todo")) {
  424. return true
  425. }
  426. } else {
  427. return true
  428. }
  429. }
  430. }
  431. function StripBlockClasses(s){
  432. return (s || "").replaceAll(/todo/g, "")
  433. }
  434. // block manipulation
  435. function IsText(el){
  436. return el.nodeName === "#text"
  437. }
  438. function PlaceCursor(node, offset){
  439. let s = getSelection()
  440. s.removeAllRanges()
  441. let nr = new Range()
  442. nr.setStart(node, offset)
  443. s.addRange(nr)
  444. }
  445. function CurrentRange(){
  446. var s = getSelection()
  447. if (s.rangeCount > 0) {
  448. return s.getRangeAt(0)}}
  449. function ReplaceNode(el, nel){
  450. el.childNodes.forEach((e)=>nel.appendChild(e))
  451. el.replaceWith(nel)
  452. return nel
  453. }
  454. // only interested in collapsed cursors at the moment
  455. function TextBeforeCursor(){
  456. r = CurrentRange()
  457. if (r) {
  458. if (r.collapsed == true && r.startContainer.nodeName === "#text" && !r.startContainer.previousSibling) {
  459. return r.startContainer.textContent.slice(0, r.startOffset).replace(/\u00A0/g, ' ')
  460. }
  461. }
  462. }
  463. function AncestorChild(el, ancestor){
  464. while (el != ancestor && el.parentNode != ancestor) {
  465. el = el.parentNode}
  466. return el}
  467. // bubble up until a valid block is found
  468. function BubbledBlock(el){
  469. if (!document.querySelector("note").contains(el)) { return }
  470. while (!ValidBlockType(el) && el.parentNode) {
  471. el = el.parentNode}
  472. if (ValidBlockType(el)) {return el}
  473. }
  474. // this should be a list of 'shallow' siblings that have the selection intersecting them
  475. // 1. find the common ancestor
  476. // 2. for start and end find the ancestor inside the common (so 1 level deep), these will be siblings
  477. // 3. iterate via nextSibling to get the list
  478. // Should this include indent children?
  479. function SelectedElements(){
  480. res = []
  481. r = CurrentRange()
  482. if (r) {
  483. start = AncestorChild(r.startContainer, r.commonAncestorContainer)
  484. end = AncestorChild(r.endContainer, r.commonAncestorContainer)
  485. res.push(start)
  486. if (start != end) {
  487. while (start.nextElementSibling && start.nextElementSibling != end) {
  488. start = start.nextElementSibling
  489. res.push(start)
  490. }
  491. res.push(end)
  492. }
  493. }
  494. res = res.map((n)=> n.nodeName === "#text" ? n.parentNode : n)
  495. return res.distinct().filter(e=>document.querySelector("note").contains(e))
  496. }
  497. function SaveSelection(){
  498. s = getSelection()
  499. window._ranges = []
  500. for (let i = 0; i < s.rangeCount; i++) {
  501. r = s.getRangeAt(i)
  502. window._ranges.push({start: r.startContainer, end: r.endContainer, so: r.startOffset, eo: r.endOffset})
  503. }
  504. }
  505. function LoadSelection(){
  506. s = getSelection()
  507. s.removeAllRanges()
  508. for (r of window._ranges) {
  509. nr = new Range()
  510. nr.setStart(r.start, r.so)
  511. nr.setEnd(r.end, r.eo)
  512. s.addRange(nr)
  513. }
  514. }
  515. function LintNote() {
  516. console.log("===== Lint =====")
  517. el = document.querySelector("note")
  518. console.log(el.innerHTML)
  519. for (node of el.children){
  520. // TODO need to only keep special divs like div.todo, need a better check here
  521. if (!ValidBlockType(node)) {
  522. node = ReplaceNode(node, Render(['p']))
  523. }
  524. while (node.previousSibling && node.previousSibling.nodeName === "#text") {
  525. node.prepend(node.previousSibling)
  526. }
  527. }
  528. // possible to have top level text when starting from empty editable
  529. SaveSelection()
  530. for (node of el.childNodes){
  531. if (node.nodeName === "#text") {
  532. p = Render(['p'])
  533. node.replaceWith(p)
  534. p.appendChild(node)
  535. }
  536. }
  537. LoadSelection()
  538. console.log(el.innerHTML)
  539. }
  540. function OPIndent(n){
  541. for (el of SelectedElements()) {
  542. let level = 0
  543. let existing = el.className.match(/indent(\d+)/)
  544. if (existing) {
  545. el.classList.remove(existing[0])
  546. level = parseInt(existing[1])
  547. }
  548. if (n + level > 0) {
  549. el.classList.add(`indent${n+level}`)
  550. }
  551. }
  552. }
  553. function OPDelete(e){
  554. let r = CurrentRange()
  555. // figure out if I'm at the beginning of a block - I think this is broken a bit
  556. if ((IsText(r.startContainer)
  557. && ValidBlockType(r.startContainer.parentNode)
  558. && !r.startContainer.previousSibling
  559. && r.startOffset == 0)
  560. || (ValidBlockType(r.startContainer) && r.startOffset == 0)){
  561. let block = ValidBlockType(r.startContainer) ? r.startContainer : r.startContainer.parentNode
  562. // delete at start converts other block types to a paragraph
  563. console.log("block:", block)
  564. if (block.tagName != "P") {
  565. let el = ReplaceNode(block, Render(['p', {class: StripBlockClasses(block.className)}]))
  566. PlaceCursor(el, 0)
  567. e.preventDefault()
  568. }
  569. }
  570. }
  571. function RenderSidebar() {
  572. StorageItems((xs)=>{
  573. let notes = xs.map(([k,v])=>[k,JSON.parse(v)]).sort(function([_a,a],[_b,b]){
  574. return new Date(a.created || "2000-01-01") - new Date(b.created || "2000-01-01")
  575. })
  576. document.querySelector('sidebar content').replaceChildren(
  577. Render(
  578. ['div',
  579. ['button', {onclick: (e)=>{OpenNote(NewNote().uid); RenderSidebar()}}, "+ new note"],
  580. ['div.note-list'].concat(
  581. notes.map(([k,o])=>{
  582. try {
  583. if (k != 'meta') {
  584. return ['p', {onclick: (e)=>OpenNote(k)}, o.title,
  585. ['button', {onclick: (e)=>{
  586. e.stopPropagation()
  587. if (confirm(`Delete note "${o.title}"`)){
  588. DeleteNote(k)
  589. RenderSidebar()
  590. }
  591. }}, 'x']]}
  592. } catch (error) {
  593. console.error(error)}}))]))})}
  594. // track blocks under mouse and under selection. Should probably also detect 'child' indentation blocks
  595. addEventListener("mousemove", (e) => {window.X = e.clientX; window.Y = e.clientY})
  596. function WithinBounds(bounds, x, y){
  597. if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) {
  598. return false
  599. }
  600. return true
  601. }
  602. function UpdateTargetAndSelection (e) {
  603. let bounds = document.querySelector("note").getBoundingClientRect()
  604. if (!WithinBounds(bounds, e.clientX,e.clientY)) {
  605. TARGET = null;
  606. }
  607. // TODO get top level block
  608. let target = BubbledBlock(e.target)
  609. SELECTION = []
  610. let r = CurrentRange()
  611. if (r && !r.collapsed) {
  612. for (e of SelectedElements()) {
  613. SELECTION.push(e)
  614. }}
  615. if (target) {
  616. TARGET = target
  617. }
  618. let place_handle = (handle, top, left, bottom) => {
  619. handle.style.display = ""
  620. handle.style.left = `${left}px`
  621. handle.style.top = `${top}px`
  622. handle.style.width = `24px`
  623. handle.style.height = `${bottom-top}px`
  624. }
  625. let a = document.querySelectorAll("selection")[0]
  626. let b = document.querySelectorAll("selection")[1]
  627. for (const x of [a, b]) {
  628. x.style.display = "none"
  629. }
  630. if (SELECTION.length > 0) {
  631. let b1 = SELECTION[0].getBoundingClientRect()
  632. let b2 = SELECTION[SELECTION.length-1].getBoundingClientRect()
  633. let groupb = SELECTION[0].parentNode.getBoundingClientRect()
  634. place_handle(a, b1.top, groupb.left, b2.bottom)
  635. }
  636. if (TARGET && !SELECTION.includes(TARGET) && TARGET.parentNode) {
  637. let b1 = TARGET.getBoundingClientRect()
  638. let groupb = TARGET.parentNode.getBoundingClientRect()
  639. place_handle(b, b1.top, groupb.left, b1.bottom)
  640. }
  641. }
  642. addEventListener("pointermove", UpdateTargetAndSelection)
  643. addEventListener("pointerup", UpdateTargetAndSelection)
  644. // TODO add an opt for possible drop targets with inside, before, after etc. handlers
  645. function SetupDrag(el, handlers) {
  646. el.style.touchAction = "none"
  647. el.addEventListener("pointerdown", (e)=> {
  648. el.setPointerCapture(e.pointerId)
  649. handlers["start"]?.(e)
  650. })
  651. el.addEventListener("pointermove", (e)=> {
  652. handlers["move"]?.(e)
  653. })
  654. el.addEventListener("pointerup", (e)=> {
  655. el.releasePointerCapture(e.pointerId)
  656. handlers["end"]?.(e)
  657. })
  658. }
  659. // Insertion stuff
  660. function BRectDistance(rect, x, y) {
  661. var dx = Math.max(rect.left - x, 0, x - rect.right)
  662. var dy = Math.max(rect.top - y, 0, y - rect.bottom)
  663. return Math.sqrt(dx*dx + dy*dy)
  664. }
  665. function ClosestChild(el, x, y) {
  666. let nodes = []
  667. for (e of el.childNodes) {
  668. if (!SELECTION.includes(e) && !(TARGET == e)) {
  669. nodes.push(e)
  670. }
  671. }
  672. nodes.sort((a, b)=> BRectDistance(a.getBoundingClientRect(), x, y) - BRectDistance(b.getBoundingClientRect(), x, y))
  673. return nodes[0]
  674. }
  675. document.querySelectorAll("selection").forEach((sel)=> {
  676. SetupDrag(sel, {
  677. 'start': e => {
  678. // using queryselector to order target in selection
  679. let marked = [... SELECTION, TARGET]
  680. marked.forEach((e)=> e?.classList.add("_dragging"))
  681. INSERTING = [... document.querySelectorAll("._dragging")]
  682. console.log("DRAG START", TARGET)
  683. },
  684. 'move': e => {
  685. if (INSERTING) {
  686. document.querySelectorAll(".insertion").forEach((el)=>{
  687. el.classList.remove("insertion")
  688. el.classList.remove("ibefore")
  689. el.classList.remove("iafter")
  690. })
  691. // if not over note panel we want no insertion point
  692. if (!WithinBounds(document.querySelector("note").getBoundingClientRect(), e.clientX,e.clientY)) {
  693. return
  694. }
  695. let closest = ClosestChild(document.querySelector("note"), e.clientX, e.clientY)
  696. if (closest) {
  697. closest.classList.add("insertion")
  698. let bounds = closest.getBoundingClientRect()
  699. if (e.clientY < bounds.top+4) {
  700. closest.classList.add("ibefore")
  701. } else if (e.clientY > bounds.bottom-4) {
  702. closest.classList.add("iafter")
  703. }
  704. }
  705. }
  706. },
  707. 'end': e => {
  708. console.log("DRAG END")
  709. INSERTING.forEach((e)=> e.classList.remove("_dragging"))
  710. let point = document.querySelector(".insertion")
  711. if (INSERTING && point && !INSERTING.includes(point)) {
  712. if (point.classList.contains("ibefore")) {
  713. INSERTING.forEach(el => {
  714. el.parentNode.removeChild(el)
  715. point.before(el)
  716. })
  717. } else {
  718. INSERTING.reverse()
  719. INSERTING.forEach(el => {
  720. el.parentNode.removeChild(el)
  721. point.after(el)
  722. })
  723. }
  724. }
  725. document.querySelectorAll(".insertion").forEach((el)=>el.classList.remove("insertion"))
  726. //document.querySelectorAll("selection").forEach((el)=>el.style.display = "none")
  727. INSERTING = false
  728. }
  729. })
  730. })
  731. document.querySelector("note").ondrop = e => {
  732. e.preventDefault()
  733. }
  734. document.querySelector("note").addEventListener("input", ()=>{LintNote(); SyncContent()}, false)
  735. document.querySelector("#title").addEventListener("input", ()=>{SyncContent(); RenderSidebar()}, false)
  736. document.querySelector("note").addEventListener("keydown", (e) => {
  737. console.log(e)
  738. let r = CurrentRange()
  739. if (r.collapsed && r.startContainer === e.target) {
  740. console.log("out of a block!!!")
  741. let block = e.target.childNodes[r.startOffset] || e.target.childNodes[0]
  742. if (block) {
  743. PlaceCursor(block, block.childNodes.length)
  744. }
  745. }
  746. if (e.key === "Tab") {
  747. e.preventDefault()
  748. OPIndent(e.shiftKey ? -1 : 1)
  749. }
  750. if (e.key === "Backspace") {
  751. OPDelete(e)
  752. }
  753. SyncContent()
  754. })
  755. document.querySelector("note").addEventListener("keyup", (e) => {
  756. console.log("keyup")
  757. if (true) { //(e.code === "Space") {
  758. let before = TextBeforeCursor()
  759. let text = getSelection().getRangeAt(0).startContainer
  760. for (const [k,v] of Object.entries(block_defs)) {
  761. if (before === v.shortcode) {
  762. let block = text.parentNode
  763. text.textContent = text.textContent.slice(v.shortcode.length)
  764. let el = ReplaceNode(block, Render(v.vdom))
  765. block.setAttribute("class", StripBlockClasses(block.getAttribute("class")))
  766. block.classList.forEach((s)=>el.classList.add(s))
  767. PlaceCursor(el, 0)
  768. }
  769. }
  770. }
  771. SyncContent()
  772. })
  773. document.querySelector("note").addEventListener("drop", (e)=>{
  774. [... e.dataTransfer.files].forEach((file, i) => {
  775. if(file.type.startsWith("image")){
  776. let reader = new FileReader()
  777. reader.onload = function (event) {
  778. document.querySelector("note").appendChild(Render(['p', ['img', {"src": event.target.result}]]))
  779. }
  780. reader.readAsDataURL(file)
  781. e.preventDefault()
  782. }
  783. })
  784. }, false)
  785. var meta = JSON.parse(localStorage.getItem('meta')) || {current_note: null}
  786. OpenNote(meta.current_note || NewNote().uid)
  787. RenderSidebar()
  788. </script>
  789. </body>
  790. </html>