123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- <!DOCTYPE html>
- <!--
- TODO
- [x] document json with metadata, title and UID key name
- [x] app metadata, last open file
- [x] start off with initial document
- [x] better system for 'new note' and 'open note'
- [ ] assertions about document health
- [x] notes have created and edited date
- [x] can delete notes
- [ ] can duplicate notes
- [ ] better in-memory storage of notes (don't have to parse every time i render sidebar or do a search)
- [ ] use indexedDB when available
- [ ] common interface for both
- [ ] mobile styling, probably with expandable side bar
- [x] mini Vdom
- [x] class and id parsing from tag strings like 'div.foo#bar'
- [x] draw sidebar
- [x] click file to open
- [x] new file button
- [x] document title
- [ ] contentEditable changes sanitize dom
- [ ] greenlist of top level elements, strip everything else
- [ ] selection styling (bold, italic). I actually think there's contenteditable commands to apply these things?
-
- [ ] drag and drop restructuring
- basically want to say SetUpDrag(el, {start: f, drag: f, end: f}), drop target and insertion could be
- genericized as well
- [ ] generic system, will use in sidebar and document
- [ ] configure hotspots for drop before, inside, after, left, right
- [ ] protocol style actions for insertion
- [ ] insert context menu
- [ ] slash, todo, and bullet list inference at start of top level element
- [ ] key commands
- [ ] linking
- [ ] 'note' links, show page title, have uid
- [ ] content editing strategy for links, can't edit the text, can delete (maybe if inside a selection)
- [ ] emoji picker
- [ ] 'file structure' graph in meta
- [ ] notes can have 'child' notes
- [ ] simple search
- [ ] export all data
- [ ] storage space estimator
- [ ] handle running out of space (alert user, ???)
- [ ] can navigate between title and content with cursor
- [ ] history should preserve per document during a session
- BUGS
- [ ] new block in h1 doesn't preserve indent. Might need to lint new blocks as todo needs it's checked cleared too
- [ ] pasting sticks content inside of a block, not even sure of the desired behaviour here, maybe
- contents should be 'children' only in so far as having the parent's indentation
- [x] <img> are really hard to edit around, should these be wrapped in a div or something?
- [ ] 'li' has different positioning in chrome vs firefox
- [ ] pasting blocks picks up style as inline, which breaks things
- [x] selection/insertion is triggering history states
- [ ] todo blocks need mouse events for more than just the checkbox (should check bounds on the rect for toggling it)
- [x] chrome - when trying to drag a block that is partially range selected the selection collapses and it fails
- [ ] the mouse target can stick around, it should use that nearest neighbor function
- [x] emoji encoding isn't surviving file transfers
- [ ] deleting the last note can get app into bad state where meta "current_note" points to non existant note
- Today's project
- [ ] I'm in need of a debug section that shows the current range with live update
- [ ] indexedDB & localStorage interface
- [x] switch localstorage calls to callbacks
- [x] drop images in note
- [ ] drag and drop blocks
- [x] draw CSS drag handle on every element
- [x] detect drag is over handle
- [x] handle shows up on hover
- [x] and selection
- [x] insertion tracking shows up only during handle drag
- [x] store 'selected' elements during drag
- [x] drag img has all selected elements
- [x] dragged elements have low opacity
- [x] drag can be cancelled, either dropping outside of note or on top of one of the dragged elements
- [/] insertion has 'in', 'before', 'after', 'left', 'right' detection
- [x] drop inserts selected elements
- [x] collapsed cursor selection shouldn't trigger 'selected'?
- [x] support touch events
- [ ] mobile needs to update block selection when range changes
- [x] change 'target' and 'selection' from classes to a list of elements - this should some bugs with platform dragging and
- avoids issues with history saving changes to ephemeral classes
- [x] the handles should be their own nodes outside of the note that are positioned
- [x] use nth child to do drag styling on selection? or disable history saving during drag?
- [x] reimplement the ondrag handler
- [x] note should still have it's drag/drop prevented
- [x] single mouse target should get a handle too
- [x] SELECTION needs to include TARGET and be ordered correctly
-
- -->
- <head>
- <meta charset="utf-8">
- <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>">
-
- <style>
- body {
- margin: 0;
- font-family: 'Courier New', Courier, monospace;
- }
- sidebar, document, note {
- box-sizing: border-box;
- }
- .hidden {
- visibility: hidden;
- pointer-events: none;
- }
- sidebar {
- position: absolute;
- top: 0px;
- bottom: 0px;
- width: 200px;
- border-right: 1px solid black;
- padding: 1em;
- }
- sidebar div.note-list {
- font-size: 0.8em;
- padding: 12px 0px;
- cursor: pointer;
- }
-
- sidebar p {
- margin-top: 4px;
- margin: 0em;
- }
- sidebar p:hover {
- text-decoration: underline;
- }
- sidebar p button {
- cursor: pointer;
- margin-left: 1em;
- padding: 0px 4px;
- font-size: 9px;
- }
- /* * {outline: 1px solid rgba(255,0,0,0.1);} */
- document {
- position: absolute;
- overflow: auto;
- left: 200px;
- top: 0px;
- bottom: 0px;
- right: 0px;
- padding: 1em;
- }
- note, #title {
- outline: none
- }
- #title {
- font-size: 4em;
- padding: 0px 24px;
- }
- note {
- width: 100%;
- min-height: calc(100vh - 200px);
- position: relative;
- display: block;
- padding: 2em;
- }
- h1,h2,h3,h4,p,li {
- min-height: 1em;
- }
- li {
- left: 14px;
- position: relative;
- }
- .todo {
- display: block;
- margin: 0.2em 0;
- line-height: 1em;
- text-indent: -1.8em;
- padding-left: 1.8em;
- pointer-events: none;
- }
- .todo::before {
- content: '';
- display: inline-block;
- width: 1.2em;
- height: 1.2em;
- margin-right: 0.5em;
- position: relative;
- top: 0.25em;
- box-sizing: border-box;
- border-radius: 2px;
- border: 3px solid #3a8dec;
- cursor: pointer;
- pointer-events: auto;
- }
- .todo.checked::before {
- background: #3a8dec;
- }
- /* s = "";for (i = 1; i <= 20; i++) {s += `.indent${i} {margin-left: ${i*24}px;}\n`}; console.log(s) */
- .indent1 {margin-left: 24px;}
- .indent2 {margin-left: 48px;}
- .indent3 {margin-left: 72px;}
- .indent4 {margin-left: 96px;}
- .indent5 {margin-left: 120px;}
- .indent6 {margin-left: 144px;}
- .indent7 {margin-left: 168px;}
- .indent8 {margin-left: 192px;}
- .indent9 {margin-left: 216px;}
- .indent10 {margin-left: 240px;}
- .indent11 {margin-left: 264px;}
- .indent12 {margin-left: 288px;}
- .indent13 {margin-left: 312px;}
- .indent14 {margin-left: 336px;}
- .indent15 {margin-left: 360px;}
- .indent16 {margin-left: 384px;}
- .indent17 {margin-left: 408px;}
- .indent18 {margin-left: 432px;}
- .indent19 {margin-left: 456px;}
- .indent20 {margin-left: 480px;}
- /* note * {
- outline: 1px solid rgba(255, 0, 0, 0.199);
- } */
- .insertion::after {
- content: '';
- display: block;
- position: absolute;
- width: 100%;
- height: 0px;
- border: 0px solid #3a8dec;
- border-top-width: 4px;
- }
- .insertion.ibefore::after {
- top: -10px;
- }
- .insertion::after, .insertion.iafter::after {
- bottom: -10px;
- }
- note > * {
- position: relative;
- }
- selection {
- position: absolute;
- display: block;
- cursor: grab;
- pointer-events: auto;
- 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);
-
- }
- ._dragging {
- opacity: 0.3;
- }
- /* note *::before, note *::after {
- content: "<?>";
- display: inline-block;
- font-family: courier;
- font-size: 9px;
- color: purple;
- }
- note p::before {content: "<p>"}
- note p::after {content: "</p>"}
- note div::before {content: "<div>"}
- note div::after {content: "</div>"} */
- * { outline: 1px solid rgba(256,0,0,0.2) !important;}
- </style>
- </head>
- <body>
- <sidebar>
- <h2>localnotes</h2>
- <content></content>
- </sidebar>
- <document>
- <h1 id="title" contenteditable="true">title</h1>
- <note contenteditable="true" ></note>
- </document>
- <selection></selection>
- <selection></selection>
- <!-- https://unicode.org/emoji/charts-12.0/emoji-style.html
- res = []; document.querySelectorAll("a.plain").forEach((el)=>res.push(el.textContent)) -->
- <script>
- emoji = ["☹️","☠️","❣️","❤️","🕳️","🗨️","🗯️","🖐️","✌️","☝️","✍️","👁️","🕵️","🕴️","⛷️","🏌️","⛹️","🏋️","🗣️","🐿️","🕊️","🕷️","🕸️","🏵️","☘️","🌶️","🍽️","🗺️","🏔️","⛰️","🏕️","🏖️","🏜️","🏝️","🏞️","🏟️","🏛️","🏗️","🏘️","🏚️","⛩️","🏙️","♨️","🏎️","🏍️","🛣️","🛤️","🛢️","🛳️","⛴️","🛥️","✈️","🛩️","🛰️","🛎️","⏱️","⏲️","🕰️","🌡️","☀️","☁️","⛈️","🌤️","🌥️","🌦️","🌧️","🌨️","🌩️","🌪️","🌫️","🌬️","☂️","⛱️","❄️","☃️","☄️","🎗️","🎟️","🎖️","⛸️","🕹️","♠️","♥️","♦️","♣️","♟️","🖼️","🕶️","🛍️","⛑️","🎙️","🎚️","🎛️","☎️","🖥️","🖨️","⌨️","🖱️","🖲️","🎞️","📽️","🕯️","🗞️","🏷️","✉️","🗳️","✏️","✒️","🖋️","🖊️","🖌️","🖍️","🗂️","🗒️","🗓️","🖇️","✂️","🗃️","🗄️","🗑️","🗝️","⛏️","⚒️","🛠️","🗡️","⚔️","🛡️","⚙️","🗜️","⚖️","⛓️","⚗️","🛏️","🛋️","⚰️","⚱️","⚠️","☢️","☣️","⬆️","↗️","➡️","↘️","⬇️","↙️","⬅️","↖️","↕️","↔️","↩️","↪️","⤴️","⤵️","⚛️","🕉️","✡️","☸️","☯️","✝️","☦️","☪️","☮️","▶️","⏭️","⏯️","◀️","⏮️","⏸️","⏹️","⏺️","⏏️","♀️","♂️","⚕️","♾️","♻️","⚜️","☑️","✔️","✖️","〽️","✳️","✴️","❇️","‼️","⁉️","〰️","©️","®️","™️","#️⃣","*️⃣","0️⃣","1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🅰️","🅱️","ℹ️","Ⓜ️","🅾️","🅿️","🈂️","🈷️","㊗️","㊙️","◼️","◻️","▪️","▫️","🏳️","😀","😃","😄","😁","😆","😅","🤣","😂","🙂","🙃","😉","😊","😇","🥰","😍","🤩","😘","😗","😚","😙","😋","😛","😜","🤪","😝","🤑","🤗","🤭","🤫","🤔","🤐","🤨","😐","😑","😶","😏","😒","🙄","😬","🤥","😌","😔","😪","🤤","😴","😷","🤒","🤕","🤢","🤮","🤧","🥵","🥶","🥴","😵","🤯","🤠","🥳","😎","🤓","🧐","😕","😟","🙁","😮","😯","😲","😳","🥺","😦","😧","😨","😰","😥","😢","😭","😱","😖","😣","😞","😓","😩","😫","🥱","😤","😡","😠","🤬","😈","👿","💀","💩","🤡","👹","👺","👻","👽","👾","🤖","😺","😸","😹","😻","😼","😽","🙀","😿","😾","🙈","🙉","🙊","💋","💌","💘","💝","💖","💗","💓","💞","💕","💟","💔","🧡","💛","💚","💙","💜","🤎","🖤","🤍","💯","💢","💥","💫","💦","💨","💣","💬","💭","💤","👋","🤚","✋","🖖","👌","🤏","🤞","🤟","🤘","🤙","👈","👉","👆","🖕","👇","👍","👎","✊","👊","🤛","🤜","👏","🙌","👐","🤲","🤝","🙏","💅","🤳","💪","🦾","🦿","🦵","🦶","👂","🦻","👃","🧠","🦷","🦴","👀","👅","👄","👶","🧒","👦","👧","🧑","👱","👨","🧔","👩","🧓","👴","👵","🙍","🙎","🙅","🙆","💁","🙋","🧏","🙇","🤦","🤷","👮","💂","👷","🤴","👸","👳","👲","🧕","🤵","👰","🤰","🤱","👼","🎅","🤶","🦸","🦹","🧙","🧚","🧛","🧜","🧝","🧞","🧟","💆","💇","🚶","🧍","🧎","🏃","💃","🕺","👯","🧖","🧗","🤺","🏇","🏂","🏄","🚣","🏊","🚴","🚵","🤸","🤼","🤽","🤾","🤹","🧘","🛀","🛌","👭","👫","👬","💏","💑","👪","👤","👥","👣","🦰","🦱","🦳","🦲","🐵","🐒","🦍","🦧","🐶","🐕","🦮","🐩","🐺","🦊","🦝","🐱","🐈","🦁","🐯","🐅","🐆","🐴","🐎","🦄","🦓","🦌","🐮","🐂","🐃","🐄","🐷","🐖","🐗","🐽","🐏","🐑","🐐","🐪","🐫","🦙","🦒","🐘","🦏","🦛","🐭","🐁","🐀","🐹","🐰","🐇","🦔","🦇","🐻","🐨","🐼","🦥","🦦","🦨","🦘","🦡","🐾","🦃","🐔","🐓","🐣","🐤","🐥","🐦","🐧","🦅","🦆","🦢","🦉","🦩","🦚","🦜","🐸","🐊","🐢","🦎","🐍","🐲","🐉","🦕","🦖","🐳","🐋","🐬","🐟","🐠","🐡","🦈","🐙","🐚","🐌","🦋","🐛","🐜","🐝","🐞","🦗","🦂","🦟","🦠","💐","🌸","💮","🌹","🥀","🌺","🌻","🌼","🌷","🌱","🌲","🌳","🌴","🌵","🌾","🌿","🍀","🍁","🍂","🍃","🍇","🍈","🍉","🍊","🍋","🍌","🍍","🥭","🍎","🍏","🍐","🍑","🍒","🍓","🥝","🍅","🥥","🥑","🍆","🥔","🥕","🌽","🥒","🥬","🥦","🧄","🧅","🍄","🥜","🌰","🍞","🥐","🥖","🥨","🥯","🥞","🧇","🧀","🍖","🍗","🥩","🥓","🍔","🍟","🍕","🌭","🥪","🌮","🌯","🥙","🧆","🥚","🍳","🥘","🍲","🥣","🥗","🍿","🧈","🧂","🥫","🍱","🍘","🍙","🍚","🍛","🍜","🍝","🍠","🍢","🍣","🍤","🍥","🥮","🍡","🥟","🥠","🥡","🦀","🦞","🦐","🦑","🦪","🍦","🍧","🍨","🍩","🍪","🎂","🍰","🧁","🥧","🍫","🍬","🍭","🍮","🍯","🍼","🥛","☕","🍵","🍶","🍾","🍷","🍸","🍹","🍺","🍻","🥂","🥃","🥤","🧃","🧉","🧊","🥢","🍴","🥄","🔪","🏺","🌍","🌎","🌏","🌐","🗾","🧭","🌋","🗻","🧱","🏠","🏡","🏢","🏣","🏤","🏥","🏦","🏨","🏩","🏪","🏫","🏬","🏭","🏯","🏰","💒","🗼","🗽","⛪","🕌","🛕","🕍","🕋","⛲","⛺","🌁","🌃","🌄","🌅","🌆","🌇","🌉","🎠","🎡","🎢","💈","🎪","🚂","🚃","🚄","🚅","🚆","🚇","🚈","🚉","🚊","🚝","🚞","🚋","🚌","🚍","🚎","🚐","🚑","🚒","🚓","🚔","🚕","🚖","🚗","🚘","🚙","🚚","🚛","🚜","🛵","🦽","🦼","🛺","🚲","🛴","🛹","🚏","⛽","🚨","🚥","🚦","🛑","🚧","⚓","⛵","🛶","🚤","🚢","🛫","🛬","🪂","💺","🚁","🚟","🚠","🚡","🚀","🛸","🧳","⌛","⏳","⌚","⏰","🕛","🕧","🕐","🕜","🕑","🕝","🕒","🕞","🕓","🕟","🕔","🕠","🕕","🕡","🕖","🕢","🕗","🕣","🕘","🕤","🕙","🕥","🕚","🕦","🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘","🌙","🌚","🌛","🌜","🌝","🌞","🪐","⭐","🌟","🌠","🌌","⛅","🌀","🌈","🌂","☔","⚡","⛄","🔥","💧","🌊","🎃","🎄","🎆","🎇","🧨","✨","🎈","🎉","🎊","🎋","🎍","🎎","🎏","🎐","🎑","🧧","🎀","🎁","🎫","🏆","🏅","🥇","🥈","🥉","⚽","⚾","🥎","🏀","🏐","🏈","🏉","🎾","🥏","🎳","🏏","🏑","🏒","🥍","🏓","🏸","🥊","🥋","🥅","⛳","🎣","🤿","🎽","🎿","🛷","🥌","🎯","🪀","🪁","🎱","🔮","🧿","🎮","🎰","🎲","🧩","🧸","🃏","🀄","🎴","🎭","🎨","🧵","🧶","👓","🥽","🥼","🦺","👔","👕","👖","🧣","🧤","🧥","🧦","👗","👘","🥻","🩱","🩲","🩳","👙","👚","👛","👜","👝","🎒","👞","👟","🥾","🥿","👠","👡","🩰","👢","👑","👒","🎩","🎓","🧢","📿","💄","💍","💎","🔇","🔈","🔉","🔊","📢","📣","📯","🔔","🔕","🎼","🎵","🎶","🎤","🎧","📻","🎷","🎸","🎹","🎺","🎻","🪕","🥁","📱","📲","📞","📟","📠","🔋","🔌","💻","💽","💾","💿","📀","🧮","🎥","🎬","📺","📷","📸","📹","📼","🔍","🔎","💡","🔦","🏮","🪔","📔","📕","📖","📗","📘","📙","📚","📓","📒","📃","📜","📄","📰","📑","🔖","💰","💴","💵","💶","💷","💸","💳","🧾","💹","💱","💲","📧","📨","📩","📤","📥","📦","📫","📪","📬","📭","📮","📝","💼","📁","📂","📅","📆","📇","📈","📉","📊","📋","📌","📍","📎","📏","📐","🔒","🔓","🔏","🔐","🔑","🔨","🪓","🔫","🏹","🔧","🔩","🦯","🔗","🧰","🧲","🧪","🧫","🧬","🔬","🔭","📡","💉","🩸","💊","🩹","🩺","🚪","🪑","🚽","🚿","🛁","🪒","🧴","🧷","🧹","🧺","🧻","🧼","🧽","🧯","🛒","🚬","🗿","🏧","🚮","🚰","♿","🚹","🚺","🚻","🚼","🚾","🛂","🛃","🛄","🛅","🚸","⛔","🚫","🚳","🚭","🚯","🚱","🚷","📵","🔞","🔃","🔄","🔙","🔚","🔛","🔜","🔝","🛐","🕎","🔯","♈","♉","♊","♋","♌","♍","♎","♏","♐","♑","♒","♓","⛎","🔀","🔁","🔂","⏩","⏪","🔼","⏫","🔽","⏬","🎦","🔅","🔆","📶","📳","📴","🔱","📛","🔰","⭕","✅","❌","❎","➕","➖","➗","➰","➿","❓","❔","❕","❗","🔟","🔠","🔡","🔢","🔣","🔤","🆎","🆑","🆒","🆓","🆔","🆕","🆖","🆗","🆘","🆙","🆚","🈁","🈶","🈯","🉐","🈹","🈚","🈲","🉑","🈸","🈴","🈳","🈺","🈵","🔴","🟠","🟡","🟢","🔵","🟣","🟤","⚫","⚪","🟥","🟧","🟨","🟩","🟦","🟪","🟫","⬛","⬜","◾","◽","🔶","🔷","🔸","🔹","🔺","🔻","💠","🔘","🔳","🔲","🏁","🚩","🎌","🏴","🇦🇨","🇦🇩","🇦🇪","🇦🇫","🇦🇬","🇦🇮","🇦🇱","🇦🇲","🇦🇴","🇦🇶","🇦🇷","🇦🇸","🇦🇹","🇦🇺","🇦🇼","🇦🇽","🇦🇿","🇧🇦","🇧🇧","🇧🇩","🇧🇪","🇧🇫","🇧🇬","🇧🇭","🇧🇮","🇧🇯","🇧🇱","🇧🇲","🇧🇳","🇧🇴","🇧🇶","🇧🇷","🇧🇸","🇧🇹","🇧🇻","🇧🇼","🇧🇾","🇧🇿","🇨🇦","🇨🇨","🇨🇩","🇨🇫","🇨🇬","🇨🇭","🇨🇮","🇨🇰","🇨🇱","🇨🇲","🇨🇳","🇨🇴","🇨🇵","🇨🇷","🇨🇺","🇨🇻","🇨🇼","🇨🇽","🇨🇾","🇨🇿","🇩🇪","🇩🇬","🇩🇯","🇩🇰","🇩🇲","🇩🇴","🇩🇿","🇪🇦","🇪🇨","🇪🇪","🇪🇬","🇪🇭","🇪🇷","🇪🇸","🇪🇹","🇪🇺","🇫🇮","🇫🇯","🇫🇰","🇫🇲","🇫🇴","🇫🇷","🇬🇦","🇬🇧","🇬🇩","🇬🇪","🇬🇫","🇬🇬","🇬🇭","🇬🇮","🇬🇱","🇬🇲","🇬🇳","🇬🇵","🇬🇶","🇬🇷","🇬🇸","🇬🇹","🇬🇺","🇬🇼","🇬🇾","🇭🇰","🇭🇲","🇭🇳","🇭🇷","🇭🇹","🇭🇺","🇮🇨","🇮🇩","🇮🇪","🇮🇱","🇮🇲","🇮🇳","🇮🇴","🇮🇶","🇮🇷","🇮🇸","🇮🇹","🇯🇪","🇯🇲","🇯🇴","🇯🇵","🇰🇪","🇰🇬","🇰🇭","🇰🇮","🇰🇲","🇰🇳","🇰🇵","🇰🇷","🇰🇼","🇰🇾","🇰🇿","🇱🇦","🇱🇧","🇱🇨","🇱🇮","🇱🇰","🇱🇷","🇱🇸","🇱🇹","🇱🇺","🇱🇻","🇱🇾","🇲🇦","🇲🇨","🇲🇩","🇲🇪","🇲🇫","🇲🇬","🇲🇭","🇲🇰","🇲🇱","🇲🇲","🇲🇳","🇲🇴","🇲🇵","🇲🇶","🇲🇷","🇲🇸","🇲🇹","🇲🇺","🇲🇻","🇲🇼","🇲🇽","🇲🇾","🇲🇿","🇳🇦","🇳🇨","🇳🇪","🇳🇫","🇳🇬","🇳🇮","🇳🇱","🇳🇴","🇳🇵","🇳🇷","🇳🇺","🇳🇿","🇴🇲","🇵🇦","🇵🇪","🇵🇫","🇵🇬","🇵🇭","🇵🇰","🇵🇱","🇵🇲","🇵🇳","🇵🇷","🇵🇸","🇵🇹","🇵🇼","🇵🇾","🇶🇦","🇷🇪","🇷🇴","🇷🇸","🇷🇺","🇷🇼","🇸🇦","🇸🇧","🇸🇨","🇸🇩","🇸🇪","🇸🇬","🇸🇭","🇸🇮","🇸🇯","🇸🇰","🇸🇱","🇸🇲","🇸🇳","🇸🇴","🇸🇷","🇸🇸","🇸🇹","🇸🇻","🇸🇽","🇸🇾","🇸🇿","🇹🇦","🇹🇨","🇹🇩","🇹🇫","🇹🇬","🇹🇭","🇹🇯","🇹🇰","🇹🇱","🇹🇲","🇹🇳","🇹🇴","🇹🇷","🇹🇹","🇹🇻","🇹🇼","🇹🇿","🇺🇦","🇺🇬","🇺🇲","🇺🇳","🇺🇸","🇺🇾","🇺🇿","🇻🇦","🇻🇨","🇻🇪","🇻🇬","🇻🇮","🇻🇳","🇻🇺","🇼🇫","🇼🇸","🇽🇰","🇾🇪","🇾🇹","🇿🇦","🇿🇲","🇿🇼","🏴","🏴","🏴","👋🏻","👋🏼","👋🏽","👋🏾","👋🏿","🤚🏻","🤚🏼","🤚🏽","🤚🏾","🤚🏿","🖐🏻","🖐🏼","🖐🏽","🖐🏾","🖐🏿","✋🏻","✋🏼","✋🏽","✋🏾","✋🏿","🖖🏻","🖖🏼","🖖🏽","🖖🏾","🖖🏿","👌🏻","👌🏼","👌🏽","👌🏾","👌🏿","🤏🏻","🤏🏼","🤏🏽","🤏🏾","🤏🏿","✌🏻","✌🏼","✌🏽","✌🏾","✌🏿","🤞🏻","🤞🏼","🤞🏽","🤞🏾","🤞🏿","🤟🏻","🤟🏼","🤟🏽","🤟🏾","🤟🏿","🤘🏻","🤘🏼","🤘🏽","🤘🏾","🤘🏿","🤙🏻","🤙🏼","🤙🏽","🤙🏾","🤙🏿","👈🏻","👈🏼","👈🏽","👈🏾","👈🏿","👉🏻","👉🏼","👉🏽","👉🏾","👉🏿","👆🏻","👆🏼","👆🏽","👆🏾","👆🏿","🖕🏻","🖕🏼","🖕🏽","🖕🏾","🖕🏿","👇🏻","👇🏼","👇🏽","👇🏾","👇🏿","☝🏻","☝🏼","☝🏽","☝🏾","☝🏿","👍🏻","👍🏼","👍🏽","👍🏾","👍🏿","👎🏻","👎🏼","👎🏽","👎🏾","👎🏿","✊🏻","✊🏼","✊🏽","✊🏾","✊🏿","👊🏻","👊🏼","👊🏽","👊🏾","👊🏿","🤛🏻","🤛🏼","🤛🏽","🤛🏾","🤛🏿","🤜🏻","🤜🏼","🤜🏽","🤜🏾","🤜🏿","👏🏻","👏🏼","👏🏽","👏🏾","👏🏿","🙌🏻","🙌🏼","🙌🏽","🙌🏾","🙌🏿","👐🏻","👐🏼","👐🏽","👐🏾","👐🏿","🤲🏻","🤲🏼","🤲🏽","🤲🏾","🤲🏿","🙏🏻","🙏🏼","🙏🏽","🙏🏾","🙏🏿","✍🏻","✍🏼","✍🏽","✍🏾","✍🏿","💅🏻","💅🏼","💅🏽","💅🏾","💅🏿","🤳🏻","🤳🏼","🤳🏽","🤳🏾","🤳🏿","💪🏻","💪🏼","💪🏽","💪🏾","💪🏿","🦵🏻","🦵🏼","🦵🏽","🦵🏾","🦵🏿","🦶🏻","🦶🏼","🦶🏽","🦶🏾","🦶🏿","👂🏻","👂🏼","👂🏽","👂🏾","👂🏿","🦻🏻","🦻🏼","🦻🏽","🦻🏾","🦻🏿","👃🏻","👃🏼","👃🏽","👃🏾","👃🏿","👶🏻","👶🏼","👶🏽","👶🏾","👶🏿","🧒🏻","🧒🏼","🧒🏽","🧒🏾","🧒🏿","👦🏻","👦🏼","👦🏽","👦🏾","👦🏿","👧🏻","👧🏼","👧🏽","👧🏾","👧🏿","🧑🏻","🧑🏼","🧑🏽","🧑🏾","🧑🏿","👱🏻","👱🏼","👱🏽","👱🏾","👱🏿","👨🏻","👨🏼","👨🏽","👨🏾","👨🏿","🧔🏻","🧔🏼","🧔🏽","🧔🏾","🧔🏿","👩🏻","👩🏼","👩🏽","👩🏾","👩🏿","🧓🏻","🧓🏼","🧓🏽","🧓🏾","🧓🏿","👴🏻","👴🏼","👴🏽","👴🏾","👴🏿","👵🏻","👵🏼","👵🏽","👵🏾","👵🏿","🙍🏻","🙍🏼","🙍🏽","🙍🏾","🙍🏿","🙎🏻","🙎🏼","🙎🏽","🙎🏾","🙎🏿","🙅🏻","🙅🏼","🙅🏽","🙅🏾","🙅🏿","🙆🏻","🙆🏼","🙆🏽","🙆🏾","🙆🏿","💁🏻","💁🏼","💁🏽","💁🏾","💁🏿","🙋🏻","🙋🏼","🙋🏽","🙋🏾","🙋🏿","🧏🏻","🧏🏼","🧏🏽","🧏🏾","🧏🏿","🙇🏻","🙇🏼","🙇🏽","🙇🏾","🙇🏿","🤦🏻","🤦🏼","🤦🏽","🤦🏾","🤦🏿","🤷🏻","🤷🏼","🤷🏽","🤷🏾","🤷🏿","👮🏻","👮🏼","👮🏽","👮🏾","👮🏿","🕵🏻","🕵🏼","🕵🏽","🕵🏾","🕵🏿","💂🏻","💂🏼","💂🏽","💂🏾","💂🏿","👷🏻","👷🏼","👷🏽","👷🏾","👷🏿","🤴🏻","🤴🏼","🤴🏽","🤴🏾","🤴🏿","👸🏻","👸🏼","👸🏽","👸🏾","👸🏿","👳🏻","👳🏼","👳🏽","👳🏾","👳🏿","👲🏻","👲🏼","👲🏽","👲🏾","👲🏿","🧕🏻","🧕🏼","🧕🏽","🧕🏾","🧕🏿","🤵🏻","🤵🏼","🤵🏽","🤵🏾","🤵🏿","👰🏻","👰🏼","👰🏽","👰🏾","👰🏿","🤰🏻","🤰🏼","🤰🏽","🤰🏾","🤰🏿","🤱🏻","🤱🏼","🤱🏽","🤱🏾","🤱🏿","👼🏻","👼🏼","👼🏽","👼🏾","👼🏿","🎅🏻","🎅🏼","🎅🏽","🎅🏾","🎅🏿","🤶🏻","🤶🏼","🤶🏽","🤶🏾","🤶🏿","🦸🏻","🦸🏼","🦸🏽","🦸🏾","🦸🏿","🦹🏻","🦹🏼","🦹🏽","🦹🏾","🦹🏿","🧙🏻","🧙🏼","🧙🏽","🧙🏾","🧙🏿","🧚🏻","🧚🏼","🧚🏽","🧚🏾","🧚🏿","🧛🏻","🧛🏼","🧛🏽","🧛🏾","🧛🏿","🧜🏻","🧜🏼","🧜🏽","🧜🏾","🧜🏿","🧝🏻","🧝🏼","🧝🏽","🧝🏾","🧝🏿","💆🏻","💆🏼","💆🏽","💆🏾","💆🏿","💇🏻","💇🏼","💇🏽","💇🏾","💇🏿","🚶🏻","🚶🏼","🚶🏽","🚶🏾","🚶🏿","🧍🏻","🧍🏼","🧍🏽","🧍🏾","🧍🏿","🧎🏻","🧎🏼","🧎🏽","🧎🏾","🧎🏿","🏃🏻","🏃🏼","🏃🏽","🏃🏾","🏃🏿","💃🏻","💃🏼","💃🏽","💃🏾","💃🏿","🕺🏻","🕺🏼","🕺🏽","🕺🏾","🕺🏿","🕴🏻","🕴🏼","🕴🏽","🕴🏾","🕴🏿","🧖🏻","🧖🏼","🧖🏽","🧖🏾","🧖🏿","🧗🏻","🧗🏼","🧗🏽","🧗🏾","🧗🏿","🏇🏻","🏇🏼","🏇🏽","🏇🏾","🏇🏿","🏂🏻","🏂🏼","🏂🏽","🏂🏾","🏂🏿","🏌🏻","🏌🏼","🏌🏽","🏌🏾","🏌🏿","🏄🏻","🏄🏼","🏄🏽","🏄🏾","🏄🏿","🚣🏻","🚣🏼","🚣🏽","🚣🏾","🚣🏿","🏊🏻","🏊🏼","🏊🏽","🏊🏾","🏊🏿","⛹🏻","⛹🏼","⛹🏽","⛹🏾","⛹🏿","🏋🏻","🏋🏼","🏋🏽","🏋🏾","🏋🏿","🚴🏻","🚴🏼","🚴🏽","🚴🏾","🚴🏿","🚵🏻","🚵🏼","🚵🏽","🚵🏾","🚵🏿","🤸🏻","🤸🏼","🤸🏽","🤸🏾","🤸🏿","🤽🏻","🤽🏼","🤽🏽","🤽🏾","🤽🏿","🤾🏻","🤾🏼","🤾🏽","🤾🏾","🤾🏿","🤹🏻","🤹🏼","🤹🏽","🤹🏾","🤹🏿","🧘🏻","🧘🏼","🧘🏽","🧘🏾","🧘🏿","🛀🏻","🛀🏼","🛀🏽","🛀🏾","🛀🏿","🛌🏻","🛌🏼","🛌🏽","🛌🏾","🛌🏿","👭🏻","👭🏼","👭🏽","👭🏾","👭🏿","👫🏻","👫🏼","👫🏽","👫🏾","👫🏿","👬🏻","👬🏼","👬🏽","👬🏾","👬🏿","🏻","🏼","🏽","🏾","🏿","👁️🗨️","👱♂️","👱🏻♂️","👱🏼♂️","👱🏽♂️","👱🏾♂️","👱🏿♂️","👨🦰","👨🏻🦰","👨🏼🦰","👨🏽🦰","👨🏾🦰","👨🏿🦰","👨🦱","👨🏻🦱","👨🏼🦱","👨🏽🦱","👨🏾🦱","👨🏿🦱","👨🦳","👨🏻🦳","👨🏼🦳","👨🏽🦳","👨🏾🦳","👨🏿🦳","👨🦲","👨🏻🦲","👨🏼🦲","👨🏽🦲","👨🏾🦲","👨🏿🦲","👱♀️","👱🏻♀️","👱🏼♀️","👱🏽♀️","👱🏾♀️","👱🏿♀️","👩🦰","👩🏻🦰","👩🏼🦰","👩🏽🦰","👩🏾🦰","👩🏿🦰","👩🦱","👩🏻🦱","👩🏼🦱","👩🏽🦱","👩🏾🦱","👩🏿🦱","👩🦳","👩🏻🦳","👩🏼🦳","👩🏽🦳","👩🏾🦳","👩🏿🦳","👩🦲","👩🏻🦲","👩🏼🦲","👩🏽🦲","👩🏾🦲","👩🏿🦲","🙍♂️","🙍🏻♂️","🙍🏼♂️","🙍🏽♂️","🙍🏾♂️","🙍🏿♂️","🙍♀️","🙍🏻♀️","🙍🏼♀️","🙍🏽♀️","🙍🏾♀️","🙍🏿♀️","🙎♂️","🙎🏻♂️","🙎🏼♂️","🙎🏽♂️","🙎🏾♂️","🙎🏿♂️","🙎♀️","🙎🏻♀️","🙎🏼♀️","🙎🏽♀️","🙎🏾♀️","🙎🏿♀️","🙅♂️","🙅🏻♂️","🙅🏼♂️","🙅🏽♂️","🙅🏾♂️","🙅🏿♂️","🙅♀️","🙅🏻♀️","🙅🏼♀️","🙅🏽♀️","🙅🏾♀️","🙅🏿♀️","🙆♂️","🙆🏻♂️","🙆🏼♂️","🙆🏽♂️","🙆🏾♂️","🙆🏿♂️","🙆♀️","🙆🏻♀️","🙆🏼♀️","🙆🏽♀️","🙆🏾♀️","🙆🏿♀️","💁♂️","💁🏻♂️","💁🏼♂️","💁🏽♂️","💁🏾♂️","💁🏿♂️","💁♀️","💁🏻♀️","💁🏼♀️","💁🏽♀️","💁🏾♀️","💁🏿♀️","🙋♂️","🙋🏻♂️","🙋🏼♂️","🙋🏽♂️","🙋🏾♂️","🙋🏿♂️","🙋♀️","🙋🏻♀️","🙋🏼♀️","🙋🏽♀️","🙋🏾♀️","🙋🏿♀️","🧏♂️","🧏🏻♂️","🧏🏼♂️","🧏🏽♂️","🧏🏾♂️","🧏🏿♂️","🧏♀️","🧏🏻♀️","🧏🏼♀️","🧏🏽♀️","🧏🏾♀️","🧏🏿♀️","🙇♂️","🙇🏻♂️","🙇🏼♂️","🙇🏽♂️","🙇🏾♂️","🙇🏿♂️","🙇♀️","🙇🏻♀️","🙇🏼♀️","🙇🏽♀️","🙇🏾♀️","🙇🏿♀️","🤦♂️","🤦🏻♂️","🤦🏼♂️","🤦🏽♂️","🤦🏾♂️","🤦🏿♂️","🤦♀️","🤦🏻♀️","🤦🏼♀️","🤦🏽♀️","🤦🏾♀️","🤦🏿♀️","🤷♂️","🤷🏻♂️","🤷🏼♂️","🤷🏽♂️","🤷🏾♂️","🤷🏿♂️","🤷♀️","🤷🏻♀️","🤷🏼♀️","🤷🏽♀️","🤷🏾♀️","🤷🏿♀️","👨⚕️","👨🏻⚕️","👨🏼⚕️","👨🏽⚕️","👨🏾⚕️","👨🏿⚕️","👩⚕️","👩🏻⚕️","👩🏼⚕️","👩🏽⚕️","👩🏾⚕️","👩🏿⚕️","👨🎓","👨🏻🎓","👨🏼🎓","👨🏽🎓","👨🏾🎓","👨🏿🎓","👩🎓","👩🏻🎓","👩🏼🎓","👩🏽🎓","👩🏾🎓","👩🏿🎓","👨🏫","👨🏻🏫","👨🏼🏫","👨🏽🏫","👨🏾🏫","👨🏿🏫","👩🏫","👩🏻🏫","👩🏼🏫","👩🏽🏫","👩🏾🏫","👩🏿🏫","👨⚖️","👨🏻⚖️","👨🏼⚖️","👨🏽⚖️","👨🏾⚖️","👨🏿⚖️","👩⚖️","👩🏻⚖️","👩🏼⚖️","👩🏽⚖️","👩🏾⚖️","👩🏿⚖️","👨🌾","👨🏻🌾","👨🏼🌾","👨🏽🌾","👨🏾🌾","👨🏿🌾","👩🌾","👩🏻🌾","👩🏼🌾","👩🏽🌾","👩🏾🌾","👩🏿🌾","👨🍳","👨🏻🍳","👨🏼🍳","👨🏽🍳","👨🏾🍳","👨🏿🍳","👩🍳","👩🏻🍳","👩🏼🍳","👩🏽🍳","👩🏾🍳","👩🏿🍳","👨🔧","👨🏻🔧","👨🏼🔧","👨🏽🔧","👨🏾🔧","👨🏿🔧","👩🔧","👩🏻🔧","👩🏼🔧","👩🏽🔧","👩🏾🔧","👩🏿🔧","👨🏭","👨🏻🏭","👨🏼🏭","👨🏽🏭","👨🏾🏭","👨🏿🏭","👩🏭","👩🏻🏭","👩🏼🏭","👩🏽🏭","👩🏾🏭","👩🏿🏭","👨💼","👨🏻💼","👨🏼💼","👨🏽💼","👨🏾💼","👨🏿💼","👩💼","👩🏻💼","👩🏼💼","👩🏽💼","👩🏾💼","👩🏿💼","👨🔬","👨🏻🔬","👨🏼🔬","👨🏽🔬","👨🏾🔬","👨🏿🔬","👩🔬","👩🏻🔬","👩🏼🔬","👩🏽🔬","👩🏾🔬","👩🏿🔬","👨💻","👨🏻💻","👨🏼💻","👨🏽💻","👨🏾💻","👨🏿💻","👩💻","👩🏻💻","👩🏼💻","👩🏽💻","👩🏾💻","👩🏿💻","👨🎤","👨🏻🎤","👨🏼🎤","👨🏽🎤","👨🏾🎤","👨🏿🎤","👩🎤","👩🏻🎤","👩🏼🎤","👩🏽🎤","👩🏾🎤","👩🏿🎤","👨🎨","👨🏻🎨","👨🏼🎨","👨🏽🎨","👨🏾🎨","👨🏿🎨","👩🎨","👩🏻🎨","👩🏼🎨","👩🏽🎨","👩🏾🎨","👩🏿🎨","👨✈️","👨🏻✈️","👨🏼✈️","👨🏽✈️","👨🏾✈️","👨🏿✈️","👩✈️","👩🏻✈️","👩🏼✈️","👩🏽✈️","👩🏾✈️","👩🏿✈️","👨🚀","👨🏻🚀","👨🏼🚀","👨🏽🚀","👨🏾🚀","👨🏿🚀","👩🚀","👩🏻🚀","👩🏼🚀","👩🏽🚀","👩🏾🚀","👩🏿🚀","👨🚒","👨🏻🚒","👨🏼🚒","👨🏽🚒","👨🏾🚒","👨🏿🚒","👩🚒","👩🏻🚒","👩🏼🚒","👩🏽🚒","👩🏾🚒","👩🏿🚒","👮♂️","👮🏻♂️","👮🏼♂️","👮🏽♂️","👮🏾♂️","👮🏿♂️","👮♀️","👮🏻♀️","👮🏼♀️","👮🏽♀️","👮🏾♀️","👮🏿♀️","🕵️♂️","🕵🏻♂️","🕵🏼♂️","🕵🏽♂️","🕵🏾♂️","🕵🏿♂️","🕵️♀️","🕵🏻♀️","🕵🏼♀️","🕵🏽♀️","🕵🏾♀️","🕵🏿♀️","💂♂️","💂🏻♂️","💂🏼♂️","💂🏽♂️","💂🏾♂️","💂🏿♂️","💂♀️","💂🏻♀️","💂🏼♀️","💂🏽♀️","💂🏾♀️","💂🏿♀️","👷♂️","👷🏻♂️","👷🏼♂️","👷🏽♂️","👷🏾♂️","👷🏿♂️","👷♀️","👷🏻♀️","👷🏼♀️","👷🏽♀️","👷🏾♀️","👷🏿♀️","👳♂️","👳🏻♂️","👳🏼♂️","👳🏽♂️","👳🏾♂️","👳🏿♂️","👳♀️","👳🏻♀️","👳🏼♀️","👳🏽♀️","👳🏾♀️","👳🏿♀️","🦸♂️","🦸🏻♂️","🦸🏼♂️","🦸🏽♂️","🦸🏾♂️","🦸🏿♂️","🦸♀️","🦸🏻♀️","🦸🏼♀️","🦸🏽♀️","🦸🏾♀️","🦸🏿♀️","🦹♂️","🦹🏻♂️","🦹🏼♂️","🦹🏽♂️","🦹🏾♂️","🦹🏿♂️","🦹♀️","🦹🏻♀️","🦹🏼♀️","🦹🏽♀️","🦹🏾♀️","🦹🏿♀️","🧙♂️","🧙🏻♂️","🧙🏼♂️","🧙🏽♂️","🧙🏾♂️","🧙🏿♂️","🧙♀️","🧙🏻♀️","🧙🏼♀️","🧙🏽♀️","🧙🏾♀️","🧙🏿♀️","🧚♂️","🧚🏻♂️","🧚🏼♂️","🧚🏽♂️","🧚🏾♂️","🧚🏿♂️","🧚♀️","🧚🏻♀️","🧚🏼♀️","🧚🏽♀️","🧚🏾♀️","🧚🏿♀️","🧛♂️","🧛🏻♂️","🧛🏼♂️","🧛🏽♂️","🧛🏾♂️","🧛🏿♂️","🧛♀️","🧛🏻♀️","🧛🏼♀️","🧛🏽♀️","🧛🏾♀️","🧛🏿♀️","🧜♂️","🧜🏻♂️","🧜🏼♂️","🧜🏽♂️","🧜🏾♂️","🧜🏿♂️","🧜♀️","🧜🏻♀️","🧜🏼♀️","🧜🏽♀️","🧜🏾♀️","🧜🏿♀️","🧝♂️","🧝🏻♂️","🧝🏼♂️","🧝🏽♂️","🧝🏾♂️","🧝🏿♂️","🧝♀️","🧝🏻♀️","🧝🏼♀️","🧝🏽♀️","🧝🏾♀️","🧝🏿♀️","🧞♂️","🧞♀️","🧟♂️","🧟♀️","💆♂️","💆🏻♂️","💆🏼♂️","💆🏽♂️","💆🏾♂️","💆🏿♂️","💆♀️","💆🏻♀️","💆🏼♀️","💆🏽♀️","💆🏾♀️","💆🏿♀️","💇♂️","💇🏻♂️","💇🏼♂️","💇🏽♂️","💇🏾♂️","💇🏿♂️","💇♀️","💇🏻♀️","💇🏼♀️","💇🏽♀️","💇🏾♀️","💇🏿♀️","🚶♂️","🚶🏻♂️","🚶🏼♂️","🚶🏽♂️","🚶🏾♂️","🚶🏿♂️","🚶♀️","🚶🏻♀️","🚶🏼♀️","🚶🏽♀️","🚶🏾♀️","🚶🏿♀️","🧍♂️","🧍🏻♂️","🧍🏼♂️","🧍🏽♂️","🧍🏾♂️","🧍🏿♂️","🧍♀️","🧍🏻♀️","🧍🏼♀️","🧍🏽♀️","🧍🏾♀️","🧍🏿♀️","🧎♂️","🧎🏻♂️","🧎🏼♂️","🧎🏽♂️","🧎🏾♂️","🧎🏿♂️","🧎♀️","🧎🏻♀️","🧎🏼♀️","🧎🏽♀️","🧎🏾♀️","🧎🏿♀️","👨🦯","👨🏻🦯","👨🏼🦯","👨🏽🦯","👨🏾🦯","👨🏿🦯","👩🦯","👩🏻🦯","👩🏼🦯","👩🏽🦯","👩🏾🦯","👩🏿🦯","👨🦼","👨🏻🦼","👨🏼🦼","👨🏽🦼","👨🏾🦼","👨🏿🦼","👩🦼","👩🏻🦼","👩🏼🦼","👩🏽🦼","👩🏾🦼","👩🏿🦼","👨🦽","👨🏻🦽","👨🏼🦽","👨🏽🦽","👨🏾🦽","👨🏿🦽","👩🦽","👩🏻🦽","👩🏼🦽","👩🏽🦽","👩🏾🦽","👩🏿🦽","🏃♂️","🏃🏻♂️","🏃🏼♂️","🏃🏽♂️","🏃🏾♂️","🏃🏿♂️","🏃♀️","🏃🏻♀️","🏃🏼♀️","🏃🏽♀️","🏃🏾♀️","🏃🏿♀️","👯♂️","👯♀️","🧖♂️","🧖🏻♂️","🧖🏼♂️","🧖🏽♂️","🧖🏾♂️","🧖🏿♂️","🧖♀️","🧖🏻♀️","🧖🏼♀️","🧖🏽♀️","🧖🏾♀️","🧖🏿♀️","🧗♂️","🧗🏻♂️","🧗🏼♂️","🧗🏽♂️","🧗🏾♂️","🧗🏿♂️","🧗♀️","🧗🏻♀️","🧗🏼♀️","🧗🏽♀️","🧗🏾♀️","🧗🏿♀️","🏌️♂️","🏌🏻♂️","🏌🏼♂️","🏌🏽♂️","🏌🏾♂️","🏌🏿♂️","🏌️♀️","🏌🏻♀️","🏌🏼♀️","🏌🏽♀️","🏌🏾♀️","🏌🏿♀️","🏄♂️","🏄🏻♂️","🏄🏼♂️","🏄🏽♂️","🏄🏾♂️","🏄🏿♂️","🏄♀️","🏄🏻♀️","🏄🏼♀️","🏄🏽♀️","🏄🏾♀️","🏄🏿♀️","🚣♂️","🚣🏻♂️","🚣🏼♂️","🚣🏽♂️","🚣🏾♂️","🚣🏿♂️","🚣♀️","🚣🏻♀️","🚣🏼♀️","🚣🏽♀️","🚣🏾♀️","🚣🏿♀️","🏊♂️","🏊🏻♂️","🏊🏼♂️","🏊🏽♂️","🏊🏾♂️","🏊🏿♂️","🏊♀️","🏊🏻♀️","🏊🏼♀️","🏊🏽♀️","🏊🏾♀️","🏊🏿♀️","⛹️♂️","⛹🏻♂️","⛹🏼♂️","⛹🏽♂️","⛹🏾♂️","⛹🏿♂️","⛹️♀️","⛹🏻♀️","⛹🏼♀️","⛹🏽♀️","⛹🏾♀️","⛹🏿♀️","🏋️♂️","🏋🏻♂️","🏋🏼♂️","🏋🏽♂️","🏋🏾♂️","🏋🏿♂️","🏋️♀️","🏋🏻♀️","🏋🏼♀️","🏋🏽♀️","🏋🏾♀️","🏋🏿♀️","🚴♂️","🚴🏻♂️","🚴🏼♂️","🚴🏽♂️","🚴🏾♂️","🚴🏿♂️","🚴♀️","🚴🏻♀️","🚴🏼♀️","🚴🏽♀️","🚴🏾♀️","🚴🏿♀️","🚵♂️","🚵🏻♂️","🚵🏼♂️","🚵🏽♂️","🚵🏾♂️","🚵🏿♂️","🚵♀️","🚵🏻♀️","🚵🏼♀️","🚵🏽♀️","🚵🏾♀️","🚵🏿♀️","🤸♂️","🤸🏻♂️","🤸🏼♂️","🤸🏽♂️","🤸🏾♂️","🤸🏿♂️","🤸♀️","🤸🏻♀️","🤸🏼♀️","🤸🏽♀️","🤸🏾♀️","🤸🏿♀️","🤼♂️","🤼♀️","🤽♂️","🤽🏻♂️","🤽🏼♂️","🤽🏽♂️","🤽🏾♂️","🤽🏿♂️","🤽♀️","🤽🏻♀️","🤽🏼♀️","🤽🏽♀️","🤽🏾♀️","🤽🏿♀️","🤾♂️","🤾🏻♂️","🤾🏼♂️","🤾🏽♂️","🤾🏾♂️","🤾🏿♂️","🤾♀️","🤾🏻♀️","🤾🏼♀️","🤾🏽♀️","🤾🏾♀️","🤾🏿♀️","🤹♂️","🤹🏻♂️","🤹🏼♂️","🤹🏽♂️","🤹🏾♂️","🤹🏿♂️","🤹♀️","🤹🏻♀️","🤹🏼♀️","🤹🏽♀️","🤹🏾♀️","🤹🏿♀️","🧘♂️","🧘🏻♂️","🧘🏼♂️","🧘🏽♂️","🧘🏾♂️","🧘🏿♂️","🧘♀️","🧘🏻♀️","🧘🏼♀️","🧘🏽♀️","🧘🏾♀️","🧘🏿♀️","🧑🤝🧑","🧑🏻🤝🧑🏻","🧑🏼🤝🧑🏻","🧑🏼🤝🧑🏼","🧑🏽🤝🧑🏻","🧑🏽🤝🧑🏼","🧑🏽🤝🧑🏽","🧑🏾🤝🧑🏻","🧑🏾🤝🧑🏼","🧑🏾🤝🧑🏽","🧑🏾🤝🧑🏾","🧑🏿🤝🧑🏻","🧑🏿🤝🧑🏼","🧑🏿🤝🧑🏽","🧑🏿🤝🧑🏾","🧑🏿🤝🧑🏿","👩🏼🤝👩🏻","👩🏽🤝👩🏻","👩🏽🤝👩🏼","👩🏾🤝👩🏻","👩🏾🤝👩🏼","👩🏾🤝👩🏽","👩🏿🤝👩🏻","👩🏿🤝👩🏼","👩🏿🤝👩🏽","👩🏿🤝👩🏾","👩🏻🤝👨🏼","👩🏻🤝👨🏽","👩🏻🤝👨🏾","👩🏻🤝👨🏿","👩🏼🤝👨🏻","👩🏼🤝👨🏽","👩🏼🤝👨🏾","👩🏼🤝👨🏿","👩🏽🤝👨🏻","👩🏽🤝👨🏼","👩🏽🤝👨🏾","👩🏽🤝👨🏿","👩🏾🤝👨🏻","👩🏾🤝👨🏼","👩🏾🤝👨🏽","👩🏾🤝👨🏿","👩🏿🤝👨🏻","👩🏿🤝👨🏼","👩🏿🤝👨🏽","👩🏿🤝👨🏾","👨🏼🤝👨🏻","👨🏽🤝👨🏻","👨🏽🤝👨🏼","👨🏾🤝👨🏻","👨🏾🤝👨🏼","👨🏾🤝👨🏽","👨🏿🤝👨🏻","👨🏿🤝👨🏼","👨🏿🤝👨🏽","👨🏿🤝👨🏾","👩❤️💋👨","👨❤️💋👨","👩❤️💋👩","👩❤️👨","👨❤️👨","👩❤️👩","👨👩👦","👨👩👧","👨👩👧👦","👨👩👦👦","👨👩👧👧","👨👨👦","👨👨👧","👨👨👧👦","👨👨👦👦","👨👨👧👧","👩👩👦","👩👩👧","👩👩👧👦","👩👩👦👦","👩👩👧👧","👨👦","👨👦👦","👨👧","👨👧👦","👨👧👧","👩👦","👩👦👦","👩👧","👩👧👦","👩👧👧","🐕🦺","🏳️🌈","🏴☠️","〰︎","‼︎","⁉︎","*︎⃣","#︎⃣","〽︎","©︎","®︎","↔︎","↕︎","↖︎","↗︎","↘︎","↙︎","↩︎","↪︎","⌨︎","⏏︎","⏭︎","⏮︎","⏯︎","⏱︎","⏲︎","⏸︎","⏹︎","⏺︎","▪︎","🖐","✌","☝","✍","☹️","☠️","❣️","❤️","🕳️","🗨️","🗯️","🖐️","✌️","☝️","✍️","👁️","🕵️","🕴️","⛷️","🏌️","⛹️","🏋️","🗣️","🐿️","🕊️","🕷️","🕸️","🏵️","☘️","🌶️","🍽️","🗺️","🏔️","⛰️","🏕️","🏖️","🏜️","🏝️","🏞️","🏟️","🏛️","🏗️","🏘️","🏚️","⛩️","🏙️","♨️","🏎️","🏍️","🛣️","🛤️","🛢️","🛳️","⛴️","🛥️","✈️","🛩️","🛰️","🛎️","⏱️","⏲️","🕰️","🌡️","☀️","☁️","⛈️","🌤️","🌥️","🌦️","🌧️","🌨️","🌩️","🌪️","🌫️","🌬️","☂️","⛱️","❄️","☃️","☄️","🎗️","🎟️","🎖️","⛸️","🕹️","♠️","♥️","♦️","♣️","♟️","🖼️","🕶️","🛍️","⛑️","🎙️","🎚️","🎛️","☎️","🖥️","🖨️","⌨️","🖱️","🖲️","🎞️","📽️","🕯️","🗞️","🏷️","✉️","🗳️","✏️","✒️","🖋️","🖊️","🖌️","🖍️","🗂️","🗒️","🗓️","🖇️","✂️","🗃️","🗄️","🗑️","🗝️","⛏️","⚒️","🛠️","🗡️","⚔️","🛡️","⚙️","🗜️","⚖️","⛓️","⚗️","🛏️","🛋️","⚰️","⚱️","⚠️","☢️","☣️","⬆️","↗️","➡️","↘️","⬇️","↙️","⬅️","↖️","↕️","↔️","↩️","↪️","⤴️","⤵️","⚛️","🕉️","✡️","☸️","☯️","✝️","☦️","☪️","☮️","▶️","⏭️","⏯️","◀️","⏮️","⏸️","⏹️","⏺️","⏏️","♀️","♂️","⚕️","♾️","♻️","⚜️","☑️","✔️","✖️","〽️","✳️","✴️","❇️"]
- </script>
- <script>
- const DOC_VERSION = 0
- var note = null
- var TARGET = null
- var SELECTION = []
- INSERTING = false
- Array.prototype.distinct = function(){
- let res = []
- for (let x of this){
- if (!res.includes(x)) {
- res.push(x)}}
- return res}
- // construct a dom fragment from nested ['tag', optional {attrs}, ... children]
- function Render(node) {
- if (!node) {
- return
- } else if (typeof(node) === 'string') {
- return document.createTextNode(node)
- } else {
- let tag = node[0].match(/^[^\.#]+/)[0]
- let classes = node[0].match(/\.[^\.#]+/g) || []
- let ids = node[0].match(/\#[^\.#]+/g) || []
- let attrs = Object.prototype.toString.call(node[1]) === '[object Object]' ? node[1] : null
- let children = node.slice(attrs ? 2 : 1)
- let el = document.createElement(tag)
- if (attrs) {
- for ([k, v] of Object.entries(attrs)) {
- if (typeof(v) === 'function') {
- el[k] = v
- } else {
- el.setAttribute(k, v)
- }}}
- classes.forEach((s)=>el.classList.add(s.slice(1)))
- if (ids[0]) {el.setAttribute('id', (ids[0].slice(1)))}
- children.forEach((child)=>{
- let cnode = Render(child)
- if (cnode) {
- el.appendChild(cnode)}})
- return el}}
- // Storage stuff
- // will use IndexedDB when available, hence the asyncronicity
- function PutItem(k, o){
- localStorage.setItem(k, JSON.stringify(o))
- }
- function GetItem(k, f){
- f(JSON.parse(localStorage.getItem(k)))
- }
- function DeleteItem(k){
- localStorage.removeItem(k)
- }
- function StorageItems(f){
- f(Object.entries(localStorage))
- }
- // Note file stuff
- function SaveMeta(){
- localStorage.setItem('meta', JSON.stringify(meta))
- }
- function SaveNote(note) {
- note.edited = new Date()
- PutItem(note.uid, note)
- }
- function NewNote() {
- random_emoji = emoji[Math.floor(Math.random()*emoji.length)]
- res = {
- uid: Math.random().toString(16).slice(2),
- title: `${random_emoji} Untitled`,
- content: "<p></p>",
- version: DOC_VERSION,
- created: new Date(),
- edited: new Date()
- }
- SaveNote(res)
- return res
- }
- function OpenNote(uid) {
- if (uid == null) {
- note = null
- meta.current_note = null
- document.querySelector("document").classList.add("hidden")
- } else {
- GetItem(uid, (o)=>{
- note = o
- meta.current_note = note.uid
- document.querySelector("document").classList.remove("hidden")
- document.querySelector("#title").textContent = note.title
- document.querySelector("note").innerHTML = note.content
- HistoryRecordState()
- SaveMeta()
- })
- }
- HISTORY = []
- HIDX = 0
- }
- function DeleteNote(uid){
- if (note.uid == uid) {
- OpenNote(null)
- }
- DeleteItem(uid)
- }
- function SyncContent() {
- note.content = document.querySelector("note").innerHTML
- note.title = document.querySelector("#title").innerText
- SaveNote(note)
- }
- /* TODO
- [x] undo/redo traversal
- [x] key command capture
- [x] state change truncates stack
- [ ] stack length limit
- [ ] cursor preservation
- */
- var HISTORY = []
- var HIDX = 0
- function HistoryRecordState(){
- if (INSERTING) {return}
- let s = document.querySelector("note").innerHTML
- if (HISTORY[HIDX] != s) {
- HISTORY = HISTORY.slice(0, HIDX+1)
- HISTORY.push(s)
- HIDX = HISTORY.length-1}
- SyncContent()}
- function HistoryUndo(){
- HistoryRecordState() // due to interval recording there might be changes
- if (HIDX > 0) {
- HIDX -= 1
- document.querySelector("note").innerHTML = HISTORY[HIDX]}
- SyncContent()
- }
- function HistoryRedo(){
- if (HIDX < HISTORY.length-1) {
- HIDX += 1
- document.querySelector("note").innerHTML = HISTORY[HIDX]
- SyncContent()}}
- document.addEventListener("keydown", (e) => {
- if (e.key === "z" && e.ctrlKey === true) {
- e.preventDefault()
- HistoryUndo()}
- if (e.key === "y" && e.ctrlKey === true) {
- e.preventDefault()
- HistoryRedo()}})
- setInterval(()=>HistoryRecordState(), 1000)
- const block_defs = {
- p: {shortcode: "/p", tagName: "P", vdom: ['p']},
- h1: {shortcode: "/h1", tagName: "H1", vdom: ['h1']},
- h2: {shortcode: "/h2", tagName: "H2", vdom: ['h2']},
- h3: {shortcode: "/h3", tagName: "H3", vdom: ['h3']},
- h4: {shortcode: "/h4", tagName: "H4", vdom: ['h4']},
- hr: {shortcode: "/hr", tagName: "HR", vdom: ['hr']},
- //img: {shortcode: "/img", tagName: "IMG", vdom: ['img']},
- li: {shortcode: "* ", tagName: "LI", vdom: ['li']},
- todo: {shortcode: "[] ", tagName: "DIV", vdom: ['div.todo', {onclick: 'this.classList.toggle("checked")'}]}
- }
- const block_tags = Object.entries(block_defs).map(([k,v])=>v.tagName)
- function ValidBlockType(node){
- if (block_tags.includes(node.tagName)) {
- if (node.tagName === "DIV") {
- if (node.classList.contains("todo")) {
- return true
- }
- } else {
- return true
- }
- }
- }
- function StripBlockClasses(s){
- return (s || "").replaceAll(/todo/g, "")
- }
- // block manipulation
- function IsText(el){
- return el.nodeName === "#text"
- }
- function PlaceCursor(node, offset){
- let s = getSelection()
- s.removeAllRanges()
- let nr = new Range()
- nr.setStart(node, offset)
- s.addRange(nr)
- }
- function CurrentRange(){
- var s = getSelection()
- if (s.rangeCount > 0) {
- return s.getRangeAt(0)}}
- function ReplaceNode(el, nel){
- el.childNodes.forEach((e)=>nel.appendChild(e))
- el.replaceWith(nel)
- return nel
- }
- // only interested in collapsed cursors at the moment
- function TextBeforeCursor(){
- r = CurrentRange()
- if (r) {
- if (r.collapsed == true && r.startContainer.nodeName === "#text" && !r.startContainer.previousSibling) {
- return r.startContainer.textContent.slice(0, r.startOffset).replace(/\u00A0/g, ' ')
- }
- }
- }
- function AncestorChild(el, ancestor){
- while (el != ancestor && el.parentNode != ancestor) {
- el = el.parentNode}
- return el}
- // bubble up until a valid block is found
- function BubbledBlock(el){
- if (!document.querySelector("note").contains(el)) { return }
- while (!ValidBlockType(el) && el.parentNode) {
- el = el.parentNode}
- if (ValidBlockType(el)) {return el}
- }
- // this should be a list of 'shallow' siblings that have the selection intersecting them
- // 1. find the common ancestor
- // 2. for start and end find the ancestor inside the common (so 1 level deep), these will be siblings
- // 3. iterate via nextSibling to get the list
- // Should this include indent children?
- function SelectedElements(){
- res = []
- r = CurrentRange()
- if (r) {
- start = AncestorChild(r.startContainer, r.commonAncestorContainer)
- end = AncestorChild(r.endContainer, r.commonAncestorContainer)
- res.push(start)
- if (start != end) {
- while (start.nextElementSibling && start.nextElementSibling != end) {
- start = start.nextElementSibling
- res.push(start)
- }
- res.push(end)
- }
- }
- res = res.map((n)=> n.nodeName === "#text" ? n.parentNode : n)
- return res.distinct().filter(e=>document.querySelector("note").contains(e))
- }
- function SaveSelection(){
- s = getSelection()
- window._ranges = []
- for (let i = 0; i < s.rangeCount; i++) {
- r = s.getRangeAt(i)
- window._ranges.push({start: r.startContainer, end: r.endContainer, so: r.startOffset, eo: r.endOffset})
- }
- }
- function LoadSelection(){
- s = getSelection()
- s.removeAllRanges()
- for (r of window._ranges) {
- nr = new Range()
- nr.setStart(r.start, r.so)
- nr.setEnd(r.end, r.eo)
- s.addRange(nr)
- }
- }
- function LintNote() {
- console.log("===== Lint =====")
- el = document.querySelector("note")
- console.log(el.innerHTML)
- for (node of el.children){
- // TODO need to only keep special divs like div.todo, need a better check here
- if (!ValidBlockType(node)) {
- node = ReplaceNode(node, Render(['p']))
- }
- while (node.previousSibling && node.previousSibling.nodeName === "#text") {
- node.prepend(node.previousSibling)
- }
- }
- // possible to have top level text when starting from empty editable
- SaveSelection()
- for (node of el.childNodes){
- if (node.nodeName === "#text") {
- p = Render(['p'])
- node.replaceWith(p)
- p.appendChild(node)
- }
- }
- LoadSelection()
- console.log(el.innerHTML)
- }
- function OPIndent(n){
- for (el of SelectedElements()) {
- let level = 0
- let existing = el.className.match(/indent(\d+)/)
- if (existing) {
- el.classList.remove(existing[0])
- level = parseInt(existing[1])
- }
- if (n + level > 0) {
- el.classList.add(`indent${n+level}`)
- }
- }
- }
- function OPDelete(e){
- let r = CurrentRange()
- // figure out if I'm at the beginning of a block - I think this is broken a bit
- if ((IsText(r.startContainer)
- && ValidBlockType(r.startContainer.parentNode)
- && !r.startContainer.previousSibling
- && r.startOffset == 0)
- || (ValidBlockType(r.startContainer) && r.startOffset == 0)){
- let block = ValidBlockType(r.startContainer) ? r.startContainer : r.startContainer.parentNode
- // delete at start converts other block types to a paragraph
- console.log("block:", block)
- if (block.tagName != "P") {
- let el = ReplaceNode(block, Render(['p', {class: StripBlockClasses(block.className)}]))
- PlaceCursor(el, 0)
- e.preventDefault()
- }
- }
- }
- function RenderSidebar() {
- StorageItems((xs)=>{
- let notes = xs.map(([k,v])=>[k,JSON.parse(v)]).sort(function([_a,a],[_b,b]){
- return new Date(a.created || "2000-01-01") - new Date(b.created || "2000-01-01")
- })
- document.querySelector('sidebar content').replaceChildren(
- Render(
- ['div',
- ['button', {onclick: (e)=>{OpenNote(NewNote().uid); RenderSidebar()}}, "+ new note"],
- ['div.note-list'].concat(
- notes.map(([k,o])=>{
- try {
- if (k != 'meta') {
- return ['p', {onclick: (e)=>OpenNote(k)}, o.title,
- ['button', {onclick: (e)=>{
- e.stopPropagation()
- if (confirm(`Delete note "${o.title}"`)){
- DeleteNote(k)
- RenderSidebar()
- }
- }}, 'x']]}
- } catch (error) {
- console.error(error)}}))]))})}
- // track blocks under mouse and under selection. Should probably also detect 'child' indentation blocks
- addEventListener("mousemove", (e) => {window.X = e.clientX; window.Y = e.clientY})
-
- function WithinBounds(bounds, x, y){
- if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) {
- return false
- }
- return true
- }
- function UpdateTargetAndSelection (e) {
- let bounds = document.querySelector("note").getBoundingClientRect()
- if (!WithinBounds(bounds, e.clientX,e.clientY)) {
- TARGET = null;
- }
- // TODO get top level block
- let target = BubbledBlock(e.target)
- SELECTION = []
- let r = CurrentRange()
- if (r && !r.collapsed) {
- for (e of SelectedElements()) {
- SELECTION.push(e)
- }}
- if (target) {
- TARGET = target
- }
-
- let place_handle = (handle, top, left, bottom) => {
- handle.style.display = ""
- handle.style.left = `${left}px`
- handle.style.top = `${top}px`
- handle.style.width = `24px`
- handle.style.height = `${bottom-top}px`
- }
- let a = document.querySelectorAll("selection")[0]
- let b = document.querySelectorAll("selection")[1]
- for (const x of [a, b]) {
- x.style.display = "none"
- }
-
- if (SELECTION.length > 0) {
- let b1 = SELECTION[0].getBoundingClientRect()
- let b2 = SELECTION[SELECTION.length-1].getBoundingClientRect()
- let groupb = SELECTION[0].parentNode.getBoundingClientRect()
- place_handle(a, b1.top, groupb.left, b2.bottom)
- }
- if (TARGET && !SELECTION.includes(TARGET) && TARGET.parentNode) {
- let b1 = TARGET.getBoundingClientRect()
- let groupb = TARGET.parentNode.getBoundingClientRect()
- place_handle(b, b1.top, groupb.left, b1.bottom)
- }
- }
- addEventListener("pointermove", UpdateTargetAndSelection)
- addEventListener("pointerup", UpdateTargetAndSelection)
- // TODO add an opt for possible drop targets with inside, before, after etc. handlers
- function SetupDrag(el, handlers) {
- el.style.touchAction = "none"
- el.addEventListener("pointerdown", (e)=> {
- el.setPointerCapture(e.pointerId)
- handlers["start"]?.(e)
- })
- el.addEventListener("pointermove", (e)=> {
- handlers["move"]?.(e)
- })
- el.addEventListener("pointerup", (e)=> {
- el.releasePointerCapture(e.pointerId)
- handlers["end"]?.(e)
- })
- }
- // Insertion stuff
- function BRectDistance(rect, x, y) {
- var dx = Math.max(rect.left - x, 0, x - rect.right)
- var dy = Math.max(rect.top - y, 0, y - rect.bottom)
- return Math.sqrt(dx*dx + dy*dy)
- }
- function ClosestChild(el, x, y) {
- let nodes = []
- for (e of el.childNodes) {
- if (!SELECTION.includes(e) && !(TARGET == e)) {
- nodes.push(e)
- }
- }
- nodes.sort((a, b)=> BRectDistance(a.getBoundingClientRect(), x, y) - BRectDistance(b.getBoundingClientRect(), x, y))
- return nodes[0]
- }
- document.querySelectorAll("selection").forEach((sel)=> {
- SetupDrag(sel, {
- 'start': e => {
- // using queryselector to order target in selection
- let marked = [... SELECTION, TARGET]
- marked.forEach((e)=> e?.classList.add("_dragging"))
- INSERTING = [... document.querySelectorAll("._dragging")]
- console.log("DRAG START", TARGET)
- },
- 'move': e => {
- if (INSERTING) {
- document.querySelectorAll(".insertion").forEach((el)=>{
- el.classList.remove("insertion")
- el.classList.remove("ibefore")
- el.classList.remove("iafter")
- })
- // if not over note panel we want no insertion point
- if (!WithinBounds(document.querySelector("note").getBoundingClientRect(), e.clientX,e.clientY)) {
- return
- }
- let closest = ClosestChild(document.querySelector("note"), e.clientX, e.clientY)
- if (closest) {
- closest.classList.add("insertion")
- let bounds = closest.getBoundingClientRect()
- if (e.clientY < bounds.top+4) {
- closest.classList.add("ibefore")
- } else if (e.clientY > bounds.bottom-4) {
- closest.classList.add("iafter")
- }
- }
- }
- },
- 'end': e => {
- console.log("DRAG END")
- INSERTING.forEach((e)=> e.classList.remove("_dragging"))
- let point = document.querySelector(".insertion")
- if (INSERTING && point && !INSERTING.includes(point)) {
- if (point.classList.contains("ibefore")) {
- INSERTING.forEach(el => {
- el.parentNode.removeChild(el)
- point.before(el)
- })
- } else {
- INSERTING.reverse()
- INSERTING.forEach(el => {
- el.parentNode.removeChild(el)
- point.after(el)
- })
- }
- }
- document.querySelectorAll(".insertion").forEach((el)=>el.classList.remove("insertion"))
- //document.querySelectorAll("selection").forEach((el)=>el.style.display = "none")
-
- INSERTING = false
- }
- })
- })
-
- document.querySelector("note").ondrop = e => {
- e.preventDefault()
- }
-
- document.querySelector("note").addEventListener("input", ()=>{LintNote(); SyncContent()}, false)
- document.querySelector("#title").addEventListener("input", ()=>{SyncContent(); RenderSidebar()}, false)
- document.querySelector("note").addEventListener("keydown", (e) => {
- console.log(e)
- let r = CurrentRange()
- if (r.collapsed && r.startContainer === e.target) {
- console.log("out of a block!!!")
- let block = e.target.childNodes[r.startOffset] || e.target.childNodes[0]
- if (block) {
- PlaceCursor(block, block.childNodes.length)
- }
- }
- if (e.key === "Tab") {
- e.preventDefault()
- OPIndent(e.shiftKey ? -1 : 1)
- }
- if (e.key === "Backspace") {
- OPDelete(e)
- }
- SyncContent()
- })
- document.querySelector("note").addEventListener("keyup", (e) => {
- console.log("keyup")
- if (true) { //(e.code === "Space") {
- let before = TextBeforeCursor()
- let text = getSelection().getRangeAt(0).startContainer
- for (const [k,v] of Object.entries(block_defs)) {
- if (before === v.shortcode) {
- let block = text.parentNode
- text.textContent = text.textContent.slice(v.shortcode.length)
- let el = ReplaceNode(block, Render(v.vdom))
- block.setAttribute("class", StripBlockClasses(block.getAttribute("class")))
- block.classList.forEach((s)=>el.classList.add(s))
- PlaceCursor(el, 0)
- }
- }
- }
- SyncContent()
- })
- document.querySelector("note").addEventListener("drop", (e)=>{
- [... e.dataTransfer.files].forEach((file, i) => {
- if(file.type.startsWith("image")){
- let reader = new FileReader()
- reader.onload = function (event) {
- document.querySelector("note").appendChild(Render(['p', ['img', {"src": event.target.result}]]))
- }
- reader.readAsDataURL(file)
- e.preventDefault()
- }
- })
-
- }, false)
- var meta = JSON.parse(localStorage.getItem('meta')) || {current_note: null}
- OpenNote(meta.current_note || NewNote().uid)
- RenderSidebar()
- </script>
- </body>
- </html>
|