├── .gitignore ├── README.md ├── elm-package.json ├── example ├── .bootstraprc ├── README.md ├── elm-package.json ├── package.json ├── server │ ├── index.js │ ├── package.json │ ├── test.html │ └── uploads │ │ └── .gitkeep ├── src │ ├── Main.elm │ ├── index.ejs │ ├── index.js │ └── styles.scss └── webpack.config.js └── src ├── FileReader.elm ├── FileReader └── FileDrop.elm └── Native └── FileReader.js /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | ignore 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 0.19 2 | 3 | With the arrival of 0.19, the Javascript code in this package is unusable. I believe that some of the functionality in https://github.com/elm/file may provide an approved alternative, but I have not had cause to access files in 0.19 yet. So this repo is effectively archived pending the emergence of issues that can be solved in the current context. 4 | 5 | # Read files into Elm apps 6 | 7 | There are two basic ways to read files from the host operating system into the browser (either to view directly or to upload to a server): 8 | 9 | - HTML5 FileReader bindings 10 | - drag 'n drop into a target DOM element 11 | 12 | This helps with both methods and, in particular provides native bindings for the [HTML5 file reader control](http://www.w3.org/TR/html-markup/input.file.html) (the JS `FileReader` class). 13 | 14 | FileReader has three main methods (see [MDN](https://developer.mozilla.org/en/docs/Web/API/FileReader)): 15 | 16 | FileReaderInstance.readAsText(); 17 | FileReaderInstance.readAsArrayBuffer(); 18 | FileReaderInstance.readAsDataURL(); 19 | 20 | The module also provides helper Elm json decoders for `change` events on `` and for relevant `drag` and `drop` events. 21 | 22 | ## Installation 23 | 24 | Due to the native (kernel) code, it is not possible to install directly using `elm-package install`. So you need on eof the following methods 25 | 26 | ### [elm-github-install](https://github.com/gdotdesign/elm-github-install) 27 | 28 | A tool to install native-code based Elm libraries 29 | 30 | Change you package.json file to readAsDataURL 31 | 32 | ``` 33 | "dependencies": { 34 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 35 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 36 | [....], 37 | "simonh1000/file-reader": "1.6.0 <= v < 2.0.0" 38 | }, 39 | ``` 40 | 41 | ``` 42 | (sudo) gem install elm_install 43 | elm-install 44 | ``` 45 | 46 | ### Manually 47 | 48 | You can just copy the src code into your own source tree. Note in particular that you then need to add `"native-modules": true,` to your elm-package.json file as is done in the examples. 49 | 50 | ## Example 51 | 52 | The example provides a fully worked through file upload interface, taking advantage of most of the key functions in this library. If you want to try it, you must *first* edit src/Native/FileReader.js to swap the comments in the first two lines. 53 | 54 | ## Changelog 55 | 56 | 1.6: add drag and drop support 57 | 1.5: no new functionality 58 | 1.4: add `onFileChange` - an event handler for an `` 59 | 1.3.1: add rawBody 60 | 1.1: Update to 0.18. An additional native code function has been added to enable multipart form uploads of binary data - see http://simonh1000.github.io/2016/12/elm-s3-uploads/ for an example of its usage. 61 | 62 | ## Disclaimer 63 | 64 | This project began in the time of 0.16 and was submitted as a library including "native code" to the elm-package manager by [Daniel Bachler](https://github.com/danyx23) and myself. It was never OKed, as was the case with all native code at that time. The native code was subsequently updated by [WangBoxue](https://github.com/WangBoxue) to work with 0.17, and I ensured it worked for 0.18. 65 | 66 | In theory Evan plans to make all browser web APIs available to Elm users, and when that includes FileReader, this library will remove the native code. The official guidance therefore is to use a port rather than the native code in this library, but you can readily verify that the native code here covers the absolute minimum to expose the APIs, so I believe this will not jeopardise the stability of your Elm apps. 67 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.6.1", 3 | "summary": "Elm bindings for HTML5 FileReader API", 4 | "repository": "https://github.com/simonh1000/file-reader.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "FileReader", 11 | "FileReader.FileDrop" 12 | ], 13 | "native-modules": true, 14 | "dependencies": { 15 | "danyx23/elm-mimetype": "4.0.0 <= v < 5.0.0", 16 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 17 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 18 | "elm-lang/http": "1.0.0 <= v < 2.0.0" 19 | }, 20 | "elm-version": "0.18.0 <= v < 0.19.0" 21 | } 22 | -------------------------------------------------------------------------------- /example/.bootstraprc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 4 7 | 8 | # If Bootstrap version 4 is used - turn on/off flexbox model 9 | # useFlexbox: true 10 | # preBootstrapCustomizations: ./src/precustomisation.scss 11 | # Webpack loaders, order matters 12 | styleLoaders: 13 | - style 14 | - css 15 | # - postcss 16 | - sass 17 | 18 | # Extract styles to stand-alone css file 19 | # Different settings for different environments can be used, 20 | # It depends on value of NODE_ENV environment variable 21 | # This param can also be set in webpack config: 22 | # entry: 'bootstrap-loader/extractStyles' 23 | extractStyles: false 24 | # env: 25 | # development: 26 | # extractStyles: false 27 | # production: 28 | # extractStyles: true 29 | 30 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 31 | # Thus original Bootstrap variables can depend on values from here. All the bootstrap 32 | # variables are configured with !default, and thus, if you define the variable here, then 33 | # that value is used, rather than the default. However, many bootstrap variables are derived 34 | # from other bootstrap variables, and thus, you want to set this up before we load the 35 | # official bootstrap versions. 36 | # For example, _variables.scss contains: 37 | # $input-color: $gray !default; 38 | # This means you can define $input-color before we load _variables.scss 39 | # preBootstrapCustomizations: ./app/styles/bootstrap/pre-customizations.scss 40 | 41 | # This gets loaded after bootstrap/variables is loaded and before bootstrap is loaded. 42 | # A good example of this is when you want to override a bootstrap variable to be based 43 | # on the default value of bootstrap. This is pretty specialized case. Thus, you normally 44 | # just override bootrap variables in preBootstrapCustomizations so that derived 45 | # variables will use your definition. 46 | # 47 | # For example, in _variables.scss: 48 | # $input-height: (($font-size-base * $line-height) + ($input-padding-y * 2) + ($border-width * 2)) !default; 49 | # This means that you could define this yourself in preBootstrapCustomizations. Or you can do 50 | # this in bootstrapCustomizations to make the input height 10% bigger than the default calculation. 51 | # Thus you can leverage the default calculations. 52 | # $input-height: $input-height * 1.10; 53 | # bootstrapCustomizations: ./app/styles/bootstrap/customizations.scss 54 | 55 | # Import your custom styles here. You have access to all the bootstrap variables. If you require 56 | # your sass files separately, you will not have access to the bootstrap variables, mixins, clases, etc. 57 | # Usually this endpoint-file contains list of @imports of your application styles. 58 | # appStyles: ./app/styles/app.scss 59 | 60 | ### Bootstrap styles 61 | styles: 62 | 63 | # Mixins 64 | mixins: true 65 | 66 | # Reset and dependencies 67 | # normalize: true 68 | print: true 69 | 70 | # Core CSS 71 | reboot: true 72 | type: true 73 | images: true 74 | code: true 75 | grid: true 76 | tables: true 77 | forms: true 78 | buttons: true 79 | 80 | # Components 81 | transitions: true 82 | dropdown: true 83 | button-group: true 84 | input-group: true 85 | custom-forms: true 86 | nav: true 87 | navbar: true 88 | card: true 89 | breadcrumb: true 90 | pagination: true 91 | jumbotron: true 92 | alert: true 93 | progress: true 94 | media: true 95 | list-group: true 96 | # responsive-embed: true 97 | close: true 98 | badge: true 99 | 100 | # Components w/ JavaScript 101 | # modal: true 102 | # tooltip: true 103 | # popover: true 104 | # carousel: true 105 | 106 | # Utility classes 107 | utilities: true 108 | 109 | ### Bootstrap scripts 110 | # scripts: 111 | # alert: true 112 | # button: true 113 | # carousel: true 114 | # collapse: true 115 | # dropdown: true 116 | # modal: true 117 | # popover: true 118 | # scrollspy: true 119 | # tab: true 120 | # tooltip: true 121 | # util: true 122 | # 123 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Elm File-Reader example 2 | 3 | 1. To use you need to edit /src/Native/FileReader.js and change the comments on the first two lines 4 | 2. Install and run using instructions below 5 | 3. Start server 6 | ``` 7 | cd server 8 | npm install 9 | node index 10 | ``` 11 | 4. Open http://localhost:3000 and try uploading a TEXT file 12 | 13 | 14 | ## Installation 15 | 16 | Based on https://github.com/simonh1000/elm-fullstack-starter 17 | 18 | With npm 19 | 20 | ```sh 21 | $ git clone git@github.com:simonh1000/elm-webpack-starter.git new-project 22 | $ cd new-project 23 | $ npm install 24 | $ npm run dev 25 | ``` 26 | 27 | With yarn 28 | ```sh 29 | $ git clone git@github.com:simonh1000/elm-webpack-starter.git new-project 30 | $ cd new-project 31 | $ yarn 32 | $ yarn dev 33 | ``` 34 | -------------------------------------------------------------------------------- /example/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "elm-program", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "ISC", 6 | "source-directories": [ 7 | "src", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "native-modules": true, 12 | "dependencies": { 13 | "danyx23/elm-mimetype": "4.0.0 <= v < 5.0.0", 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 15 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 16 | "elm-lang/http": "1.0.0 <= v < 2.0.0" 17 | }, 18 | "elm-version": "0.18.0 <= v < 0.19.0" 19 | } 20 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Simon Hampton", 3 | "name": "elm-webpack-starter", 4 | "version": "1.0.0", 5 | "description": "Elm starter with Webpack 3 hot-loading", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "elm-test", 9 | "start": "npm run dev", 10 | "dev": "webpack-dev-server --hot --port 3000", 11 | "build": "webpack", 12 | "prod": "webpack -p", 13 | "postinstall": "elm-package install -y && elm-test init" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/simonh1000/elm-webpack-starter.git" 18 | }, 19 | "license": "MIT", 20 | "devDependencies": { 21 | "babel-core": "^6.25.0", 22 | "babel-loader": "^7.1.1", 23 | "babel-preset-env": "^1.6.0", 24 | "bootstrap-loader": "^2.2.0", 25 | "chokidar-cli": "^1.2.0", 26 | "clean-webpack-plugin": "^0.1.16", 27 | "copy-webpack-plugin": "^4.0.1", 28 | "css-loader": "^0.28.4", 29 | "elm-hot-loader": "^0.5.4", 30 | "elm-webpack-loader": "^4.3.1", 31 | "extract-text-webpack-plugin": "^3.0.0", 32 | "file-loader": "^0.11.2", 33 | "html-webpack-plugin": "^2.30.1", 34 | "node-sass": "^4.5.3", 35 | "postcss-loader": "^2.0.6", 36 | "resolve-url-loader": "^2.1.0", 37 | "sass-loader": "^6.0.6", 38 | "style-loader": "^0.18.2", 39 | "url-loader": "^0.5.9", 40 | "webpack": "^3.5.3", 41 | "webpack-dev-server": "^2.7.1", 42 | "webpack-merge": "^4.1.0" 43 | }, 44 | "dependencies": { 45 | "bootstrap": "^4.0.0-beta", 46 | "loglevel": "^1.4.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/server/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | var express = require('express') 5 | var multer = require('multer') 6 | var upload = multer({ dest: 'uploads/' }); 7 | var cors = require('cors'); 8 | 9 | var app = express(); 10 | app.use(cors()); 11 | 12 | app.get('/', function (req, res) { 13 | // res.send('Hello World!') 14 | res.sendFile(__dirname+'/index.html'); 15 | }) 16 | 17 | app.get('/test', function (req, res) { 18 | res.sendFile(__dirname+'/test.html'); 19 | }) 20 | 21 | app.post('/upload', upload.single('upload'), function (req, res, next) { 22 | console.log(req.file); 23 | console.log(req.body); 24 | // fs.writeFileSync(req.body, path.join(__dirname, 'uploads', req.file.originalname)); 25 | res.send({"message": req.file.filename}); 26 | }) 27 | 28 | app.listen(5000, function () { 29 | console.log('Example app listening on port 5000!') 30 | }); 31 | -------------------------------------------------------------------------------- /example/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multipartserver", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.14.0", 13 | "multer": "^1.2.0" 14 | }, 15 | "devDependencies": { 16 | "cors": "^2.8.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/server/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 |

Test

15 |
16 | 17 |
18 | 19 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /example/server/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonh1000/file-reader/5291df8c67c05c6952574522ed7da07d583948d5/example/server/uploads/.gitkeep -------------------------------------------------------------------------------- /example/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (onClick) 6 | import Json.Decode as Json exposing (Value) 7 | import Task 8 | import Http 9 | import FileReader exposing (NativeFile) 10 | import FileReader.FileDrop as DZ 11 | 12 | 13 | type alias Model = 14 | { file : Maybe NativeFile 15 | , dragHovering : Int 16 | , content : String 17 | } 18 | 19 | 20 | init : Model 21 | init = 22 | { file = Nothing 23 | , dragHovering = 0 24 | , content = "" 25 | } 26 | 27 | 28 | type Msg 29 | = OnDragEnter Int 30 | | OnDrop (List NativeFile) 31 | | StartUpload 32 | | OnFileContent (Result FileReader.Error String) 33 | | PostResult (Result Http.Error Value) 34 | | NoOp 35 | 36 | 37 | update : Msg -> Model -> ( Model, Cmd Msg ) 38 | update message model = 39 | case message of 40 | OnDragEnter inc -> 41 | ( { model | dragHovering = model.dragHovering + inc }, Cmd.none ) 42 | 43 | OnDrop file -> 44 | case file of 45 | -- Only handling case of a single file 46 | [ f ] -> 47 | ( { model | file = Just f, dragHovering = 0 }, getFileContents f ) 48 | 49 | _ -> 50 | ( { model | dragHovering = 0 }, Cmd.none ) 51 | 52 | OnFileContent res -> 53 | case res of 54 | Ok content -> 55 | ( { model | content = content }, Cmd.none ) 56 | 57 | Err err -> 58 | Debug.crash (toString err) 59 | 60 | StartUpload -> 61 | ( model, model.file |> Maybe.map sendFileToServer |> Maybe.withDefault Cmd.none ) 62 | 63 | PostResult res -> 64 | case Debug.log "PostResult" res of 65 | _ -> 66 | ( model, Cmd.none ) 67 | 68 | _ -> 69 | ( model, Cmd.none ) 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | let 75 | dzAttrs_ = 76 | DZ.dzAttrs (OnDragEnter 1) (OnDragEnter -1) NoOp OnDrop 77 | 78 | dzClass = 79 | if model.dragHovering > 0 then 80 | class "drop-zone active" :: dzAttrs_ 81 | else 82 | class "drop-zone" :: dzAttrs_ 83 | in 84 | div [ class "panel" ] <| 85 | [ h1 [] [ text "File Reader library example" ] 86 | , p [] [ text "Drag n Drop file below or use the file dialog to load file" ] 87 | , div dzClass 88 | [ input 89 | [ type_ "file" 90 | , FileReader.onFileChange OnDrop 91 | , multiple False 92 | ] 93 | [] 94 | ] 95 | , case model.file of 96 | Just nf -> 97 | div [] 98 | [ span [] [ text nf.name ] 99 | , button [ onClick StartUpload ] [ text "Upload" ] 100 | , div [] [ small [] [ text model.content ] ] 101 | ] 102 | 103 | Nothing -> 104 | text "" 105 | ] 106 | 107 | 108 | 109 | -- 110 | 111 | 112 | getFileContents : NativeFile -> Cmd Msg 113 | getFileContents nf = 114 | FileReader.readAsTextFile nf.blob 115 | |> Task.attempt OnFileContent 116 | 117 | 118 | sendFileToServer : NativeFile -> Cmd Msg 119 | sendFileToServer nf = 120 | let 121 | body = 122 | Http.multipartBody 123 | [ Http.stringPart "part1" nf.name 124 | , FileReader.filePart "upload" nf 125 | ] 126 | in 127 | Http.post "http://localhost:5000/upload" body Json.value 128 | |> Http.send PostResult 129 | 130 | 131 | 132 | -- 133 | 134 | 135 | main : Program Never Model Msg 136 | main = 137 | Html.program 138 | { init = ( init, Cmd.none ) 139 | , update = update 140 | , view = view 141 | , subscriptions = always Sub.none 142 | } 143 | -------------------------------------------------------------------------------- /example/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm hotloading dev environment 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('bootstrap-loader'); 4 | require("./styles.scss"); 5 | 6 | var Elm = require('./Main'); 7 | var app = Elm.Main.fullscreen(); 8 | -------------------------------------------------------------------------------- /example/src/styles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #ddd; 3 | margin-top: 20px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | .panel { 8 | background-color: whites; 9 | padding: 20px; 10 | width: 500px; 11 | } 12 | .drop-zone { 13 | border: 3px dashed #444; 14 | height: 300px; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | &.active { 19 | border-color: #dd9999; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | const webpack = require('webpack'); 3 | var merge = require('webpack-merge'); 4 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | var HTMLWebpackPlugin = require('html-webpack-plugin'); 6 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | 8 | var TARGET_ENV = process.env.npm_lifecycle_event === 'prod' ? 'production' : 'development'; 9 | var filename = (TARGET_ENV == 'production') ? '[name]-[hash].js' : 'index.js'; 10 | 11 | var common = { 12 | entry: './src/index.js', 13 | output: { 14 | path: path.join(__dirname, "dist"), 15 | // webpack -p automatically adds hash when building for production 16 | filename: filename 17 | }, 18 | plugins: [new HTMLWebpackPlugin({ 19 | // using .ejs prevents other loaders causing errors 20 | template: 'src/index.ejs', 21 | // inject details of output file at end of body 22 | inject: 'body' 23 | })], 24 | resolve: { 25 | modules: [ 26 | path.join(__dirname, "src"), 27 | "node_modules" 28 | ], 29 | extensions: ['.js', '.elm', '.scss', '.png'] 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.html$/, 35 | exclude: /node_modules/, 36 | loader: 'file-loader?name=[name].[ext]' 37 | }, { 38 | test: /\.js$/, 39 | exclude: /node_modules/, 40 | use: { 41 | loader: 'babel-loader', 42 | options: { 43 | // env: automatically determines the Babel plugins you need based on your supported environments 44 | presets: ['env'] 45 | } 46 | } 47 | }, { 48 | test: /\.scss$/, 49 | exclude: [ 50 | /elm-stuff/, /node_modules/ 51 | ], 52 | loaders: ["style-loader", "css-loader", "sass-loader"] 53 | }, { 54 | test: /\.css$/, 55 | exclude: [ 56 | /elm-stuff/, /node_modules/ 57 | ], 58 | loaders: ["style-loader", "css-loader"] 59 | }, { 60 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 61 | exclude: [ 62 | /elm-stuff/, /node_modules/ 63 | ], 64 | loader: "url-loader", 65 | options: { 66 | limit: 10000, 67 | mimetype: "application/font-woff" 68 | } 69 | }, { 70 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 71 | exclude: [ 72 | /elm-stuff/, /node_modules/ 73 | ], 74 | loader: "file-loader" 75 | }, { 76 | test: /\.(jpe?g|png|gif|svg)$/i, 77 | loader: 'file-loader' 78 | } 79 | ] 80 | } 81 | } 82 | 83 | if (TARGET_ENV === 'development') { 84 | console.log('Building for dev...'); 85 | module.exports = merge(common, { 86 | plugins: [ 87 | // Suggested for hot-loading 88 | new webpack.NamedModulesPlugin(), 89 | // Prevents compilation errors causing the hot loader to lose state 90 | new webpack.NoEmitOnErrorsPlugin() 91 | ], 92 | module: { 93 | rules: [ 94 | { 95 | test: /\.elm$/, 96 | exclude: [ 97 | /elm-stuff/, /node_modules/ 98 | ], 99 | use: [ 100 | { 101 | loader: "elm-hot-loader" 102 | }, { 103 | loader: "elm-webpack-loader", 104 | // add Elm's debug overlay to output 105 | options: { 106 | debug: true 107 | } 108 | } 109 | ] 110 | } 111 | ] 112 | }, 113 | devServer: { 114 | inline: true, 115 | stats: 'errors-only', 116 | contentBase: path.join(__dirname, "src/assets"), 117 | // For SPAs: serve index.html in place of 404 responses 118 | historyApiFallback: true 119 | } 120 | }); 121 | } 122 | 123 | if (TARGET_ENV === 'production') { 124 | console.log('Building for prod...'); 125 | module.exports = merge(common, { 126 | plugins: [ 127 | // Delete everything from output directory and report to user 128 | new CleanWebpackPlugin(['dist'], { 129 | root: __dirname, 130 | exclude: [], 131 | verbose: true, 132 | dry: false 133 | }), 134 | new CopyWebpackPlugin([ 135 | { 136 | from: 'src/assets' 137 | } 138 | ]), 139 | // TODO update to version that handles => 140 | new webpack.optimize.UglifyJsPlugin() 141 | ], 142 | module: { 143 | rules: [ 144 | { 145 | test: /\.elm$/, 146 | exclude: [ 147 | /elm-stuff/, /node_modules/ 148 | ], 149 | use: [ 150 | { 151 | loader: "elm-webpack-loader" 152 | } 153 | ] 154 | } 155 | ] 156 | } 157 | }); 158 | } 159 | -------------------------------------------------------------------------------- /src/FileReader.elm: -------------------------------------------------------------------------------- 1 | module FileReader 2 | exposing 3 | ( FileRef 4 | , FileContentArrayBuffer 5 | , FileContentDataUrl 6 | , NativeFile 7 | , Error(..) 8 | , onFileChange 9 | , readAsTextFile 10 | , readAsArrayBuffer 11 | , readAsDataUrl 12 | , prettyPrint 13 | , parseSelectedFiles 14 | , parseDroppedFiles 15 | , filePart 16 | , rawBody 17 | ) 18 | 19 | {-| Elm bindings for the main [HTML5 FileReader APIs](https://developer.mozilla.org/en/docs/Web/API/FileReader): 20 | 21 | FileReaderInstance.readAsText(); 22 | FileReaderInstance.readAsArrayBuffer(); 23 | FileReaderInstance.readAsDataURL(); 24 | 25 | The module also provides helper Json Decoders for the files values on 26 | `` `change` events, and on `drop` events, 27 | together with a set of examples. 28 | 29 | 30 | # API functions 31 | 32 | @docs readAsTextFile, readAsArrayBuffer, readAsDataUrl 33 | 34 | 35 | # Multi-part support 36 | 37 | @docs filePart, rawBody 38 | 39 | 40 | # Helper aliases 41 | 42 | @docs NativeFile, FileRef, FileContentArrayBuffer, FileContentDataUrl, Error, prettyPrint 43 | 44 | 45 | # Helper Json Decoders 46 | 47 | @docs parseSelectedFiles, parseDroppedFiles 48 | 49 | 50 | # Helpers: Html event handlers 51 | 52 | @docs onFileChange 53 | 54 | -} 55 | 56 | import Html exposing (Attribute) 57 | import Html.Events exposing (on) 58 | import Native.FileReader 59 | import Http exposing (Part, Body) 60 | import Task exposing (Task, fail) 61 | import Json.Decode as Json exposing (Decoder, Value) 62 | import MimeType 63 | 64 | 65 | {-| Helper type for interpreting the Files event value from Input and drag 'n drop. 66 | The first three elements are useful meta data, while the fourth is the handle 67 | needed to read the file. 68 | 69 | type alias NativeFile = 70 | { name : String 71 | , size : Int 72 | , mimeType : Maybe MimeType.MimeType 73 | , blob : Value 74 | } 75 | 76 | -} 77 | type alias NativeFile = 78 | { name : String 79 | , size : Int 80 | , mimeType : Maybe MimeType.MimeType 81 | , blob : FileRef 82 | } 83 | 84 | 85 | {-| A FileRef (or Blob) is a Elm Json Value. 86 | -} 87 | type alias FileRef = 88 | Value 89 | 90 | 91 | {-| An ArrayBuffer is a Elm Json Value. 92 | -} 93 | type alias FileContentArrayBuffer = 94 | Value 95 | 96 | 97 | {-| A DataUrl is an Elm Json Value. 98 | -} 99 | type alias FileContentDataUrl = 100 | Value 101 | 102 | 103 | {-| FileReader can fail in the following cases: 104 | 105 | - the File reference / blob passed in was not valid 106 | - an native error occurs during file reading 107 | - readAsTextFile is passed a FileRef that does not have a text format (unrecognised formats are read) 108 | 109 | -} 110 | type Error 111 | = NoValidBlob 112 | | ReadFail 113 | | NotTextFile 114 | 115 | 116 | {-| Takes a "File" or "Blob" JS object as a Json.Value. If the File is a text 117 | format, returns a task that reads the file as a text file. The Success value is 118 | represented as a String to Elm. 119 | 120 | readAsTextFile ref 121 | 122 | -} 123 | readAsTextFile : FileRef -> Task Error String 124 | readAsTextFile fileRef = 125 | if isTextFile fileRef then 126 | Native.FileReader.readAsTextFile fileRef 127 | else 128 | fail NotTextFile 129 | 130 | 131 | {-| Takes a "File" or "Blob" JS object as a Json.Value 132 | and starts a task to read the contents as an ArrayBuffer. 133 | The ArrayBuffer value returned in the Success case of the Task will 134 | be represented as a Json.Value to Elm. 135 | 136 | readAsArrayBuffer ref 137 | 138 | -} 139 | readAsArrayBuffer : FileRef -> Task Error FileContentArrayBuffer 140 | readAsArrayBuffer fileRef = 141 | Native.FileReader.readAsArrayBuffer fileRef 142 | 143 | 144 | {-| Takes a "File" or "Blob" JS object as a Json.Value 145 | and starts a task to read the contents as an DataURL (so it can 146 | be assigned to the src property of an img e.g.). 147 | The DataURL value returned in the Success case of the Task will 148 | be represented as a Json.Value to Elm. 149 | 150 | readAsDataUrl ref 151 | 152 | -} 153 | readAsDataUrl : FileRef -> Task Error FileContentDataUrl 154 | readAsDataUrl fileRef = 155 | Native.FileReader.readAsDataUrl fileRef 156 | 157 | 158 | {-| Creates an Http.Part from a NativeFile, to support uploading of binary files using multipart. 159 | -} 160 | filePart : String -> NativeFile -> Part 161 | filePart name nf = 162 | Native.FileReader.filePart name nf.blob 163 | 164 | 165 | {-| Creates an Http.Body from a NativeFile, to support uploading of binary files without using multipart. 166 | -} 167 | rawBody : String -> NativeFile -> Body 168 | rawBody mimeType nf = 169 | Native.FileReader.rawBody mimeType nf.blob 170 | 171 | 172 | {-| Pretty print FileReader errors. 173 | -} 174 | prettyPrint : Error -> String 175 | prettyPrint err = 176 | case err of 177 | ReadFail -> 178 | "File reading error" 179 | 180 | NoValidBlob -> 181 | "Blob was not valid" 182 | 183 | NotTextFile -> 184 | "Not a text file" 185 | 186 | 187 | {-| A 'change' event handler for a `input [ type_ "file" ] []` form element 188 | -} 189 | onFileChange : (List NativeFile -> msg) -> Attribute msg 190 | onFileChange msg = 191 | on "change" (Json.map msg parseSelectedFiles) 192 | 193 | 194 | {-| JSON Decoder for change event from an HTML input element with 'type="file"'. 195 | -} 196 | parseSelectedFiles : Decoder (List NativeFile) 197 | parseSelectedFiles = 198 | fileParser "target" 199 | 200 | 201 | {-| Parse files selected using an HTML drop event. 202 | Returns a list of files. 203 | -} 204 | parseDroppedFiles : Decoder (List NativeFile) 205 | parseDroppedFiles = 206 | fileParser "dataTransfer" 207 | 208 | 209 | 210 | -- UN-EXPORTED HELPERS 211 | 212 | 213 | {-| -- Used by readAsText, defaults to True if format not recognised 214 | -} 215 | isTextFile : FileRef -> Bool 216 | isTextFile fileRef = 217 | case Json.decodeValue mtypeDecoder fileRef of 218 | Ok (Just (MimeType.Text text)) -> 219 | True 220 | 221 | Ok Nothing -> 222 | True 223 | 224 | _ -> 225 | False 226 | 227 | 228 | 229 | {- DECODERS 230 | The Files event has a structure 231 | 232 | { 1 : file1..., 2: file2..., 3 : ... } 233 | 234 | It also inherits other properties that we need to ignore during parsing. 235 | fileParser achieves this by using Json.maybe and then filtering out Nothing(s) 236 | -} 237 | 238 | 239 | fileParser : String -> Decoder (List NativeFile) 240 | fileParser fieldName = 241 | Json.field fieldName <| 242 | Json.field "files" <| 243 | fileListDecoder nativeFileDecoder 244 | 245 | 246 | {-| Apply a decoder to each file in the FileList, in order. 247 | -} 248 | fileListDecoder : Decoder a -> Decoder (List a) 249 | fileListDecoder decoder = 250 | let 251 | decodeFileValues indexes = 252 | indexes 253 | |> List.map (\index -> Json.field (toString index) decoder) 254 | |> List.foldr (Json.map2 (::)) (Json.succeed []) 255 | in 256 | Json.field "length" Json.int 257 | |> Json.map (\i -> List.range 0 (i - 1)) 258 | |> Json.andThen decodeFileValues 259 | 260 | 261 | {-| mime type: parsed as string and then converted to a MimeType 262 | -} 263 | mtypeDecoder : Decoder (Maybe MimeType.MimeType) 264 | mtypeDecoder = 265 | Json.map MimeType.parseMimeType (Json.field "type" Json.string) 266 | 267 | 268 | {-| blob: the whole JS File object as a Json.Value so we can pass 269 | it to a library that reads the content with a native FileReader 270 | -} 271 | nativeFileDecoder : Decoder NativeFile 272 | nativeFileDecoder = 273 | Json.map4 NativeFile 274 | (Json.field "name" Json.string) 275 | (Json.field "size" Json.int) 276 | mtypeDecoder 277 | Json.value 278 | -------------------------------------------------------------------------------- /src/FileReader/FileDrop.elm: -------------------------------------------------------------------------------- 1 | module FileReader.FileDrop exposing (..) 2 | 3 | import Html exposing (Attribute) 4 | import Html.Events exposing (onWithOptions, Options) 5 | import Json.Decode as Json 6 | import FileReader exposing (parseDroppedFiles, NativeFile) 7 | 8 | 9 | dzAttrs : msg -> msg -> msg -> (List NativeFile -> msg) -> List (Attribute msg) 10 | dzAttrs dragEnter dragLeave dragOverMsg dropMsg = 11 | [ onDragEnter dragEnter 12 | , onDragLeave dragLeave 13 | , onDragOver dragOverMsg -- Needed for drop to work - should generally be passed NoOp 14 | , onDropFiles dropMsg 15 | ] 16 | 17 | 18 | 19 | -- 20 | 21 | 22 | onDragEnter : msg -> Attribute msg 23 | onDragEnter msgCreator = 24 | onPreventDefault "dragenter" msgCreator 25 | 26 | 27 | onDragLeave : msg -> Attribute msg 28 | onDragLeave msgCreator = 29 | onPreventDefault "dragleave" msgCreator 30 | 31 | 32 | onDragOver : msg -> Attribute msg 33 | onDragOver = 34 | onPreventDefault "dragover" 35 | 36 | 37 | 38 | -- onDrop : msg -> Attribute msg 39 | -- onDrop msgCreator = 40 | -- onPreventDefault "drop" msgCreator 41 | 42 | 43 | onDropFiles : (List NativeFile -> msg) -> Attribute msg 44 | onDropFiles msgCreator = 45 | onWithOptions "drop" stopProp <| 46 | Json.map msgCreator parseDroppedFiles 47 | 48 | 49 | 50 | -- Helpers 51 | 52 | 53 | stopProp : Options 54 | stopProp = 55 | { stopPropagation = False, preventDefault = True } 56 | 57 | 58 | preventDef : Options 59 | preventDef = 60 | { stopPropagation = False, preventDefault = True } 61 | 62 | 63 | onStopPropagation : String -> a -> Attribute a 64 | onStopPropagation evt msgCreator = 65 | onWithOptions evt stopProp <| 66 | Json.succeed msgCreator 67 | 68 | 69 | onPreventDefault : String -> a -> Attribute a 70 | onPreventDefault evt msgCreator = 71 | onWithOptions evt preventDef <| 72 | Json.succeed msgCreator 73 | -------------------------------------------------------------------------------- /src/Native/FileReader.js: -------------------------------------------------------------------------------- 1 | // To use the examples, swap the commenting on the next two lines 2 | 3 | // var _user$project$Native_FileReader = function() { 4 | var _simonh1000$file_reader$Native_FileReader = function() { 5 | 6 | var scheduler = _elm_lang$core$Native_Scheduler; 7 | 8 | function useReader(method, fileObjectToRead) { 9 | return scheduler.nativeBinding(function(callback){ 10 | 11 | /* 12 | * Test for existence of FileReader using 13 | * if(window.FileReader) { ... 14 | * http://caniuse.com/#search=filereader 15 | * main gap is IE10 and 11 which do not support readAsBinaryFile 16 | * but we do not use this API either as it is deprecated 17 | */ 18 | var reader = new FileReader(); 19 | 20 | reader.onload = function(evt) { 21 | return callback(scheduler.succeed(evt.target.result)); 22 | }; 23 | 24 | reader.onerror = function() { 25 | return callback(scheduler.fail({ctor : 'ReadFail'})); 26 | }; 27 | 28 | // Error if not passed an objectToRead or if it is not a Blob 29 | if (!fileObjectToRead || !(fileObjectToRead instanceof Blob)) { 30 | return callback(scheduler.fail({ctor : 'NoValidBlob'})); 31 | } 32 | 33 | if (reader[method]) { 34 | var result = reader[method](fileObjectToRead); 35 | // prevent memory leak by nullifying fileObjectToRead 36 | fileObjectToRead = null; 37 | return result; 38 | } else { 39 | return callback(scheduler.fail({ctor : 'ReadFail'})); 40 | } 41 | }); 42 | } 43 | 44 | // readAsTextFile : Value -> Task error String 45 | var readAsTextFile = function(fileObjectToRead){ 46 | return useReader("readAsText", fileObjectToRead); 47 | }; 48 | 49 | // readAsArrayBuffer : Value -> Task error String 50 | var readAsArrayBuffer = function(fileObjectToRead){ 51 | return useReader("readAsArrayBuffer", fileObjectToRead); 52 | }; 53 | 54 | // readAsDataUrl : Value -> Task error String 55 | var readAsDataUrl = function(fileObjectToRead){ 56 | return useReader("readAsDataURL", fileObjectToRead); 57 | }; 58 | 59 | var filePart = function(name, blob) { 60 | return { 61 | _0: name, 62 | _1: blob 63 | } 64 | }; 65 | 66 | var rawBody = function (mimeType, blob) { 67 | return { 68 | ctor: "StringBody", 69 | _0: mimeType, 70 | _1: blob 71 | }; 72 | }; 73 | 74 | return { 75 | readAsTextFile : readAsTextFile, 76 | readAsArrayBuffer : readAsArrayBuffer, 77 | readAsDataUrl: readAsDataUrl, 78 | filePart: F2(filePart), 79 | rawBody: F2(rawBody) 80 | }; 81 | }(); 82 | --------------------------------------------------------------------------------