1/* The venue card. Tapping a venue (or any map link on an iPhone or iPad) 2 opens a small sheet instead of dropping the person onto Google's web map 3 and its "upgrade to the app" interstitial (Furi, 9-3): the venue's photo 4 when it has one, its name, the address with a Copy button, any special 5 instructions ("park in the north lot"), then the ways to open it: Apple 6 Maps where it exists, Google Maps, the browser. The Apple Maps link is 7 built from the venue's address when the link carries it 8 (data-rt-map-address), else from the query in the Google URL, else from 9 the link's own words. A plain map link with no venue data only gets the 10 sheet on iOS, where the interstitial problem lives. */ 11( function () { 12 'use strict'; 13 14 var ua = navigator.userAgent || ''; 15 var isIOS = /iPhone|iPad|iPod/.test( ua ) || ( /Macintosh/.test( ua ) && navigator.maxTouchPoints > 1 ); 16 var isMac = /Macintosh/.test( ua ) && ! isIOS; 17 var isAndroid = /Android/.test( ua ); 18 var isPhone = isIOS || isAndroid; 19 20 var MAP_HOSTS = /(^|\.)(maps\.google\.[a-z.]+|google\.[a-z.]+\/maps|maps\.app\.goo\.gl|goo\.gl\/maps|maps\.apple\.com)/i; 21 22 function isMapLink( a ) { 23 var href = a.getAttribute( 'href' ) || ''; 24 if ( /^geo:/i.test( href ) ) { return true; } 25 var m = href.replace( /^https?:\/\//i, '' ); 26 return MAP_HOSTS.test( m ) || /^www\.google\.[a-z.]+\/maps/i.test( m ) || /^google\.[a-z.]+\/maps/i.test( m ); 27 } 28 29 function venueFrom( a ) { 30 var v = { 31 name: a.getAttribute( 'data-rt-map-name' ) || '', 32 address: a.getAttribute( 'data-rt-map-address' ) || '', 33 notes: a.getAttribute( 'data-rt-map-notes' ) || '', 34 image: a.getAttribute( 'data-rt-map-image' ) || '' 35 }; 36 if ( v.name || v.address ) { return v; } 37 try { 38 var u = new URL( a.href, window.location.href ); 39 var q = u.searchParams.get( 'q' ) || u.searchParams.get( 'query' ) || u.searchParams.get( 'daddr' ) || u.searchParams.get( 'destination' ) || ''; 40 if ( ! q ) { 41 var m = u.pathname.match( /\/maps\/(?:place|search)\/([^/]+)/ ); 42 if ( m ) { q = decodeURIComponent( m[ 1 ].replace( /\+/g, ' ' ) ); } 43 } 44 if ( q ) { v.name = q; return v; } 45 } catch ( e ) { /* not a URL we can read */ } 46 v.name = ( a.textContent || '' ).replace( /\s+/g, ' ' ).trim(); 47 return v; 48 } 49 50 function appleUrl( v ) { 51 var url = 'https://maps.apple.com/?'; 52 if ( v.address ) { 53 url += 'address=' + encodeURIComponent( v.address ) + ( v.name ? '&q=' + encodeURIComponent( v.name ) : '' ); 54 } else { 55 url += 'q=' + encodeURIComponent( v.name ); 56 } 57 return url; 58 } 59 60 function escapeHtml( s ) { 61 return String( s ).replace( /[&<>"']/g, function ( c ) { 62 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ c ]; 63 } ); 64 } 65 66 var sheet = null; 67 68 function close() { 69 if ( sheet ) { sheet.remove(); sheet = null; } 70 document.removeEventListener( 'keydown', onKey ); 71 } 72 function onKey( e ) { if ( e.key === 'Escape' ) { close(); } } 73 74 function copyText( text, btn ) { 75 function done( ok ) { 76 if ( ! btn ) { return; } 77 var was = btn.innerHTML; 78 btn.innerHTML = ok ? '<i class="fa-solid fa-check" aria-hidden="true"></i> Copied' : 'Could not copy'; 79 btn.classList.toggle( 'is-done', ok ); 80 setTimeout( function () { btn.innerHTML = was; btn.classList.remove( 'is-done' ); }, 1600 ); 81 } 82 function fallback( t ) { 83 var ta = document.createElement( 'textarea' ); 84 ta.value = t; 85 ta.setAttribute( 'readonly', '' ); 86 ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0'; 87 document.body.appendChild( ta ); 88 ta.select(); 89 var ok = false; 90 try { ok = document.execCommand( 'copy' ); } catch ( e ) { ok = false; } 91 ta.remove(); 92 return ok; 93 } 94 if ( navigator.clipboard && navigator.clipboard.writeText ) { 95 navigator.clipboard.writeText( text ).then( function () { done( true ); }, function () { done( fallback( text ) ); } ); 96 } else { 97 done( fallback( text ) ); 98 } 99 } 100 101 function open( a ) { 102 close(); 103 var v = venueFrom( a ); 104 var label = v.name || ( a.textContent || '' ).trim() || 'this location'; 105 var href = a.href; 106 /* Real links, tapped by a real finger: that is the one path iOS 107 reliably hands to the Maps app (maps.apple.com) or to the Google 108 Maps app when it is installed (its universal link), and that 109 Safari never treats as a blocked popup. On a desktop the Google 110 link opens a tab, since there is no app to hand off to. */ 111 var choices = ''; 112 if ( isIOS || isMac ) { 113 choices += '<a class="rt-maps-choice-btn" href="' + escapeHtml( appleUrl( v ) ) + '"' + ( isMac ? ' target="_blank" rel="noopener"' : '' ) + '><i class="fa-brands fa-apple" aria-hidden="true"></i> Open in Apple Maps</a>'; 114 } 115 choices += '<a class="rt-maps-choice-btn" href="' + escapeHtml( href ) + '"' + ( isPhone ? '' : ' target="_blank" rel="noopener"' ) + '><i class="fa-brands fa-google" aria-hidden="true"></i> Open in Google Maps</a>'; 116 if ( isPhone ) { 117 choices += '<a class="rt-maps-choice-btn rt-maps-choice-btn--web" href="' + escapeHtml( href ) + '" target="_blank" rel="noopener"><i class="fa-solid fa-globe" aria-hidden="true"></i> Open in browser</a>'; 118 } 119 sheet = document.createElement( 'div' ); 120 sheet.className = 'rt-maps-choice'; 121 sheet.setAttribute( 'role', 'dialog' ); 122 sheet.setAttribute( 'aria-modal', 'true' ); 123 sheet.setAttribute( 'aria-label', label ); 124 sheet.innerHTML = 125 '<div class="rt-maps-choice-scrim" data-close></div>' + 126 '<div class="rt-maps-choice-sheet">' + 127 '<button type="button" class="rt-maps-choice-x" data-close aria-label="Close"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button>' + 128 ( v.image ? '<img class="rt-maps-choice-photo" src="' + escapeHtml( v.image ) + '" alt="">' : '' ) + 129 '<p class="rt-maps-choice-title">' + escapeHtml( label ) + '</p>' + 130 ( v.address 131 ? '<p class="rt-maps-choice-address"><span>' + escapeHtml( v.address ) + '</span> <button type="button" class="rt-maps-choice-copy" data-copy><i class="fa-regular fa-copy" aria-hidden="true"></i> Copy</button></p>' 132 : '' ) + 133 ( v.notes 134 ? '<p class="rt-maps-choice-notes"><i class="fa-solid fa-circle-info" aria-hidden="true"></i> <span>' + escapeHtml( v.notes ).replace( /\n/g, '<br>' ) + '</span></p>' 135 : '' ) + 136 choices + 137 '<button type="button" class="rt-maps-choice-btn rt-maps-choice-btn--cancel" data-close>Cancel</button>' + 138 '</div>'; 139 document.body.appendChild( sheet ); 140 sheet.addEventListener( 'click', function ( e ) { 141 var t = e.target.closest( '[data-copy], [data-close], a.rt-maps-choice-btn' ); 142 if ( ! t ) { return; } 143 if ( t.hasAttribute( 'data-close' ) ) { close(); return; } 144 if ( t.hasAttribute( 'data-copy' ) ) { copyText( v.address, t ); return; } 145 // A choice is a plain link: let it navigate, fold the sheet after. 146 setTimeout( close, 0 ); 147 } ); 148 document.addEventListener( 'keydown', onKey ); 149 var first = sheet.querySelector( 'a.rt-maps-choice-btn' ); 150 if ( first ) { first.focus(); } 151 } 152 153 document.addEventListener( 'click', function ( e ) { 154 var a = e.target.closest( 'a[href]' ); 155 if ( ! a || ! isMapLink( a ) || a.hasAttribute( 'data-rt-map-direct' ) || a.closest( '.rt-maps-choice' ) ) { return; } 156 var hasVenue = a.hasAttribute( 'data-rt-map-name' ) || a.hasAttribute( 'data-rt-map-address' ); 157 if ( ! hasVenue && ! isIOS ) { return; } 158 if ( e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1 ) { return; } 159 e.preventDefault(); 160 open( a ); 161 }, true ); 162 163 /* For checking the sheet from a desktop browser: rtMapsChoice.open(link). */ 164 window.rtMapsChoice = { isIOS: isIOS, open: open, isMapLink: isMapLink }; 165}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.