├── .npmignore ├── src ├── index.ts ├── dree-browser.scss └── dree-browser.ts ├── tsconfig.json ├── webpack.prod.js ├── public └── index.html ├── webpack.dev.js ├── README.md ├── LICENSE ├── webpack.common.js ├── package.json └── .gitignore /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DreeBrowser from "./dree-browser"; 2 | import { Dree } from "dree"; 3 | 4 | declare const DREE: Dree; 5 | 6 | const db = new DreeBrowser(document.getElementById("App")!, DREE, { 7 | on: { 8 | file: console.log, 9 | folder: console.log 10 | }, 11 | fileContent(d) { 12 | return `
${JSON.stringify(d, null, 2)}
`; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "sourceMap": true, 5 | "declaration": true, 6 | "strict": true, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": true, 9 | "module": "es6", 10 | "moduleResolution": "node", 11 | "target": "es5", 12 | "lib": ["es2015", "dom"], 13 | "allowSyntheticDefaultImports": true 14 | }, 15 | "include": [ 16 | "./src/**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common"); 2 | const TerserJSPlugin = require('terser-webpack-plugin'); 3 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 4 | const path = require("path"); 5 | 6 | module.exports = { 7 | ...common, 8 | mode: "production", 9 | devtool: "source-map", 10 | optimization: { 11 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], 12 | }, 13 | entry: { 14 | "dree-browser": path.resolve(__dirname, "src/dree-browser.ts") 15 | } 16 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Dree browser 9 | 10 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common"); 2 | const webpack = require("webpack"); 3 | const dree = require("dree"); 4 | const dotenv = require("dotenv"); 5 | const path = require("path"); 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | dotenv.config(); 8 | 9 | common.plugins.push(...[ 10 | new webpack.DefinePlugin({ 11 | DREE: JSON.stringify(dree.scan(process.env.DREE || ".")) 12 | }), 13 | new CopyWebpackPlugin([ 14 | "./public" 15 | ]) 16 | ]) 17 | 18 | module.exports = { 19 | ...common, 20 | mode: "development", 21 | devtool: "inline-source-map", 22 | devServer: { 23 | watchContentBase: true, 24 | contentBase: "public", 25 | open: true 26 | }, 27 | entry: { 28 | index: path.resolve(__dirname, "src/index.ts") 29 | }, 30 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dree Browser 2 | 3 | File browser on static websites, made specifically for [dree](https://www.npmjs.com/package/dree) library (which still has to be executed at server side, e.g. via `webpack.DefinePlugin` or web server.) 4 | 5 | ## Example 6 | 7 | 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm i dree-browser 13 | ``` 14 | 15 | And then, 16 | 17 | The example script can be seen at [`/src/index.ts`](/src/index.ts), and the HTML [`/public/index.html`](/public/index.html). (Don't forget to import the CSS at `/node_modules/dree-browser/dist/dree-browser.min.css`.) 18 | 19 | ## Plan 20 | 21 | [gitignore-parser](https://github.com/codemix/gitignore-parser) integration. 22 | 23 | ## Used in 24 | 25 | - 26 | - 27 | -------------------------------------------------------------------------------- /src/dree-browser.scss: -------------------------------------------------------------------------------- 1 | .dree-browser { 2 | display: flex; 3 | top: 0; 4 | bottom: 0; 5 | flex-direction: row; 6 | align-items: flex-start; 7 | height: 100%; 8 | flex-wrap: nowrap; 9 | 10 | .column { 11 | height: 100%; 12 | overflow: scroll; 13 | flex-grow: 0; 14 | flex-shrink: 0; 15 | flex-basis: 15em; 16 | border-right: 1px solid lightgray; 17 | 18 | ul { 19 | list-style-type: none; 20 | padding: 0; 21 | margin-block-start: 0; 22 | } 23 | } 24 | 25 | .icon { 26 | height: 1em; 27 | width: 1em; 28 | margin-left: 0.5em; 29 | margin-right: 0.5em; 30 | } 31 | 32 | .item { 33 | span { 34 | font-family: sans-serif; 35 | line-height: 1.5em; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | color: blue; 40 | cursor: pointer; 41 | } 42 | } 43 | } 44 | 45 | .file-content { 46 | // max-width: 500px; 47 | flex: 0 0 500px; 48 | height: 100%; 49 | border-right: 1px solid lightgray; 50 | } 51 | 52 | pre { 53 | white-space: pre-wrap; 54 | } 55 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pacharapol Withayasakpunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | const devMode = process.env.NODE_ENV !== 'production'; 5 | 6 | module.exports = { 7 | plugins: [ 8 | new MiniCssExtractPlugin({ 9 | filename: '[name].min.css', 10 | chunkFilename: '[id].min.css' 11 | }) 12 | ], 13 | output: { 14 | path: path.resolve(__dirname, "dist"), 15 | filename: "[name].min.js", 16 | library: 'beta', 17 | libraryTarget: 'umd', 18 | globalObject: "typeof self !== 'undefined' ? self : this" 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(sa|sc|c)ss$/, 24 | use: [ 25 | { 26 | loader: MiniCssExtractPlugin.loader, 27 | options: { 28 | hmr: devMode, 29 | reloadAll: true, 30 | }, 31 | }, 32 | 'css-loader', 33 | 'sass-loader', 34 | ], 35 | }, 36 | { 37 | test: /\.(ts|tsx)?$/, 38 | loader: "ts-loader", 39 | exclude: /node_modules/ 40 | }, 41 | { 42 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 43 | use: [{ 44 | loader: "file-loader", 45 | options: { 46 | name: "[name].[ext]", 47 | outputPath: "fonts" 48 | } 49 | }] 50 | }, 51 | ] 52 | }, 53 | resolve: { 54 | extensions: [ 55 | ".tsx", 56 | ".ts", 57 | ".js" 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dree-browser", 3 | "version": "0.1.3", 4 | "description": "File browser via passing dree object or the like", 5 | "main": "dist/dree-browser.min.js", 6 | "types": "dist/dree-browser.d.ts", 7 | "scripts": { 8 | "start": "webpack-dev-server --config webpack.dev.js", 9 | "build": "rimraf dist && webpack --config webpack.prod.js", 10 | "prepare": "npm run build", 11 | "deploy": "rimraf dist && webpack --config webpack.dev.js && gh-pages -d dist", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "dree", 16 | "file-manager", 17 | "file-browser" 18 | ], 19 | "author": { 20 | "name": "Pacharapol Withayasakpunt", 21 | "email": "patarapolw@gmail.com", 22 | "url": "https://polvcode.dev" 23 | }, 24 | "license": "MIT", 25 | "devDependencies": { 26 | "copy-webpack-plugin": "^5.0.4", 27 | "css-loader": "^3.2.0", 28 | "dotenv": "^8.1.0", 29 | "dree": "^2.1.10", 30 | "file-loader": "^4.2.0", 31 | "gh-pages": "^2.1.1", 32 | "mini-css-extract-plugin": "^0.8.0", 33 | "node-sass": "^4.12.0", 34 | "optimize-css-assets-webpack-plugin": "^5.0.3", 35 | "rimraf": "^3.0.0", 36 | "sass-loader": "^7.2.0", 37 | "terser-webpack-plugin": "^1.4.1", 38 | "ts-loader": "^6.0.4", 39 | "typescript": "^3.5.3", 40 | "webpack": "^4.39.2", 41 | "webpack-cli": "^3.3.7", 42 | "webpack-dev-server": "^3.8.0" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/patarpolw/dree-browser.git" 47 | }, 48 | "dependencies": { 49 | "vscode-icons-js": "^9.3.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,macos 3 | # Edit at https://www.gitignore.io/?templates=node,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Node ### 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | lerna-debug.log* 41 | 42 | # Diagnostic reports (https://nodejs.org/api/report.html) 43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # Directory for instrumented libs generated by jscoverage/JSCover 52 | lib-cov 53 | 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (https://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # TypeScript v1 declaration files 78 | typings/ 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Optional REPL history 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | *.tgz 94 | 95 | # Yarn Integrity file 96 | .yarn-integrity 97 | 98 | # dotenv environment variables file 99 | .env 100 | .env.test 101 | 102 | # parcel-bundler cache (https://parceljs.org/) 103 | .cache 104 | 105 | # next.js build output 106 | .next 107 | 108 | # nuxt.js build output 109 | .nuxt 110 | 111 | # vuepress build output 112 | .vuepress/dist 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # End of https://www.gitignore.io/api/node,macos 124 | 125 | /dist/ 126 | -------------------------------------------------------------------------------- /src/dree-browser.ts: -------------------------------------------------------------------------------- 1 | import "./dree-browser.scss"; 2 | import { Dree } from "dree"; 3 | import { getIconForFile, getIconForFolder, getIconForOpenFolder } from 'vscode-icons-js'; 4 | 5 | export interface IDreeBrowserOptions { 6 | colWidth?: number; 7 | fileContentWidth?: number; 8 | on?: { 9 | file?: (d: Dree) => void; 10 | folder?: (d: Dree) => void; 11 | } 12 | fileContent?: (d: Dree) => string; 13 | iconPath?: string; 14 | } 15 | 16 | export default class DreeBrowser { 17 | private el: HTMLElement; 18 | private dree: Dree; 19 | private options: IDreeBrowserOptions; 20 | 21 | constructor(el: HTMLElement, dree: Dree, options: IDreeBrowserOptions = {}) { 22 | this.el = el; 23 | this.el.classList.add("dree-browser"); 24 | this.options = options; 25 | this.options.iconPath = this.options.iconPath || "https://dderevjanik.github.io/vscode-icons-js-example/icons"; 26 | 27 | this.dree = dree; 28 | if (this.dree.children) { 29 | this.buildColumn(this.dree.children, 0); 30 | } 31 | } 32 | 33 | private buildColumn(ds: Dree[], depth: number) { 34 | const ulDiv = document.createElement("div"); 35 | 36 | for (const n of Array.from(this.el.childNodes)) { 37 | if (n instanceof HTMLDivElement) { 38 | if (n.classList.contains("file-content")) { 39 | this.el.removeChild(n); 40 | } 41 | } 42 | }; 43 | 44 | this.addOrReplaceColumn(ulDiv, depth, "column"); 45 | 46 | const ul = document.createElement("ul"); 47 | ulDiv.append(ul); 48 | 49 | for (const d of ds.sort((a, b) => { 50 | if (a.type === b.type) { 51 | return a.name.localeCompare(b.name); 52 | } else { 53 | return a.type === "directory" ? -1 : 1; 54 | } 55 | })) { 56 | const item = document.createElement("li"); 57 | item.classList.add("item"); 58 | 59 | if (d.type === "directory") { 60 | const icon1 = document.createElement("img"); 61 | icon1.classList.add("icon"); 62 | icon1.classList.add("folder-closed"); 63 | icon1.src = this.options.iconPath + "/" + getIconForFolder(d.name); 64 | item.append(icon1); 65 | 66 | const icon2 = document.createElement("img"); 67 | icon2.classList.add("icon"); 68 | icon2.classList.add("folder-open"); 69 | icon2.style.display = "none"; 70 | icon2.src = this.options.iconPath + "/" + getIconForOpenFolder(d.name); 71 | item.append(icon2); 72 | } else { 73 | const icon1 = document.createElement("img"); 74 | icon1.classList.add("icon"); 75 | const icon = getIconForFile(d.name); 76 | if (icon) { 77 | icon1.src = this.options.iconPath + "/" + icon; 78 | } 79 | item.append(icon1); 80 | } 81 | 82 | const span = document.createElement("span"); 83 | span.innerText = d.name; 84 | item.append(span); 85 | 86 | item.addEventListener("click", () => { 87 | if (d.children) { 88 | if (this.options.on && this.options.on.folder) { 89 | this.options.on.folder(d); 90 | } 91 | 92 | this.el.childNodes.forEach((n) => { 93 | if (n instanceof HTMLImageElement) { 94 | if (n.classList.contains("folder-open")) { 95 | n.style.display === "none"; 96 | } 97 | if (n.classList.contains("folder-closed")) { 98 | n.style.display === "inline-block"; 99 | } 100 | } 101 | }) 102 | 103 | item.childNodes.forEach((n) => { 104 | if (n instanceof HTMLImageElement) { 105 | if (n.classList.contains("folder-closed")) { 106 | n.style.display === "none"; 107 | } 108 | } 109 | }) 110 | 111 | this.buildColumn(d.children, depth + 1); 112 | } else { 113 | if (this.options.on && this.options.on.file) { 114 | this.options.on.file(d); 115 | } 116 | if (this.options.fileContent) { 117 | const col = document.createElement("div"); 118 | col.innerHTML = this.options.fileContent(d); 119 | 120 | this.addOrReplaceColumn(col, 0, "file-content"); 121 | } 122 | } 123 | }); 124 | 125 | ul.append(item); 126 | } 127 | } 128 | 129 | private addOrReplaceColumn(item: HTMLElement, depth: number, filterClass: string) { 130 | let i = 0; 131 | let isInserted = false; 132 | 133 | for (const n of Array.from(this.el.childNodes)) { 134 | if (n instanceof HTMLDivElement) { 135 | if (n.classList.contains(filterClass)) { 136 | if (depth === i) { 137 | n.replaceChild(item, n.childNodes[0]); 138 | isInserted = true; 139 | } else if (i > depth) { 140 | this.el.removeChild(n); 141 | } 142 | i++; 143 | } 144 | } 145 | }; 146 | 147 | if (!isInserted) { 148 | const col = document.createElement("div"); 149 | col.classList.add(filterClass); 150 | if (filterClass === "file-content") { 151 | if (this.options.fileContentWidth) { 152 | col.style.flexBasis = `${this.options.fileContentWidth}px`; 153 | } 154 | } else { 155 | if (this.options.colWidth) { 156 | col.style.flexBasis = `${this.options.colWidth}px`; 157 | } 158 | } 159 | 160 | col.append(item); 161 | this.el.append(col); 162 | } 163 | } 164 | } 165 | --------------------------------------------------------------------------------