├── .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 |
--------------------------------------------------------------------------------