HTML
CSS
JS
Result
Skip Results Iframe
EDIT ON
Live
Live
Live
This Pen is owned by
web.dev
on
CodePen
.
See more by @web-dot-dev on CodePen
External CSS
https://codepen.io/web-dot-dev/pen/XWqWYgB.css
https://codepen.io/web-dot-dev/pen/ZExZWBQ.css
External JavaScript
https://codepen.io/web-dot-dev/pen/XWqWYgB.js
https://codepen.io/web-dot-dev/pen/ZExZWBQ.js
{"__browser":{"country":"US","device":"unknown_device","mobile":false,"name":"unknown browser","platform":"unknown_platform","version":"0"},"__constants":{},"__CPDATA":{"domain_iframe":"https://cdpn.io","environment":"production","host":"codepen.io","iframe_allow":"accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; geolocation; gyroscope; microphone; midi; payment; vr; web-share","iframe_sandbox":"allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation allow-downloads allow-presentation"},"__graphql":{"data":{"data":null,"errors":[{"message":"Cannot return null for non-nullable field Query.sessionUser"}]},"url":"https://codepen.io/graphql","api":"cprails"},"__pay_stripe_public_key":"pk_live_2GndomDfiklqpSNQn8FrGuwZSMIMzha7DkLJqlYe7IR0ihKAlKdiHg68JJc5eVPt68rzAjzAAVXcUwjySHRCsgjQQ00gtRBUFNH","__pay_braintree_env":"production","__boomboom":{"serve_url":"https://cdpn.io/cpe/boomboom","store_url":"https://codepen.io/cpe/boomboom/store"},"__pageType":"embed","__item":"{\"editor_settings\":{\"auto_run\":true,\"autocomplete\":false,\"code_folding\":true,\"css_pre_processor\":\"none\",\"css_prefix\":\"neither\",\"css_starter\":\"neither\",\"emmet_active\":true,\"font_size\":14,\"font_type\":\"system\",\"format_on_save\":true,\"html_pre_processor\":\"none\",\"indent_with\":\"spaces\",\"js_pre_processor\":\"none\",\"key_bindings\":\"normal\",\"line_numbers\":true,\"line_wrapping\":true,\"match_brackets\":true,\"snippets\":{\"markupSnippets\":{},\"stylesheetSnippets\":{}},\"tab_size\":2,\"theme\":\"twilight\",\"id\":\"oNdgvoR\",\"auto_save\":true},\"hashid\":\"oNdgvoR\",\"itemType\":\"pen\",\"resources\":[{\"url\":\"https://codepen.io/web-dot-dev/pen/XWqWYgB.css\",\"order\":0,\"resource_type\":\"css\",\"par_content\":\"\"},{\"url\":\"https://codepen.io/web-dot-dev/pen/XWqWYgB.js\",\"order\":0,\"resource_type\":\"js\",\"par_content\":\"\"},{\"url\":\"https://codepen.io/web-dot-dev/pen/ZExZWBQ.css\",\"order\":1,\"resource_type\":\"css\",\"par_content\":\"\"},{\"url\":\"https://codepen.io/web-dot-dev/pen/ZExZWBQ.js\",\"order\":1,\"resource_type\":\"js\",\"par_content\":\"\"}],\"tags\":[],\"id\":62926245,\"user_id\":5928893,\"html\":\"<main>\\n <header>\\n <h1>Caret position pop-ups</h1>\\n </header>\\n <article>\\n <p>\\n <strong>\\n Grab the caret position of an <code>input</code> element. Then position a pop-up based on that. Handy for UI patterns seen when editing text or for tagging.\\n </strong>\\n </p>\\n <section>\\n <h2>Track caret</h2>\\n <p>Display a pop-up where the caret is.</p>\\n <textarea name=\\\"basic\\\" id=\\\"basic\\\" placeholder=\\\"Interact with me!\\\" spellcheck=\\\"false\\\"></textarea>\\n </section>\\n <section>\\n <h2>On selection</h2>\\n <p>Display a pop-up where text is selected.</p>\\n <textarea name=\\\"select\\\" id=\\\"select\\\" spellcheck=\\\"false\\\">\\nLorem ipsum dolor sit amet consectetur adipisicing, elit. Quas ratione nesciunt nulla perspiciatis voluptate expedita praesentium modi quaerat exercitationem voluptatibus dolorem ducimus sunt, fugiat assumenda velit dolorum eum, ipsum aperiam.\\n </textarea>\\n </section>\\n <section>\\n <h2>Character trigger</h2>\\n <p>Use a text trigger to show a pop-up.</p>\\n <textarea name=\\\"trigger\\\" id=\\\"trigger\\\" placeholder=\\\"Type the @ symbol to trigger\\\" spellcheck=\\\"false\\\"></textarea>\\n </section>\\n </article>\\n</main>\\n<div class=\\\"caret-marker\\\" popover id=\\\"caret-marker\\\"></div>\",\"css\":\"@layer demo {\\n [popover] {\\n margin: 0;\\n background: var(--text-1);\\n padding: var(--size-2);\\n color: var(--surface-1);\\n top: calc(var(--y) * 1px);\\n left: calc(var(--x) * 1px);\\n border-radius: var(--radius-2);\\n transition: all 0.1s ease;\\n }\\n\\n .popup--basic {\\n transform: translate(10%, -10%);\\n }\\n .popup--select {\\n transform: translate(-50%, -125%);\\n }\\n .popup--trigger {\\n transform: translate(5%, -110%);\\n }\\n}\\n\\n@layer base {\\n *,\\n *:after,\\n *:before {\\n box-sizing: border-box;\\n }\\n\\n body {\\n min-height: 100vh;\\n display: block;\\n overflow: auto;\\n background: var(--surface-2);\\n font-family: \\\"Google Sans\\\", sans-serif, system-ui;\\n }\\n\\n :where([popover]) {\\n margin: auto;\\n border-width: 0;\\n border-style: none;\\n background: transparent;\\n }\\n\\n h1 {\\n margin: 0;\\n color: var(--gray-0);\\n }\\n\\n header {\\n height: 15vmin;\\n min-height: 200px;\\n background: var(--gradient-16);\\n display: grid;\\n place-items: center;\\n color: var(--gray-0);\\n padding: var(--size-4);\\n }\\n\\n h1 {\\n background: var(--gradient-19);\\n background-clip: text;\\n -webkit-background-clip: text;\\n background-attachment: fixed;\\n color: transparent;\\n font-size: var(--font-size-fluid-2);\\n text-align: center;\\n }\\n\\n h2 {\\n font-size: var(--font-size-fluid-1);\\n }\\n\\n main {\\n margin: 0 auto;\\n }\\n\\n textarea {\\n width: 100%;\\n background: var(--surface-1);\\n border-radius: var(--radius-2);\\n border: 2px solid var(--text-2);\\n min-height: 120px;\\n }\\n\\n article {\\n padding: var(--size-4);\\n display: flex;\\n flex-direction: column;\\n margin: 0 auto;\\n width: var(--size-content-3);\\n max-width: 100%;\\n padding: var(--size-4);\\n }\\n\\n article > * + * {\\n margin-top: var(--size-4);\\n }\\n\\n section > * + * {\\n margin-top: var(--size-2);\\n }\\n}\\n\",\"js\":\"/**\\n * Utility methods from https://github.com/jh3y/use-caret-position\\n * */\\nconst getCaretPosition = (input, selection = \\\"selectionStart\\\") => {\\n const { scrollLeft, scrollTop } = input;\\n // This provides a hook for getSelection to reuse getCaretPosition.\\n const selectionPoint = input[selection] || input.selectionStart;\\n const { height, width, left, top } = input.getBoundingClientRect();\\n // create a dummy element that will be a clone of our input\\n const div = document.createElement(\\\"div\\\");\\n // get the computed style of the input and clone it onto the dummy element\\n const copyStyle = getComputedStyle(input);\\n for (const prop of copyStyle) {\\n div.style[prop] = copyStyle[prop];\\n }\\n // we need a character that will replace whitespace when filling our dummy element if it's a single line <input/>\\n const swap = \\\".\\\";\\n const inputValue =\\n input.tagName === \\\"INPUT\\\" ? input.value.replace(/ /g, swap) : input.value;\\n // set the div content to that of the textarea up until selection\\n const textContent = inputValue.substr(0, selectionPoint);\\n // set the text content of the dummy element div\\n div.textContent = textContent;\\n if (input.tagName === \\\"TEXTAREA\\\") div.style.height = \\\"auto\\\";\\n // if a single line input then the div needs to be single line and not break out like a text area\\n if (input.tagName === \\\"INPUT\\\") div.style.width = \\\"auto\\\";\\n // Apply absolute positioning to account for textarea resize, etc.\\n div.style.position = \\\"absolute\\\";\\n // create a marker element to obtain caret position\\n const span = document.createElement(\\\"span\\\");\\n // give the span the textContent of remaining content so that the recreated dummy element is as close as possible\\n span.textContent = inputValue.substr(selectionPoint) || \\\".\\\";\\n // append the span marker to the div\\n div.appendChild(span);\\n // append the dummy element to the body\\n document.body.appendChild(div);\\n // get the marker position, this is the caret position top and left relative to the input\\n const { offsetLeft: spanX, offsetTop: spanY } = span;\\n // lastly, remove that dummy element\\n // NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered\\n document.body.removeChild(div);\\n // return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input\\n let x = left + spanX;\\n let y = top + spanY;\\n const { lineHeight, paddingRight } = copyStyle;\\n x = Math.min(x - scrollLeft, left + width - parseInt(paddingRight, 10));\\n // Need to account for any scroll position for the window.\\n y =\\n Math.min(y - scrollTop, top + height - parseInt(lineHeight, 10)) +\\n window.scrollY;\\n return {\\n x,\\n y\\n };\\n};\\n\\n/**\\n * Returns the position given an inputs current selection\\n * */\\nconst getSelectionPosition = (input) => {\\n const { y: startY, x: startX } = getCaretPosition(input, \\\"selectionStart\\\");\\n const { x: endX } = getCaretPosition(input, \\\"selectionEnd\\\");\\n // Gives you a basic left position for where to put it and the starting position.\\n const x = startX + (endX - startX) / 2;\\n const y = startY;\\n return {\\n x,\\n y\\n };\\n};\\n/** End utility methods */\\n\\nconst BASIC = document.querySelector(\\\"#basic\\\");\\nconst SELECT = document.querySelector(\\\"#select\\\");\\nconst TRIGGER = document.querySelector(\\\"#trigger\\\");\\nconst POPUP = document.querySelector(\\\"[popover]\\\");\\nconst EVENTS = [\\\"keyup\\\", \\\"click\\\"];\\n\\nconst updatePopUp = ({ x, y }, html, mod) => {\\n if (!POPUP.matches(\\\":open\\\")) POPUP.showPopover();\\n POPUP.innerHTML = html;\\n POPUP.className = `popup popup--${mod}`;\\n POPUP.style = `\\n --x: ${x};\\n --y: ${y - window.scrollY};\\n `;\\n};\\nEVENTS.forEach((eventType) => {\\n BASIC.addEventListener(eventType, () =>\\n updatePopUp(getCaretPosition(BASIC), \\\"👈 Here!\\\", \\\"basic\\\")\\n );\\n});\\n\\nSELECT.addEventListener(\\\"select\\\", () => {\\n if (SELECT.selectionStart !== SELECT.selectionEnd)\\n updatePopUp(getSelectionPosition(SELECT), \\\"Here! 👇\\\", \\\"select\\\");\\n});\\nTRIGGER.addEventListener(\\\"input\\\", () => {\\n const previousCharacter = TRIGGER.value\\n .charAt(TRIGGER.selectionStart - 2)\\n .trim();\\n const character = TRIGGER.value.charAt(TRIGGER.selectionStart - 1).trim();\\n if (character === \\\"@\\\" && previousCharacter === \\\"\\\") {\\n updatePopUp(getCaretPosition(TRIGGER), \\\"Special! ✨\\\", \\\"trigger\\\");\\n }\\n if (character === \\\"\\\" && POPUP.matches(\\\":open\\\")) {\\n POPUP.hidePopover();\\n }\\n});\\n\",\"html_pre_processor\":\"none\",\"css_pre_processor\":\"none\",\"js_pre_processor\":\"none\",\"html_classes\":\"popup-support\",\"css_starter\":\"neither\",\"js_library\":null,\"created_at\":\"2022-09-03T20:30:35.326Z\",\"updated_at\":\"2022-11-04T12:13:32.092Z\",\"title\":\"25. Input anchored pop-ups\",\"description\":\"\",\"slug_hash\":\"oNdgvoR\",\"head\":\"\",\"private\":false,\"has_animation\":false,\"team_id\":0,\"css_prefix\":\"neither\",\"template\":false,\"parent_id\":0,\"comments_count\":0,\"custom_screenshot_filename\":null,\"loves_count\":0,\"pick\":false,\"popularity_score\":0,\"views_count\":0,\"pick_visible_at\":null,\"cpid\":\"0183050c-4b3e-7539-98d4-4cef2e2c8c6a\",\"is_new_editor_pen\":false,\"pen_hash\":null}","__processorsMap":{"autoprefixer":"autoprefixer-10","babel":"babel-7","coffeescript":"coffeescript-2","format-1":"format-1","flutter":"flutter-1","haml":"haml-4","less":"less-3","lint-1":"lint-1","livescript":"livescript-1","markdown":"markdown-11","postcss":"postcss-7","pug":"pug-2","sass":"sass-1","scss":"sass-1","sass-ruby-3":"sass-ruby-3","sass-ruby-compass-3":"sass-ruby-compass-3","slim":"slim-3","stylus":"stylus-0","typescript":"typescript-4","vue":"vue-2","vue3":"vue-3"},"__favicon_mask_icon":"https://cpwebassets.codepen.io/assets/favicon/logo-pin-b4b4269c16397ad2f0f7a01bcdf513a1994f4c94b8af2f191c09eb0d601762b1.svg","__favicon_shortcut_icon":"https://cpwebassets.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico","__path_to_iframe_console_runner":"https://cpwebassets.codepen.io/assets/editor/iframe/iframeConsoleRunner-6d8bf8b4b479137260842506acbb12717dace0823c023e08b96360e60b0840d9.js","__path_to_iframe_refresh_css":"https://cpwebassets.codepen.io/assets/editor/iframe/iframeRefreshCSS-44fe83e49b63affec96918c9af88c0d80b209a862cf87ac46bc933074b8c557d.js","__path_to_iframe_runtime_errors":"https://cpwebassets.codepen.io/assets/editor/iframe/iframeRuntimeErrors-4f205f2c14e769b448bcf477de2938c681660d5038bc464e3700256713ebe261.js","__path_to_processor_worker":"https://cpwebassets.codepen.io/assets/packs/router.js","__path_to_stop_execution_on_timeout":"https://cpwebassets.codepen.io/assets/common/stopExecutionOnTimeout-2c7831bb44f98c1391d6a4ffda0e1fd302503391ca806e7fcc7b9b87197aec26.js","__pen_normalize_css_url":"https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css","__pen_prefix_free_url":"https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js","__pen_reset_css_url":"https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css","__path_to_infinite_loop_detection":"https://cpwebassets.codepen.io/assets/packs/js/infiniteLoopDetection-f0d5935eb4c2121dfc41.js"}