Lurch web app user interface

source

load-from-url.js


/**
 * This file defines several tools for loading Lurch documents from URLs on the
 * web, specified in a dialog box or specified in the query string of the app on
 * launch.
 * 
 * @module ImportFromURL
 */

import { LurchDocument } from './lurch-document.js'
import { appURL, isValidURL, makeAbsoluteURL } from './utilities.js'
import { Dialog } from './dialog.js'

// Internal use only
// A few routines for making a tiny, mock filesystem inside localStorage
// Note that this functions very much like what's implemented in the
// BrowserFileSystem class, except those are async, and these are synchronous.
const prefix = 'lurch-temp-file-'
const allFileNames = () => {
    let result = [ ]
    for ( let i = 0 ; i < window.localStorage.length ; i++ )
        if ( window.localStorage.key( i ).startsWith( prefix ) )
            result.push( window.localStorage.key( i ).substring( prefix.length ) )
    return result
}
const fileExists = name => allFileNames().includes( name )
const readFile = name => window.localStorage.getItem( prefix + name )
const writeFile = ( name, content ) =>
    window.localStorage.setItem( prefix + name, content )
const deleteFile = name => window.localStorage.removeItem( prefix + name )

/**
 * Download a file from the web asynchronously, returning a Promise that
 * resolves when the download completes (passing the content of the file as a
 * string) or rejects if the download fails.  Note that many servers have
 * cross-origin restrictions that will prevent this, but servers whose purpose
 * is to share files publicly (e.g., GitHub) do not have this problem.  So this
 * may fail not only because the URL is invalid or the network is unreachable,
 * but also because the server may reject the request.
 * 
 * @param {string} url - URL from which to load data
 * @returns {Promise} a Promise that resolves if the file downloaded correctly
 *   and rejects if an error occurred or a response with status other than 200
 *   was returned
 * @see {@link module:ImportFromURL.loadFromQueryString loadFromQueryString()}
 */
export const loadFromURL = url => new Promise( ( resolve, reject ) => {
    const request = new XMLHttpRequest()
    request.addEventListener( 'load', event => {
        if ( event.target.status != 200 )
            reject( event.currentTarget.responseText )
        else
            resolve( event.currentTarget.responseText )
    } )
    request.addEventListener( 'error', reject )
    request.open( 'GET', url )
    request.setRequestHeader( 'Cache-Control', 'max-age=0' )
    request.send()
} )

/**
 * Check to see if the query string for the current page contains an instruction
 * for loading a file.  This can be in one of two formats.
 * 
 * If the query string contains a "load=..." parameter, treat its value as
 * either an URL (and try to load a Lurch document from that URL) or a filename
 * in the browser's local storage (and try to load a Lurch document from there).
 * Place the document in the given editor on success, and report an error with a
 * notification on failure.  The priority for how to treat the parameter is as
 * follows.
 * 
 *  1. If it is a file in the user's browser's local storage, load it and stop.
 *  1. If it is a valid URL, load it and stop.
 *  1. Try treating it as a relative URL and use the current page's URL as the
 *     base from which to make it into an absolute URL.  If that produced a
 *     valid absolute URL, load it and stop.
 *  1. Give up with an error notification.
 * 
 * If the query string contains a "data=..." parameter, treat its value as the
 * base-64 encoding of a document.  Decode it into a string containing HTML, and
 * load that document into the given editor.
 * 
 * If the query string has both parameters (which it should not), the "load=..."
 * parameter takes precedence and the "data=..." parameter is ignored.
 * 
 * @param {tinymce.Editor} editor - the TinyMCE editor instance into which to
 *   load the document specified in the query string, if there is one
 * @function
 * @see {@link module:ImportFromURL.loadFromURL loadFromURL()}
 * @see {@link module:ImportFromURL.autoOpenLink autoOpenLink()}
 */
export const loadFromQueryString = editor => {
    const params = new URL( window.location ).searchParams
    // Handle the load=... case:
    if ( params.has( 'load' ) ) {
        let source = params.get( 'load' )
        if ( fileExists( source ) ) {
            new LurchDocument( editor ).setDocument( readFile( source ) )
            if ( params.has( 'delete' ) && params.get( 'delete' ) == 'true' )
                deleteFile( source )
            // window.history.replaceState( null, null, appURL() )
            return
        }
        // If it's not a full URL, it might be a relative URL; try that.
        if ( !isValidURL( source ) )
            source = makeAbsoluteURL( source )
        // If reinterpreteing as a relative URL failed, give up now.
        if ( !isValidURL( source ) ) {
            Dialog.notify( editor, 'error', 'Not a valid file source: ' + source )
            return
        }
        // It's a valid URL, so let's try to load from it.
        loadFromURL( source )
        .then( content => {
            const LD = new LurchDocument( editor )
            LD.setDocument( content )
            LD.setFileID( source )
        } ).catch( () =>
            Dialog.notify( editor, 'error',
                `Unable to import document from ${source}` )
            // Not all servers permit downloads from other domains.
        )
        // window.history.replaceState( null, null, appURL() )
        return
    }
    // Handle the data=... case:
    if ( params.has( 'data' ) ) {
        try {
            const content = atob( decodeURIComponent( params.get( 'data' ) ) )
            new LurchDocument( editor ).setDocument( content )
            // window.history.replaceState( null, null, appURL() )
        } catch ( _ ) {
            Dialog.notify( editor, 'error', 'Could not load the encoded document.' )
        }
    }
}

/**
 * Open a new browser tab, and in that tab load another copy of the Lurch
 * application, and have that copy load into its editor the document whose
 * content is given as argument.  This is done by saving that content to a
 * temporary file in the browser's local storage, and using the query string of
 * the new tab to direct it to that temporary file, which that new tab will also
 * delete once it has read it.
 * 
 * @param {string} content - the contents of a document to display in a new
 *   window
 */
export const openFileInNewWindow = content => {
    let i = 0
    while ( fileExists( `temp_file_${i}` ) ) i++
    writeFile( `temp_file_${i}`, content )
    window.open( autoOpenLink( `temp_file_${i}`, true ), '_blank' )
}

/**
 * Create a URL that will load the Lurch app and then import a document from a
 * given source immediately.
 * 
 * @param {string} source - the URL or local filename referring to the document
 *   that should be opened ("local filename" means in the browser's local storage)
 * @param {boolean} [andDelete=false] - whether to delete the file after opening
 *   it (which makes sense only for local files, not URLs)
 * @returns {string} a URL that points to this Lurch app, but with the given
 *   source embedded in the query string, as an instruction to open it
 * @function
 * @see {@link module:ImportFromURL.loadFromQueryString loadFromQueryString()}
 * @see {@link module:Utilities.appURL appURL()}
 */
export const autoOpenLink = ( source, andDelete = false ) =>
    appURL() + '?load=' + encodeURIComponent( source )
             + ( andDelete ? '&delete=true' : '' )

export default { loadFromQueryString, loadFromURL }