namespace pxt . BrowserUtils { export function isIFrame ( ) { try { return window && window . self !== window . top ; } catch ( e ) { return true ; } } export function hasNavigator ( ) { return typeof navigator !== "undefined" ; } export function hasWindow ( ) { return typeof window !== "undefined" ; } export function isWindows ( ) { return hasNavigator ( ) && /(Win32|Win64|WOW64)/i . test ( navigator . platform ) ; } export function isWindows10 ( ) { return hasNavigator ( ) && /(Win32|Win64|WOW64)/i . test ( navigator . platform ) && /Windows NT 10/i . test ( navigator . userAgent ) ; } export function isMobile ( ) { return hasNavigator ( ) && /mobi/i . test ( navigator . userAgent ) ; } export function isIOS ( ) { return hasNavigator ( ) && ( /iPad|iPhone|iPod/ . test ( navigator . userAgent ) || navigator . platform === 'MacIntel' && navigator . maxTouchPoints > 1 ) ; } export function isAndroid ( ) { return hasNavigator ( ) && /android/i . test ( navigator . userAgent ) ; } export function isMac ( ) { return hasNavigator ( ) && /Mac/i . test ( navigator . platform ) ; } export function isLinux ( ) { return ! ! navigator && /Linux/i . test ( navigator . platform ) ; } export function isARM ( ) { return hasNavigator ( ) && /arm/i . test ( navigator . platform ) ; } export function isUwpEdge ( ) { return typeof window !== "undefined" && ! ! ( < any > window ) . Windows ; } export function isEdge ( ) { return hasNavigator ( ) && /Edge/i . test ( navigator . userAgent ) ; } export function isChromiumEdge ( ) { return hasNavigator ( ) && /Edg\//i . test ( navigator . userAgent ) ; } export function isIE ( ) { return hasNavigator ( ) && /Trident/i . test ( navigator . userAgent ) ; } export function isChrome ( ) { return ! isEdge ( ) && ! isIE ( ) && ! ! navigator && ( /Chrome/i . test ( navigator . userAgent ) || /Chromium/i . test ( navigator . userAgent ) ) ; } export function isSafari ( ) { return ! isChrome ( ) && ! isEdge ( ) && ! ! navigator && /(Macintosh|Safari|iPod|iPhone|iPad)/i . test ( navigator . userAgent ) ; } export function isFirefox ( ) { return ! isSafari ( ) && ! ! navigator && ( /Firefox/i . test ( navigator . userAgent ) || /Seamonkey/i . test ( navigator . userAgent ) ) ; } export function isOpera ( ) { return hasNavigator ( ) && /Opera|OPR/i . test ( navigator . userAgent ) ; } export function isMidori ( ) { return hasNavigator ( ) && /Midori/i . test ( navigator . userAgent ) ; } export function isEpiphany ( ) { return hasNavigator ( ) && /Epiphany/i . test ( navigator . userAgent ) ; } export function isTouchEnabled ( ) { return typeof window !== "undefined" && ( 'ontouchstart' in window || ( navigator && navigator . maxTouchPoints > 0 ) ) ; } export function isPxtElectron ( ) { return typeof window != "undefined" && ! ! ( window as any ) . pxtElectron ; } export function isIpcRenderer ( ) { return typeof window != "undefined" && ! ! ( window as any ) . ipcRenderer ; } export function isElectron ( ) { return isPxtElectron ( ) || isIpcRenderer ( ) ; } export let isWinRT = ( ) => false ; export function isLocalHost ( ignoreFlags ? ) { try { return typeof window !== "undefined" && /^http:\/\/(localhost|127\.0\.0\.1):\d+\// . test ( window . location . href ) && ( ignoreFlags || ! /nolocalhost=1/ . test ( window . location . href ) ) && ! ( pxt ?. webConfig ?. isStatic ) ; } catch ( e ) { return false ; } } export function isLocalHostDev ( ) { return isLocalHost ( ) && ! isElectron ( ) ; } export function isSkillmapEditor ( ) { try { return /skill(?:s?)Map=1/ . test ( window . location . href ) ; } catch ( e ) { return false ; } } export function isTabletSize ( ) { return window ?. innerWidth < pxt . BREAKPOINT_TABLET ; } export function isComputerSize ( ) { return window ?. innerWidth >= pxt . BREAKPOINT_TABLET ; } export function noSharedLocalStorage ( ) { try { return /nosharedlocalstorage/i . test ( window . location . href ) ; } catch ( e ) { return false ; } } export function useOldTutorialLayout ( ) { try { return ( /tutorialview=old/ . test ( window . location . href ) ) ; } catch ( e ) { return false ; } } export function hasPointerEvents ( ) { return typeof window != "undefined" && ! ! ( window as any ) . PointerEvent ; } export function hasSaveAs ( ) { return isEdge ( ) || isIE ( ) || isFirefox ( ) ; } export function os ( ) { if ( isWindows ( ) ) return "windows" ; else if ( isMac ( ) ) return "mac" ; else if ( isLinux ( ) && isARM ( ) ) return "rpi" ; else if ( isLinux ( ) ) return "linux" ; else return "unknown" ; } export function browser ( ) { if ( isEdge ( ) ) return "edge" ; if ( isEpiphany ( ) ) return "epiphany" ; else if ( isMidori ( ) ) return "midori" ; else if ( isOpera ( ) ) return "opera" ; else if ( isIE ( ) ) return "ie" ; else if ( isChrome ( ) ) return "chrome" ; else if ( isSafari ( ) ) return "safari" ; else if ( isFirefox ( ) ) return "firefox" ; else return "unknown" ; } export function browserVersion ( ) { if ( ! hasNavigator ( ) ) return null ; let matches = [ ] ; if ( isOpera ( ) ) { matches = /(Opera|OPR)\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } if ( isEpiphany ( ) ) { matches = /Epiphany\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } else if ( isMidori ( ) ) { matches = /Midori\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } else if ( isSafari ( ) ) { matches = /Version\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; if ( ! matches ) matches = /(Macintosh|iPod( touch)?|iPhone|iPad); (CPU|Intel).*?OS (X )?(\d+)/i . exec ( navigator . userAgent ) ; } else if ( isChrome ( ) ) { matches = /(Chrome|Chromium)\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } else if ( isEdge ( ) ) { matches = /Edge\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } else if ( isIE ( ) ) { matches = /(MSIE |rv:)([0-9\.]+)/i . exec ( navigator . userAgent ) ; } else { matches = /(Firefox|Seamonkey)\/([0-9\.]+)/i . exec ( navigator . userAgent ) ; } if ( ! matches || matches . length == 0 ) { return null ; } return matches [ matches . length - 1 ] ; } let hasLoggedBrowser = false ; export function isBrowserSupported ( ) { if ( ! navigator ) { return true ; } if ( /bot|crawler|spider|crawling/i . test ( navigator . userAgent ) ) return true ; const unsupportedBrowsers = pxt . appTarget ?. unsupportedBrowsers || ( window as any ) . pxtTargetBundle ?. unsupportedBrowsers as BrowserOptions [ ] ; if ( unsupportedBrowsers ?. some ( b => b . id == browser ( ) ) ) { return false ; } const versionString = browserVersion ( ) ; const v = parseInt ( versionString || "0" ) ; const isRecentChrome = isChrome ( ) && v >= 38 ; const isRecentFirefox = isFirefox ( ) && v >= 31 ; const isRecentEdge = isEdge ( ) ; const isRecentSafari = isSafari ( ) && v >= 9 ; const isRecentOpera = ( isOpera ( ) && isChrome ( ) ) && v >= 21 ; const isModernBrowser = isRecentChrome || isRecentFirefox || isRecentEdge || isRecentSafari || isRecentOpera ; let isSupported = isModernBrowser ; const isUnsupportedRPI = isMidori ( ) || ( isLinux ( ) && isARM ( ) && isEpiphany ( ) ) ; const isNotSupported = isUnsupportedRPI ; isSupported = isSupported && ! isNotSupported ; isSupported = isSupported || /anybrowser=(true|1)/ . test ( window . location . href ) ; if ( ! hasLoggedBrowser ) { pxt . log ( `Browser: ${ browser ( ) } ${ versionString } on ${ os ( ) }` ) ; if ( ! isSupported ) { pxt . tickEvent ( "browser.unsupported" , { useragent : navigator . userAgent } ) ; } hasLoggedBrowser = true ; } return isSupported ; } export function devicePixelRatio ( ) { if ( typeof window === "undefined" || ! window . screen ) return 1 ; const sysXDPI = ( window . screen as any ) . systemXDPI ; const logicalXDPI = ( window . screen as any ) . logicalXDPI ; if ( sysXDPI !== undefined && logicalXDPI !== undefined && sysXDPI > logicalXDPI ) { return sysXDPI / logicalXDPI ; } else if ( window && window . devicePixelRatio !== undefined ) { return window . devicePixelRatio ; } return 1 ; } export function browserDownloadBinText ( text , name , opt ? ) { return browserDownloadBase64 ( ts . pxtc . encodeBase64 ( text ) , name , opt ) ; } export function browserDownloadText ( text , name , opt ? ) { return browserDownloadBase64 ( ts . pxtc . encodeBase64 ( Util . toUTF8 ( text ) ) , name , opt ) ; } export function isBrowserDownloadInSameWindow ( ) { const windowOpen = isMobile ( ) && isSafari ( ) && ! /downloadWindowOpen=0/i . test ( window . location . href ) ; return windowOpen ; } export function isBrowserDownloadWithinUserContext ( ) { const versionString = browserVersion ( ) ; const v = parseInt ( versionString || "0" ) ; const r = ( isMobile ( ) && isSafari ( ) && v >= 11 ) || /downloadUserContext=1/i . test ( window . location . href ) ; return r ; } export function browserDownloadDataUri ( uri , name , userContextWindow ? ) { const windowOpen = isBrowserDownloadInSameWindow ( ) ; const versionString = browserVersion ( ) ; const v = parseInt ( versionString || "0" ) ; if ( windowOpen ) { if ( userContextWindow ) userContextWindow . location . href = uri ; else window . open ( uri , "_self" ) ; } else if ( pxt . BrowserUtils . isSafari ( ) && ( v < 10 || ( versionString . indexOf ( '10.0' ) == 0 ) || isMobile ( ) ) ) { let iframe = document . getElementById ( "downloader" ) as HTMLIFrameElement ; if ( ! iframe ) { pxt . debug ( 'injecting downloader iframe' ) ; iframe = document . createElement ( "iframe" ) as HTMLIFrameElement ; iframe . id = "downloader" ; iframe . style . position = "absolute" ; iframe . style . right = "0" ; iframe . style . bottom = "0" ; iframe . style . zIndex = "-1" ; iframe . style . width = "1px" ; iframe . style . height = "1px" ; document . body . appendChild ( iframe ) ; } iframe . src = uri ; } else if ( /^data:/i . test ( uri ) && ( pxt . BrowserUtils . isEdge ( ) || pxt . BrowserUtils . isIE ( ) ) ) { let byteString = atob ( uri . split ( ',' ) [ 1 ] ) ; let ia = Util . stringToUint8Array ( byteString ) ; let blob = new Blob ( [ ia ] , { type : "img/png" } ) ; window . navigator . msSaveOrOpenBlob ( blob , name ) ; } else { let link = < any > window . document . createElement ( 'a' ) ; if ( typeof link . download == "string" ) { link . href = uri ; link . download = name ; document . body . appendChild ( link ) ; link . click ( ) ; document . body . removeChild ( link ) ; } else { document . location . href = uri ; } } } export function browserDownloadUInt8Array ( buf , name , opt ? ) { return browserDownloadBase64 ( ts . pxtc . encodeBase64 ( Util . uint8ArrayToString ( buf ) ) , name , opt ) ; } export function toDownloadDataUri ( b64 , contentType ) { let protocol = "data" ; if ( isMobile ( ) && isSafari ( ) && pxt . appTarget . appTheme . mobileSafariDownloadProtocol ) protocol = pxt . appTarget . appTheme . mobileSafariDownloadProtocol ; const m = /downloadProtocol=([a-z0-9:/?]+)/i . exec ( window . location . href ) ; if ( m ) protocol = m [ 1 ] ; const dataurl = protocol + ":" + contentType + ";base64," + b64 ; return dataurl ; } export interface BrowserDownloadOptions { contentType ? : string ; userContextWindow ? : Window ; onError ? : ( err : any ) => void ; maintainObjectURL ? : boolean ; } export function browserDownloadBase64 ( b64 , name , opt = { } ) { pxt . debug ( 'trigger download' ) ; const { contentType = "application/octet-stream" , userContextWindow , onError , maintainObjectURL } = opt ; const createObjectURL = window . URL ?. createObjectURL ; const asDataUri = pxt . appTarget . appTheme . disableBlobObjectDownload ; let downloadurl ; try { if ( ! ! createObjectURL && ! asDataUri ) { const b = new Blob ( [ Util . stringToUint8Array ( atob ( b64 ) ) ] , { type : contentType } ) ; const objUrl = createObjectURL ( b ) ; browserDownloadDataUri ( objUrl , name , userContextWindow ) ; if ( maintainObjectURL ) { downloadurl = objUrl ; } else { window . setTimeout ( ( ) => window . URL . revokeObjectURL ( downloadurl ) , 0 ) ; } } else { downloadurl = toDownloadDataUri ( b64 , name ) ; browserDownloadDataUri ( downloadurl , name , userContextWindow ) ; } } catch ( e ) { if ( onError ) onError ( e ) ; pxt . debug ( "saving failed" ) ; } return downloadurl ; } export function loadImageAsync ( data ) { const img = document . createElement ( "img" ) ; return new Promise < HTMLImageElement > ( ( resolve , reject ) => { img . onload = ( ) => resolve ( img ) ; img . onerror = ( ) => resolve ( undefined ) ; img . crossOrigin = "anonymous" ; img . src = data ; } ) ; } export function loadCanvasAsync ( url ) { return loadImageAsync ( url ) . then ( img => { const canvas = document . createElement ( "canvas" ) ; canvas . width = img . width ; canvas . height = img . height ; const ctx = canvas . getContext ( "2d" ) ; ctx . drawImage ( img , 0 , 0 ) ; return canvas ; } ) ; } export function scaleImageData ( img , scale ) { const cvs = document . createElement ( "canvas" ) ; cvs . width = img . width * scale ; cvs . height = img . height * scale ; const ctx = cvs . getContext ( "2d" ) ; ctx . putImageData ( img , 0 , 0 ) ; ctx . imageSmoothingEnabled = false ; ctx . scale ( scale , scale ) ; ctx . drawImage ( cvs , 0 , 0 ) ; return ctx . getImageData ( 0 , 0 , img . width * scale , img . height * scale ) ; } export function imageDataToPNG ( img , scale = 1 ) { if ( ! img ) return undefined ; const canvas = document . createElement ( "canvas" ) ; canvas . width = img . width * scale ; canvas . height = img . height * scale ; const ctx = canvas . getContext ( "2d" ) ; ctx . putImageData ( img , 0 , 0 ) ; ctx . imageSmoothingEnabled = false ; ctx . scale ( scale , scale ) ; ctx . drawImage ( canvas , 0 , 0 ) ; return canvas . toDataURL ( "image/png" ) ; } const MAX_SCREENSHOT_SIZE = 1e6 ; export function encodeToPngAsync ( dataUri , options ? ) { const { width , height , pixelDensity = 4 , maxSize = MAX_SCREENSHOT_SIZE , text } = options || { } ; return new Promise < string > ( ( resolve , reject ) => { const img = new Image ; img . onload = function ( ) { const cvs = document . createElement ( "canvas" ) as HTMLCanvasElement ; const ctx = cvs . getContext ( "2d" ) ; cvs . width = ( width || img . width ) * pixelDensity ; cvs . height = ( height || img . height ) * pixelDensity ; if ( text ) { ctx . fillStyle = "#fff" ; ctx . fillRect ( 0 , 0 , cvs . width , cvs . height ) ; } ctx . drawImage ( img , 0 , 0 , width , height , 0 , 0 , cvs . width , cvs . height ) ; let canvasdata = cvs . toDataURL ( "image/png" ) ; while ( canvasdata . length > maxSize ) { cvs . width = ( cvs . width / 2 ) >> 0 ; cvs . height = ( cvs . height / 2 ) >> 0 ; pxt . debug ( `screenshot size ${ canvasdata . length }b, shrinking to ${ cvs . width }x${ cvs . height }` ) ; ctx . drawImage ( img , 0 , 0 , width , height , 0 , 0 , cvs . width , cvs . height ) ; canvasdata = cvs . toDataURL ( "image/png" ) ; } if ( text ) { let p = pxt . lzmaCompressAsync ( text ) . then ( blob => { const datacvs = pxt . Util . encodeBlobAsync ( cvs , blob ) ; resolve ( datacvs . toDataURL ( "image/png" ) ) ; } ) ; } else { resolve ( canvasdata ) ; } } ; img . onerror = ev => { pxt . reportError ( "png" , "png rendering failed" ) ; resolve ( undefined ) ; } ; img . src = dataUri ; } ) ; } export function resolveCdnUrl ( path ) { if ( /^https?:\/\//i . test ( path ) ) return path ; const monacoPaths = ( window as any ) . MonacoPaths || { } ; const blobPath = monacoPaths [ path ] ; if ( blobPath ) return blobPath ; if ( U . startsWith ( path , pxt . webConfig . commitCdnUrl ) ) return path ; return pxt . webConfig . commitCdnUrl + path ; } export function loadStyleAsync ( path , rtl ? ) { if ( rtl ) path = "rtl" + path ; const id = "style-" + path ; if ( document . getElementById ( id ) ) return Promise . resolve ( ) ; const url = resolveCdnUrl ( path ) ; const links = Util . toArray ( document . head . getElementsByTagName ( "link" ) ) ; const link = links . filter ( l => l . getAttribute ( "href" ) == url ) [ 0 ] ; if ( link ) { if ( ! link . id ) link . id = id ; return Promise . resolve ( ) ; } return new Promise < void > ( ( resolve , reject ) => { const el = document . createElement ( "link" ) ; el . href = url ; el . rel = "stylesheet" ; el . type = "text/css" ; el . id = id ; el . addEventListener ( 'load' , ( ) => resolve ( ) ) ; el . addEventListener ( 'error' , ( e ) => reject ( e ) ) ; document . head . appendChild ( el ) ; } ) ; } let loadScriptPromises = { } ; export function loadScriptAsync ( path ) { const url = resolveCdnUrl ( path ) ; let p = loadScriptPromises [ url ] ; if ( ! p ) { p = loadScriptPromises [ url ] = new Promise < void > ( ( resolve , reject ) => { pxt . debug ( `script: loading ${ url }` ) ; const script = document . createElement ( 'script' ) ; script . type = 'text/javascript' ; script . addEventListener ( 'load' , ( ) => resolve ( ) ) ; script . addEventListener ( 'error' , ( e ) => { delete loadScriptPromises [ url ] ; reject ( e ) ; } ) ; script . src = url ; script . async = true ; document . body . appendChild ( script ) ; } ) ; } return p ; } export function loadAjaxAsync ( url ) { return new Promise < string > ( ( resolve , reject ) => { let httprequest = new XMLHttpRequest ( ) ; httprequest . onreadystatechange = function ( ) { if ( httprequest . readyState == XMLHttpRequest . DONE ) { if ( httprequest . status == 200 ) { resolve ( httprequest . responseText ) ; } else { reject ( httprequest . status ) ; } } } ; httprequest . open ( "GET" , url , true ) ; httprequest . send ( ) ; } ) ; } let loadBlocklyPromise ; export function loadBlocklyAsync ( ) { if ( ! loadBlocklyPromise ) { pxt . debug ( `blockly: delay load` ) ; let p = pxt . BrowserUtils . loadStyleAsync ( "blockly.css" , ts . pxtc . Util . isUserLanguageRtl ( ) ) ; if ( typeof Blockly === "undefined" ) p = p . then ( ( ) => pxt . BrowserUtils . loadScriptAsync ( "pxtblockly.js" ) ) ; p = p . then ( ( ) => { pxt . debug ( `blockly: loaded` ) ; } ) ; loadBlocklyPromise = p ; } return loadBlocklyPromise ; } export function patchCdn ( url ) { if ( ! url ) return url ; const online = pxt . getOnlineCdnUrl ( ) ; if ( online ) return url . replace ( "@cdnUrl@" , online ) ; else return url . replace ( /@cdnUrl@\/(blob|commit)\/[a-f0-9]{40}\// , "./" ) ; } export function initTheme ( ) { const theme = pxt . appTarget . appTheme ; if ( theme ) { if ( theme . accentColor ) { let style = document . createElement ( 'style' ) ; style . type = 'text/css' ; style . appendChild ( document . createTextNode ( `.ui.accent { color: ${ theme . accentColor }; }
                .ui.inverted.menu .accent.active.item, .ui.inverted.accent.menu  { background-color: ${ theme . accentColor }; }` ) ) ; document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( style ) ; } } if ( Util . isUserLanguageRtl ( ) ) { pxt . debug ( "rtl layout" ) ; pxt . BrowserUtils . addClass ( document . body , "rtl" ) ; document . body . style . direction = "rtl" ; const links = Util . toArray ( document . head . getElementsByTagName ( "link" ) ) ; const semanticLink = links . filter ( l => Util . endsWith ( l . getAttribute ( "href" ) , "semantic.css" ) ) [ 0 ] ; if ( semanticLink ) { const semanticHref = semanticLink . getAttribute ( "data-rtl" ) ; if ( semanticHref ) { pxt . debug ( `swapping to ${ semanticHref }` ) ; semanticLink . setAttribute ( "href" , semanticHref ) ; } } const blocklyLink = links . filter ( l => Util . endsWith ( l . getAttribute ( "href" ) , "blockly.css" ) ) [ 0 ] ; if ( blocklyLink ) { const blocklyHref = blocklyLink . getAttribute ( "data-rtl" ) ; if ( blocklyHref ) { pxt . debug ( `swapping to ${ blocklyHref }` ) ; blocklyLink . setAttribute ( "href" , blocklyHref ) ; blocklyLink . removeAttribute ( "data-rtl" ) ; } } } } /**
     * Utility method to change the hash.
     * Pass keepHistory to retain an entry of the change in the browser history.
     */ export function changeHash ( hash , keepHistory ? ) { if ( hash . charAt ( 0 ) != '#' ) hash = '#' + hash ; if ( keepHistory ) { window . location . hash = hash ; } else { window . history . replaceState ( '' , '' , hash ) ; } } /**
     * Simple utility method to join urls.
     */ export function urlJoin ( urlPath1 , urlPath2 ) { if ( ! urlPath1 ) return urlPath2 ; if ( ! urlPath2 ) return urlPath1 ; const normalizedUrl1 = ( urlPath1 . indexOf ( '/' ) == urlPath1 . length - 1 ) ? urlPath1 . substring ( 0 , urlPath1 . length - 1 ) : urlPath1 ; const normalizedUrl2 = ( urlPath2 . indexOf ( '/' ) == 0 ) ? urlPath2 . substring ( 1 ) : urlPath2 ; return normalizedUrl1 + "/" + normalizedUrl2 ; } /**
     * Simple utility method to join multiple urls.
     */ export function joinURLs ( ... parts ) { let result ; if ( parts ) { for ( let i = 0 ; i < parts . length ; i ++ ) { result = urlJoin ( result , parts [ i ] ) ; } } return result ; } export function storageEstimateAsync ( ) { const nav = hasNavigator ( ) && < any > window . navigator ; if ( nav && nav . storage && nav . storage . estimate ) return nav . storage . estimate ( ) ; else return Promise . resolve ( { } ) ; } export const scheduleStorageCleanup = hasNavigator ( ) && ( < any > navigator ) . storage && ( < any > navigator ) . storage . estimate ? ts . pxtc . Util . throttle ( function ( ) { const MIN_QUOTA = 1000000 ; const MAX_USAGE_RATIO = 0.9 ; storageEstimateAsync ( ) . then ( estimate => { pxt . debug ( `storage estimate: ${ ( estimate . usage / estimate . quota * 100 ) >> 0 }%, ${ ( estimate . usage / 1000000 ) >> 0 }/${ ( estimate . quota / 1000000 ) >> 0 }Mb` ) ; if ( estimate . quota && estimate . usage && estimate . quota > MIN_QUOTA && ( estimate . usage / estimate . quota ) > MAX_USAGE_RATIO ) { pxt . log ( `quota usage exceeded, clearing translations` ) ; pxt . tickEvent ( 'storage.cleanup' ) ; return clearTranslationDbAsync ( ) ; } return Promise . resolve ( ) ; } ) . catch ( e => { pxt . reportException ( e ) ; } ) ; } , 10000 , false ) : ( ) => { } ; export function stressTranslationsAsync ( ) { let md = "..." ; for ( let i = 0 ; i < 16 ; ++ i ) md += md + Math . random ( ) ; console . log ( `adding entry ${ md . length * 2 } bytes` ) ; return U . delay ( 1 ) . then ( ( ) => translationDbAsync ( ) ) . then ( db => db . setAsync ( "foobar" , Math . random ( ) . toString ( ) , "" , null , undefined , md ) ) . then ( ( ) => pxt . BrowserUtils . storageEstimateAsync ( ) ) . then ( estimate => ! estimate . quota || estimate . usage / estimate . quota < 0.8 ? stressTranslationsAsync ( ) : Promise . resolve ( ) ) ; } export interface ITranslationDbEntry { id ? : string ; etag : string ; time : number ; strings ? : pxt . Map < string > ; md ? : string ; } export interface ITranslationDb { getAsync ( lang , filename , branch ) : Promise < ITranslationDbEntry > ; setAsync ( lang , filename , branch , etag , strings ? , md ? ) : Promise < void > ; clearAsync ( ) : Promise < void > ; } class MemTranslationDb implements ITranslationDb { translations : pxt . Map < ITranslationDbEntry > = { } ; key ( lang , filename , branch ) { return `${ lang }|${ filename }|${ branch || "master" }` ; } get ( lang , filename , branch ) { return this . translations [ this . key ( lang , filename , branch ) ] ; } getAsync ( lang , filename , branch ) { return Promise . resolve ( this . get ( lang , filename , branch ) ) ; } set ( lang , filename , branch , etag , time , strings ? , md ? ) { this . translations [ this . key ( lang , filename , branch ) ] = { etag , time , strings , md } ; } setAsync ( lang , filename , branch , etag , strings ? , md ? ) { this . set ( lang , filename , branch , etag , Util . now ( ) , strings ) ; return Promise . resolve ( ) ; } clearAsync ( ) { this . translations = { } ; return Promise . resolve ( ) ; } } export type IDBUpgradeHandler = ( ev : IDBVersionChangeEvent , request : IDBRequest ) => void ; export class IDBWrapper { private _db : IDBDatabase ; constructor ( private name , private version , private upgradeHandler ? , private quotaExceededHandler ? ) { } private throwIfNotOpened ( ) { if ( ! this . _db ) { throw new Error ( "Database not opened; call IDBWrapper.openAsync() first" ) ; } } private errorHandler ( err , op , reject ) { console . error ( new Error ( `${ this . name } IDBWrapper error for ${ op }: ${ err . message }` ) ) ; reject ( err ) ; if ( err . name == "QuotaExceededError" ) { pxt . log ( `storage quota exceeded...` ) ; pxt . tickEvent ( 'storage.quotaexceedederror' ) ; if ( this . quotaExceededHandler ) this . quotaExceededHandler ( ) ; } } private getObjectStore ( name , mode = "readonly" ) { this . throwIfNotOpened ( ) ; const transaction = this . _db . transaction ( [ name ] , mode ) ; return transaction . objectStore ( name ) ; } static deleteDatabaseAsync ( name ) { return new Promise ( ( resolve , reject ) => { const idbFactory = window . indexedDB || ( < any > window ) . mozIndexedDB || ( < any > window ) . webkitIndexedDB || ( < any > window ) . msIndexedDB ; const request = idbFactory . deleteDatabase ( name ) ; request . onsuccess = ( ) => resolve ( ) ; request . onerror = ( ) => reject ( request . error ) ; } ) ; } public openAsync ( ) { return new Promise ( ( resolve , reject ) => { const idbFactory = window . indexedDB || ( < any > window ) . mozIndexedDB || ( < any > window ) . webkitIndexedDB || ( < any > window ) . msIndexedDB ; const request = idbFactory . open ( this . name , this . version ) ; request . onsuccess = ( ) => { this . _db = request . result ; resolve ( ) ; } ; request . onerror = ( ) => this . errorHandler ( request . error , "open" , reject ) ; request . onupgradeneeded = ( ev ) => this . upgradeHandler ( ev , request ) ; } ) ; } public getAsync < T > ( storeName , id ) { return new Promise ( ( resolve , reject ) => { const store = this . getObjectStore ( storeName ) ; const request = store . get ( id ) ; request . onsuccess = ( ) => resolve ( request . result as T ) ; request . onerror = ( ) => this . errorHandler ( request . error , "get" , reject ) ; } ) ; } public getAllAsync < T > ( storeName ) { return new Promise ( ( resolve , reject ) => { const store = this . getObjectStore ( storeName ) ; const cursor = store . openCursor ( ) ; const data = [ ] ; cursor . onsuccess = ( ) => { if ( cursor . result ) { data . push ( cursor . result . value ) ; cursor . result . continue ( ) ; } else { resolve ( data ) ; } } ; cursor . onerror = ( ) => this . errorHandler ( cursor . error , "getAll" , reject ) ; } ) ; } public setAsync ( storeName , data ) { return new Promise ( ( resolve , reject ) => { const store = this . getObjectStore ( storeName , "readwrite" ) ; let request ; if ( typeof data . id !== "undefined" && data . id !== null ) { request = store . put ( data ) ; } else { request = store . add ( data ) ; } request . onsuccess = ( ) => resolve ( ) ; request . onerror = ( ) => this . errorHandler ( request . error , "set" , reject ) ; } ) ; } public deleteAsync ( storeName , id ) { return new Promise ( ( resolve , reject ) => { const store = this . getObjectStore ( storeName , "readwrite" ) ; const request = store . delete ( id ) ; request . onsuccess = ( ) => resolve ( ) ; request . onerror = ( ) => this . errorHandler ( request . error , "delete" , reject ) ; } ) ; } public deleteAllAsync ( storeName ) { return new Promise ( ( resolve , reject ) => { const store = this . getObjectStore ( storeName , "readwrite" ) ; const request = store . clear ( ) ; request . onsuccess = ( ) => resolve ( ) ; request . onerror = ( ) => this . errorHandler ( request . error , "deleteAll" , reject ) ; } ) ; } } class IndexedDbTranslationDb implements ITranslationDb { static TABLE = "files" ; static KEYPATH = "id" ; static dbName ( ) { return `__pxt_translations_${ pxt . appTarget . id || "" }` ; } static createAsync ( ) { function openAsync ( ) { const idbWrapper = new IDBWrapper ( IndexedDbTranslationDb . dbName ( ) , 2 , ( ev , r ) => { const db = r . result as IDBDatabase ; db . createObjectStore ( IndexedDbTranslationDb . TABLE , { keyPath : IndexedDbTranslationDb . KEYPATH } ) ; } , ( ) => { clearTranslationDbAsync ( ) . catch ( e => { } ) ; } ) ; return idbWrapper . openAsync ( ) . then ( ( ) => new IndexedDbTranslationDb ( idbWrapper ) ) ; } return openAsync ( ) . catch ( e => { console . log ( `db: failed to open database, try delete entire store...` ) ; return IDBWrapper . deleteDatabaseAsync ( IndexedDbTranslationDb . dbName ( ) ) . then ( ( ) => openAsync ( ) ) ; } ) ; } private db : IDBWrapper ; private mem : MemTranslationDb ; constructor ( db ) { this . db = db ; this . mem = new MemTranslationDb ( ) ; } getAsync ( lang , filename , branch ) { lang = ( lang || "en-US" ) . toLowerCase ( ) ; const id = this . mem . key ( lang , filename , branch ) ; const r = this . mem . get ( lang , filename , branch ) ; if ( r ) return Promise . resolve ( r ) ; return this . db . getAsync < ITranslationDbEntry > ( IndexedDbTranslationDb . TABLE , id ) . then ( ( res ) => { if ( res ) { this . mem . set ( lang , filename , branch , res . etag , res . time , res . strings ) ; return Promise . resolve ( res ) ; } return Promise . resolve ( undefined ) ; } ) . catch ( ( e ) => { return Promise . resolve ( undefined ) ; } ) ; } setAsync ( lang , filename , branch , etag , strings ? , md ? ) { lang = ( lang || "en-US" ) . toLowerCase ( ) ; const id = this . mem . key ( lang , filename , branch ) ; let time = Util . now ( ) ; this . mem . set ( lang , filename , branch , etag , time , strings , md ) ; if ( strings ) { Object . keys ( strings ) . filter ( k => ! strings [ k ] ) . forEach ( k => delete strings [ k ] ) ; } const entry = { id , etag , time , strings , md } ; return this . db . setAsync ( IndexedDbTranslationDb . TABLE , entry ) . finally ( ( ) => scheduleStorageCleanup ( ) ) . catch ( ( e ) => { console . log ( `db: set failed (${ e . message }), recycling...` ) ; return this . clearAsync ( ) ; } ) ; } clearAsync ( ) { return this . db . deleteAllAsync ( IndexedDbTranslationDb . TABLE ) . then ( ( ) => console . debug ( `db: all clean` ) ) . catch ( e => { console . error ( 'db: failed to delete all' ) ; } ) ; } } let _translationDbPromise ; export function translationDbAsync ( ) { if ( pxt . Util . isNodeJS ) return Promise . resolve ( new MemTranslationDb ( ) ) ; if ( ! _translationDbPromise ) _translationDbPromise = IndexedDbTranslationDb . createAsync ( ) . catch ( ( ) => new MemTranslationDb ( ) ) ; return _translationDbPromise ; } export function clearTranslationDbAsync ( ) { function deleteDbAsync ( ) { const n = IndexedDbTranslationDb . dbName ( ) ; return IDBWrapper . deleteDatabaseAsync ( n ) . then ( ( ) => { _translationDbPromise = undefined ; } ) . catch ( e => { pxt . log ( `db: failed to delete ${ n }` ) ; _translationDbPromise = undefined ; } ) ; } if ( ! _translationDbPromise ) return deleteDbAsync ( ) ; return _translationDbPromise . then ( db => db . clearAsync ( ) ) . catch ( e => deleteDbAsync ( ) . then ( ) ) ; } export function getTutorialCodeHash ( code ) { const input = JSON . stringify ( code ) + pxt . appTarget . versions . pxt + "_" + pxt . appTarget . versions . target ; return pxtc . U . sha256 ( input ) ; } function getTutorialInfoKey ( filename , branch ? ) { return `${ filename }|${ branch || "master" }` ; } interface TutorialInfoIndexedDbEntry { id : string ; time : number ; hash : string ; blocks : Map < number > ; snippets : Map < Map < number > > ; } export interface ITutorialInfoDb { getAsync ( filename , code , branch ? ) : Promise < TutorialInfoIndexedDbEntry > ; setAsync ( filename , snippets , code , branch ? ) : Promise < void > ; clearAsync ( ) : Promise < void > ; } class TutorialInfoIndexedDb implements ITutorialInfoDb { static TABLE = "info" ; static KEYPATH = "id" ; static dbName ( ) { return `__pxt_tutorialinfo_${ pxt . appTarget . id || "" }` ; } static createAsync ( ) { function openAsync ( ) { const idbWrapper = new pxt . BrowserUtils . IDBWrapper ( TutorialInfoIndexedDb . dbName ( ) , 2 , ( ev , r ) => { const db = r . result as IDBDatabase ; db . createObjectStore ( TutorialInfoIndexedDb . TABLE , { keyPath : TutorialInfoIndexedDb . KEYPATH } ) ; } , ( ) => { pxt . BrowserUtils . IDBWrapper . deleteDatabaseAsync ( TutorialInfoIndexedDb . dbName ( ) ) ; } ) ; return idbWrapper . openAsync ( ) . then ( ( ) => new TutorialInfoIndexedDb ( idbWrapper ) ) ; } return openAsync ( ) . catch ( e => { console . log ( `db: failed to open tutorial info database, try delete entire store...` ) ; return pxt . BrowserUtils . IDBWrapper . deleteDatabaseAsync ( TutorialInfoIndexedDb . dbName ( ) ) . then ( ( ) => openAsync ( ) ) ; } ) ; } private constructor ( protected readonly db ) { } getAsync ( filename , code , branch ? ) { const key = getTutorialInfoKey ( filename , branch ) ; const hash = getTutorialCodeHash ( code ) ; return this . db . getAsync < TutorialInfoIndexedDbEntry > ( TutorialInfoIndexedDb . TABLE , key ) . then ( ( res ) => { if ( res && res . hash == hash && ( Util . now ( ) - ( res . time || 0 ) ) < 86400000 ) { return res ; } this . db . deleteAsync ( TutorialInfoIndexedDb . TABLE , key ) ; return undefined ; } ) ; } setAsync ( filename , snippets , code , branch ? ) { pxt . perf . measureStart ( "tutorial info db setAsync" ) ; const key = getTutorialInfoKey ( filename , branch ) ; const hash = getTutorialCodeHash ( code ) ; return this . setWithHashAsync ( filename , snippets , hash ) ; } setWithHashAsync ( filename , snippets , hash , branch ? ) { pxt . perf . measureStart ( "tutorial info db setAsync" ) ; const key = getTutorialInfoKey ( filename , branch ) ; const blocks = { } ; Object . keys ( snippets ) . forEach ( hash => { Object . keys ( snippets [ hash ] ) . forEach ( blockId => { blocks [ blockId ] = snippets [ hash ] [ blockId ] ; } ) ; } ) ; const entry = { id : key , time : Util . now ( ) , hash , snippets , blocks } ; return this . db . setAsync ( TutorialInfoIndexedDb . TABLE , entry ) . then ( ( ) => { pxt . perf . measureEnd ( "tutorial info db setAsync" ) ; } ) ; } clearAsync ( ) { return this . db . deleteAllAsync ( TutorialInfoIndexedDb . TABLE ) . then ( ( ) => console . debug ( `db: all clean` ) ) . catch ( e => { console . error ( 'db: failed to delete all' ) ; } ) ; } } let _tutorialInfoDbPromise ; export function tutorialInfoDbAsync ( ) { if ( ! _tutorialInfoDbPromise ) _tutorialInfoDbPromise = TutorialInfoIndexedDb . createAsync ( ) ; return _tutorialInfoDbPromise ; } export function clearTutorialInfoDbAsync ( ) { function deleteDbAsync ( ) { const n = TutorialInfoIndexedDb . dbName ( ) ; return IDBWrapper . deleteDatabaseAsync ( n ) . then ( ( ) => { _tutorialInfoDbPromise = undefined ; } ) . catch ( e => { pxt . log ( `db: failed to delete ${ n }` ) ; _tutorialInfoDbPromise = undefined ; } ) ; } if ( ! _tutorialInfoDbPromise ) return deleteDbAsync ( ) ; return _tutorialInfoDbPromise . then ( db => db . clearAsync ( ) ) . catch ( e => deleteDbAsync ( ) . then ( ) ) ; } export interface IPointerEvents { up : string ; down : string [ ] ; move : string ; enter : string ; leave : string ; } export const pointerEvents = ( ( ) => { if ( hasPointerEvents ( ) ) { return { up : "pointerup" , down : [ "pointerdown" ] , move : "pointermove" , enter : "pointerenter" , leave : "pointerleave" } ; } else if ( isTouchEnabled ( ) ) { return { up : "mouseup" , down : [ "mousedown" , "touchstart" ] , move : "touchmove" , enter : "touchenter" , leave : "touchend" } ; } else { return { up : "mouseup" , down : [ "mousedown" ] , move : "mousemove" , enter : "mouseenter" , leave : "mouseleave" } ; } } ) ( ) ; export function getPageX ( event ) { if ( "pageX" in event ) { return ( event as MouseEvent ) . pageX ; } else { return ( event as TouchEvent ) . changedTouches [ 0 ] . pageX ; } } export function getPageY ( event ) { if ( "pageY" in event ) { return ( event as MouseEvent ) . pageY ; } else { return ( event as TouchEvent ) . changedTouches [ 0 ] . pageY ; } } export function getClientX ( event ) { if ( "clientX" in event ) { return ( event as MouseEvent ) . clientX ; } else { return ( event as TouchEvent ) . changedTouches [ 0 ] . clientX ; } } export function getClientY ( event ) { if ( "clientY" in event ) { return ( event as MouseEvent ) . clientY ; } else { return ( event as TouchEvent ) . changedTouches [ 0 ] . clientY ; } } export function popupWindow ( url , title , popUpWidth , popUpHeight ) { try { const winLeft = window . screenLeft ? window . screenLeft : window . screenX ; const winTop = window . screenTop ? window . screenTop : window . screenY ; const width = window . innerWidth || document . documentElement . clientWidth || document . body . clientWidth ; const height = window . innerHeight || document . documentElement . clientHeight || document . body . clientHeight ; const left = ( ( width / 2 ) - ( popUpWidth / 2 ) ) + winLeft ; const top = ( ( height / 2 ) - ( popUpHeight / 2 ) ) + winTop ; const popupWindow = window . open ( url , title , "width=" + popUpWidth + ", height=" + popUpHeight + ", top=" + top + ", left=" + left ) ; if ( popupWindow . focus ) { popupWindow . focus ( ) ; } return popupWindow ; } catch ( e ) { pxt . tickEvent ( 'pxt.popupError' , { url : url , msg : e . message } ) ; return null ; } } export function containsClass ( el , classes ) { return splitClasses ( classes ) . every ( cls => containsSingleClass ( el , cls ) ) ; function containsSingleClass ( el , cls ) { if ( el . classList ) { return el . classList . contains ( cls ) ; } else { const classes = ( el . className + "" ) . split ( /\s+/ ) ; return ! ( classes . indexOf ( cls ) < 0 ) ; } } } export function addClass ( el , classes ) { splitClasses ( classes ) . forEach ( cls => addSingleClass ( el , cls ) ) ; function addSingleClass ( el , cls ) { if ( el . classList ) { el . classList . add ( cls ) ; } else { const classes = ( el . className + "" ) . split ( /\s+/ ) ; if ( classes . indexOf ( cls ) < 0 ) { el . className . baseVal += " " + cls ; } } } } export function removeClass ( el , classes ) { splitClasses ( classes ) . forEach ( cls => removeSingleClass ( el , cls ) ) ; function removeSingleClass ( el , cls ) { if ( el . classList ) { el . classList . remove ( cls ) ; } else { el . className . baseVal = ( el . className + "" ) . split ( /\s+/ ) . filter ( c => c != cls ) . join ( " " ) ; } } } function splitClasses ( classes ) { return classes . split ( /\s+/ ) . filter ( s => ! ! s ) ; } export function getCookieLang ( ) { const cookiePropRegex = new RegExp ( `${ pxt . Util . escapeForRegex ( pxt . Util . pxtLangCookieId ) }=(.*?)(?:;|$)` ) ; const cookieValue = cookiePropRegex . exec ( document . cookie ) ; return cookieValue && cookieValue [ 1 ] || null ; } export function setCookieLang ( langId , docs = false ) { if ( ! pxt . Util . allLanguages [ langId ] ) { return ; } if ( langId !== getCookieLang ( ) ) { pxt . tickEvent ( `menu.lang.setcookielang` , { lang : langId , docs : `${ docs }` } ) ; const expiration = new Date ( ) ; expiration . setTime ( expiration . getTime ( ) + ( pxt . Util . langCookieExpirationDays * 24 * 60 * 60 * 1000 ) ) ; document . cookie = `${ pxt . Util . pxtLangCookieId }=${ langId }; expires=${ expiration . toUTCString ( ) }; path=/` ; } } export function cacheBustingUrl ( url ) { if ( ! url ) return url ; if ( /[?&]rnd=/ . test ( url ) ) return url ; return `${ url }${ url . indexOf ( '?' ) > 0 ? "&" : "?" }rnd=${ Math . random ( ) }` ; } }