Lurch web app user interface

source

offline-file-system.js


import { FileSystem } from './file-system.js'
import { UploadItem, downloadFile } from './upload-download.js'
import { TextInputItem, ButtonItem } from './dialog.js'

/**
 * A subclass of {@link FileSystem} that represents the file system on the
 * user's computer, which Lurch access only indirectly, by allowing the user to
 * upload files from it or download files to it.
 * 
 * Because the browser cannot access the user's file system, the `path` member
 * of file objects is not used.  (In some methods below, if that member is
 * present and is a nonempty string, then the method will fail in the way
 * documented in that method.)  Also, UIDs are not used by this file system;
 * rather, filenames are used to identify files.  UIDs are ignored in all
 * methods of this class.
 * 
 * Note that there are three abstract methods of the parent class that this
 * class does not implement, because the browser has no access to the user's
 * computer, and thus cannot implement {@link FileSystem#delete delete()},
 * {@link FileSystem#has has()}, or {@link FileSystem#list list()}.
 */
export class OfflineFileSystem extends FileSystem {

    static subclassName = FileSystem.registerSubclass(
        'your computer', OfflineFileSystem )

    /**
     * Override the base class implementation to fit the needs of this class.
     * The parent class requires the file system to be able to list its files,
     * but the offline file system cannot do so, because JavaScript in the
     * browser has no access to the user's hard drive.  Therefore, this file
     * system needs a different UI than the one the parent class provides.
     * 
     * Here, we provide two ways for a user to upload a file from their own
     * files.  This function returns a single dialog item that packages those
     * two methods into one item.  The first method is a UI element that
     * invites the user to drag and drop files onto it to upload them.  The
     * second method is a button the user can click to open a dialog for
     * browsing their computer's files and choosing one to upload.
     * 
     * If the user takes either of those actions, when this function calls the
     * `selectFile()` method in the dialog, it will pass a file object with the
     * file's contents already loaded into it, because the dialog cannot be
     * expected to read from the user's computer on its own.
     * 
     * @returns {Object[]} an array of dialog items, in this case, just one
     */
    fileChooserItems () {
        const item = new UploadItem( 'uploadedFile', 'File to open' )
        const originalOnShow = item.onShow
        item.onShow = () => {
            item.dialog.selectFile() // none yet
            originalOnShow.apply( item )
        }
        item.onFileChanged = () => {
            const { filename, content } = item.dialog.get( 'uploadedFile' )
            item.dialog.selectFile( {
                fileSystemName : this.getName(),
                filename : filename,
                contents : content
            } )
        }
        return [ item ]
    }

    /**
     * Overriding the default implementation of {@link FileSystem#fileSaverItems
     * fileSaverItems()} to return a different UI than the default:  This one
     * contains a text blank into which the user can type the filename into
     * which they want to save the document, then click a button to download
     * the file using that filename.
     */
    fileSaverItems ( fileObject ) {
        const filenameBlank = new TextInputItem( `saveFilename`, 'Filename' )
        const filenameElement = () =>
            filenameBlank.dialog.querySelector( 'input[type="text"]' )
        filenameBlank.onShow = () =>
            filenameElement().value = fileObject?.filename ||
                filenameBlank.dialog.get( 'saveFilename' ) || ''
        const downloadButton = new ButtonItem( `${name}Download`, () => {
            const newFileObject = {
                fileSystemName : this.getName(),
                filename : filenameElement().value
            }
            downloadFile( this.editor, newFileObject.filename )
            this.documentSaved( newFileObject )
            filenameBlank.dialog.close()
        } )
        return [ filenameBlank, downloadButton ]
    }

   /**
     * See the documentation of the {@link FileSystem#write write()} method in
     * the parent class for the definition of how this method must behave.  It
     * implements the requirements specified there for a file system that
     * represents the user's own computer.
     * 
     * Specifically, "saving" a file really means giving the user the
     * opportunity to download the file to their computer.  Consequently, this
     * method will fail if the given file object contains a path, since we
     * cannot dictate where the user must download it.  However, you may specify
     * a filename, and it will be the initial suggestion in the download dialog
     * that appears subsequently, but the user can change it thereafter.
     * 
     * An important limitation here is that we do not know whether the user
     * actually accepted the download or not; we simply initiate the process and
     * let the user go from there.  Consequently, this function will say that
     * the file is saved, when in reality, we know only that the saving process
     * was initiated, and the user is responsible for the rest.  They might
     * cancel the download, and yet the app (not knowing that) thinks that they
     * have saved the file, and will let them close the app without prompting
     * them to save their work, nor auto-saving it for them.
     * 
     * @param {Object} fileObject - as documented in the {@link FileSystem}
     *   class
     * @returns {Promise} as documented in {@link FileSystem#write the abstract
     *   method of the parent class}
     */
    write ( fileObject ) {
        // Case 1: Invalid input of various types
        if ( !fileObject )
            throw new Error( 'File object required for saving' )
        if ( fileObject.fileSystemName
          && fileObject.fileSystemName != this.getName() )
            throw new Error( `Wrong file system: ${fileObject.fileSystemName}` )
        if ( fileObject.path )
            throw new Error( 'OfflineFileSystem does not support paths' )
        if ( !fileObject.hasOwnProperty( 'contents' ) )
            throw new Error( 'No content to write' )
        // Case 2: Contents provided, and optionally also the filename
        if ( !fileObject.hasOwnProperty( 'filename' ) )
            fileObject.filename = 'Untitled'
        fileObject.fileSystemName = this.getName()
        downloadFile( this.editor, fileObject.filename )
        this.documentSaved( fileObject )
        return Promise.resolve( fileObject )
    }

}