UNPKG

should-handle-link

Version:

A utility to help libraries and frameworks handle `<a>` clicks, properly handling all the default behavior that comes with clicking links (ctrl+click, cmd+click, etc).

157 lines (138 loc) 4.52 kB
export function getAnchor(event) { /** * Using composed path in case the link is removed from the DOM * before the event handler evaluates. * * (Which can happen in the event of links in dropdowns that auto-close) */ let composedPath = event.composedPath(); /** * Example 1: * - button * - div * - article * - section * - body * - html * - HTMLDocument * - Window * * Example 2: * - svg * - span * - a # and the rest is skipped * - article * - section * - body * - html * - HTMLDocument * - Window */ for (let element of composedPath) { if (!element.nodeName) { return; } if (element.nodeName.toUpperCase() === 'A') { return element; } } } /** * Returns `true` if the link should be handled by the Ember router * Returns `false` if the link should be handled by the browser */ export function shouldHandle(href, element, event, ignore = []) { if (!element) return false; /** * If we don't have an href, the <a> is invalid. * If you're debugging your code and end up finding yourself * early-returning here, please add an href ;) */ if (!element.href) return false; /** * This is partially an escape hatch, but any time target is set, * we are usually wanting to escape the behavior of single-page-apps. * * Some folks desire to have in-SPA links, but still do native browser behavior * (which for the case of SPAs is a full page refresh) * but they can set target="_self" to get that behavior back if they want. * * I expect that this'll be a super edge case, because the whole goal of * "proper links" is to do what is expected, always -- for in-app SPA links * as well as external, cross-domain links */ if (element.target) return false; /** * rel="external" indicates that the hyperlink leads to a resource outside * the site of the current page; that is, following the link will make * the user leave the site. * https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types */ if (element.rel === 'external') return false; /** * Clicking <a href="..." download> should open up browser's download dialog */ if (element.hasAttribute('download')) return false; /** * If the click is not a "left click" we don't want to intercept the event. * This allows folks to * - middle click (usually open the link in a new tab) * - right click (usually opens the context menu) */ if (event.button !== 0) return false; /** * for MacOS users, this default behavior opens the link in a new tab */ if (event.metaKey) return false; /** * for for everyone else, this default behavior opens the link in a new tab */ if (event.ctrlKey) return false; /** * The default behavior here downloads the link content */ if (event.altKey) return false; /** * The default behavior here opens the link in a new window */ if (event.shiftKey) return false; /** * If another event listener called event.preventDefault(), we don't want to proceed. */ if (event.defaultPrevented) return false; /** * The href includes the protocol/host/etc * In order to not have the page look like a full page refresh, * we need to chop that "origin" off, and just use the path */ let url = new URL(element.href); let location = new URL(href); /** * If the domains are different, we want to fall back to normal link behavior * */ if (location.origin !== url.origin) return false; /** * Hash-only links are handled by the browser, except for the case where the * hash is being removed entirely, e.g. /foo#bar to /foo. In that case the * browser will do a full page refresh which is not what we want. Instead * we let the router handle such transitions. The current implementation of * the Ember router will skip the transition in this case because the path * is the same. */ let [prehash, posthash] = url.href.split('#'); if (posthash !== undefined && prehash === location.href.split('#')[0]) { return false; } /** * We can optionally declare some paths as ignored, * or "let the browser do its default thing, * because there is other server-based routing to worry about" * * `ignore` is an array of either: * - string elements representing explicit paths * - RegExp elements to match parts of URL */ if (ignore.some((element) => url.pathname.match(element))) return false; return true; }