diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index b1ff04a0..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -platform: x64 - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - -build: off - -build_script: - - node --version - - npm --version - - - npm i now nupdate -g - - nupdate -a gritty - - now rm cloudcmd -y -t %NOW_TOKEN% - - now --public -t %NOW_TOKEN% - - now alias -t %NOW_TOKEN% - diff --git a/.babelrc b/.babelrc deleted file mode 100644 index ceb7b89a..00000000 --- a/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "exclude": [ - "transform-regenerator" - ] - }], - ], - "plugins": [ - "@babel/plugin-transform-object-assign", - "@babel/plugin-proposal-object-rest-spread", - "module:fast-async", - "module:babel-plugin-macros", - ] -} diff --git a/.babelrc.json b/.babelrc.json new file mode 100644 index 00000000..33d70c39 --- /dev/null +++ b/.babelrc.json @@ -0,0 +1,9 @@ +{ + "presets": [ + "@babel/preset-env" + ], + "plugins": [ + "module:babel-plugin-macros", + "@babel/plugin-transform-optional-chaining" + ] +} diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 0ab1766a..00000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory" : "modules" -} \ No newline at end of file diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..bde9f522 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,4 @@ +last 2 Chrome versions +last 2 Safari versions +Firefox ESR +not dead diff --git a/.cloudcmd.menu.js b/.cloudcmd.menu.js new file mode 100644 index 00000000..0e0126b6 --- /dev/null +++ b/.cloudcmd.menu.js @@ -0,0 +1,31 @@ +export default { + 'F2 - Rename File': renameCurrent, + 'L - Lint': run('npm run lint'), + 'F - Fix Lint': run('npm run fix:lint'), + 'T - Test': run('npm run test'), + 'C - Coverage': run('npm run coverage'), + 'D - Build Dev': run('npm run build:client:dev'), + 'P - Build Prod': run('npm run build:client'), +}; + +async function renameCurrent(DOM) { + await DOM.renameCurrent(); +} + +function run(command) { + return async ({CloudCmd, DOM}) => { + const {TerminalRun, config} = CloudCmd; + + const {CurrentInfo} = DOM; + const {dirPath} = CurrentInfo; + + const cwd = config('root') + dirPath; + + return await TerminalRun.show({ + cwd, + command, + closeMessage: 'Press any key to close Terminal', + autoClose: false, + }); + }; +} diff --git a/.dockerignore b/.dockerignore index d0221be9..1c9d9d9a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,27 +11,13 @@ yarn-error.log yarn.lock now.json -Dockerfile -Dockerfile.alpine +docker webpack.config.js +cssnano.config.js bin/release.js -modules/jquery/dist -modules/jquery/external -modules/jquery/src - -!modules/jquery/dist/jquery.min.js - -modules/fancybox/lib/ -modules/fancybox/demo -modules/fancybox/sprite.psd - -modules/execon -modules/emitify - client -legacy server_ diff --git a/.editorconfig b/.editorconfig index 09fa9422..439abfd0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ root = true charset = utf-8 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true +trim_trailing_whitespace = false indent_style = space indent_size = 4 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3b54c586..00000000 --- a/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "env": { - "es6": true, - "node": true, - }, - "parserOptions": { - "ecmaVersion": 2017, - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true - } - }, - "rules": { - "indent": ["error", 4], - "semi": "error", - "no-console": 0, - "no-use-before-define": ["error", "nofunc"] - }, - "extends": [ - "eslint:recommended" - ] -} diff --git a/.eslintrc.server b/.eslintrc.server deleted file mode 100644 index 2e2555ea..00000000 --- a/.eslintrc.server +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "node": true - }, - "rules": { - "no-process-exit": 0 - }, - "extends": [ - ".eslintrc", - "plugin:node/recommended" - ], - "plugins": [ - "node" - ] -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9fdb6d3c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: coderaiser +open_collective: cloudcmd +ko_fi: coderaiser diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 6cc34cc8..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ - - -* **Version** (`cloudcmd -v`): -* **Node Version** `node -v`: -* **OS** (`uname -a` on Linux): -* **Browser name/version**: -* **Used Command Line Parameters**: -* **Changed Config**: - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..5c1e7460 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,45 @@ +--- + +name: Bug report +about: Create a report to help us improve +title: '' +labels: needs clarification +assignees: coderaiser + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- **Version** (`cloudcmd -v`): +- **Node Version** `node -v`: +- **OS** (`uname -a` on Linux): +- **Browser name/version**: +- **Used Command Line Parameters**: +- **Changed Config**: + +```json +{} +``` +- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd** +- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser** +- [ ] 💪 **I'm willing to work on this issue** + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..5f41e73a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Stack Overflow + url: https://stackoverflow.com/search?q=cloudcmd + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..549a8874 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- + +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md new file mode 100644 index 00000000..17bf5831 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -0,0 +1,24 @@ +*** + +name: Tracking issue +about: Create an issue with bug report or feature request. +title: "" +labels: needs triage +assignees: coderaiser + +*** + +- **Version** (`cloudcmd -v`): +- **Node Version** `node -v`: +- **OS** (`uname -a` on Linux): +- **Browser name/version**: +- **Used Command Line Parameters**: +- **Changed Config**: + +```json +{} +``` + +- [ ] 🎁 **I'm ready to donate on https://opencollective.com/cloudcmd** +- [ ] 🎁 **I'm ready to donate on https://ko-fi.com/coderaiser** +- [ ] 💪 **I'm willing to work on this issue** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 98b1f1ad..fb62bd82 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,5 @@ about something, just do as best as you're able. --> - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide") -- [ ] `npm run codestyle` is OK +- [ ] `npm run fix:lint` is OK - [ ] `npm test` is OK - diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml new file mode 100644 index 00000000..8cbeb042 --- /dev/null +++ b/.github/workflows/docker-io.yml @@ -0,0 +1,60 @@ +name: Docker IO +permissions: + contents: write +on: + - push +jobs: + buildx: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Node.js 24.x + uses: actions/setup-node@v6 + with: + node-version: 24.x + - name: Install Redrun + run: bun i redrun -g --no-save + - name: NPM Install + run: bun i --no-save + - name: Lint + run: redrun lint + - name: Build + id: build + run: | + redrun build + VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4) + echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push io-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.io + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:io + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-io + ghcr.io/${{ github.repository }}-io + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-io diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..5c81d368 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,88 @@ +name: Docker CI +permissions: + contents: write +on: + push: + tags: + - "*" +jobs: + buildx: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Node.js 22.x + uses: actions/setup-node@v6 + with: + node-version: 22.x + - name: Install Redrun + run: bun i redrun -g --no-save + - name: NPM Install + run: bun i --no-save + - name: Lint + run: redrun lint + - name: Build + id: build + run: | + redrun build + VERSION=$(grep '"version":' package.json -m1 | cut -d\" -f4) + echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push base-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:latest + coderaiser/cloudcmd:${{ steps.build.outputs.version }} + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }} + - name: Build and push alpine-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.alpine + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:alpine + coderaiser/cloudcmd:latest-alpine + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-alpine + ghcr.io/${{ github.repository }}:latest-alpine + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-alpine + - name: Build and push slim-image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/Dockerfile.slim + platforms: linux/amd64,linux/arm64 + push: true + tags: | + coderaiser/cloudcmd:slim + coderaiser/cloudcmd:latest-slim + coderaiser/cloudcmd:${{ steps.build.outputs.version }}-slim + ghcr.io/${{ github.repository }}-slim + ghcr.io/${{ github.repository }}:${{ steps.build.outputs.version }}-slim diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 00000000..429e5568 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,53 @@ +name: Node CI +on: + - push +permissions: + contents: write +jobs: + build: + runs-on: ubuntu-latest + env: + NAME: cloudcmd + strategy: + matrix: + node-version: + - 22.x + - 24.x + - 26.x + steps: + - uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - name: Install Redrun + run: bun i redrun madrun -g --no-save + - name: Install + run: bun i --no-save + - name: Init Madrun + run: madrun --init + - name: Lint + run: redrun fix:lint + - name: Typos + uses: coderaiser/typos.ai@v1.1.8 + with: + key: ${{ secrets.TYPOS_AI_KEY }} + - name: Commit fixes + uses: EndBug/add-and-commit@v10 + continue-on-error: true + with: + message: "chore: ${{ env.NAME }}: actions: lint ☘️" + - name: Build + run: redrun build + - name: Test + run: redrun test + - name: Coverage + run: redrun coverage coverage:report + - name: Coveralls + uses: coverallsapp/github-action@v2 + continue-on-error: true + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 7e027b4e..6aa86aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,17 @@ -package-lock.json -yarn.lock -yarn-error.log -node_modules -npm-debug.log* -coverage - -modules/jquery-mouse-wheel - -modules/jquery/dist -modules/jquery/external -modules/jquery/src - -!modules/jquery/dist/jquery.min.js - -modules/fancybox/lib/ -modules/fancybox/demo -modules/fancybox/sprite.psd -modules/fancybox/*.json -modules/fancybox/gulpfile.js -modules/jquery-mousewheel - -modules/execon -modules/emitify +*.swp +*.log +*.lock .nyc_output +.DS_Store +.idea -*.swp +package-lock.json +npm-debug.log* +node_modules +coverage +modules/execon +modules/emitify dist dist-dev - diff --git a/.madrun.js b/.madrun.js new file mode 100644 index 00000000..efa8ce98 --- /dev/null +++ b/.madrun.js @@ -0,0 +1,62 @@ +import {run, cutEnv} from 'madrun'; +import {defineEnv} from 'supertape/env'; + +const testEnv = defineEnv({ + timeout: 7000, + css: true, +}); + +const buildEnv = { + NODE_ENV: 'production', +}; + +export default { + 'start': () => 'node bin/cloudcmd.js', + 'start:dev': async () => await run('start', null, { + NODE_ENV: 'development', + }), + 'build:start': () => run(['build:client', 'start']), + 'build:start:dev': () => run([ + 'build:client:dev', + 'start:dev', + ]), + 'lint:all': () => run('lint:progress'), + 'lint': () => 'redlint fix; putout .', + 'lint:progress': () => run('lint', '-f progress'), + 'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"', + 'fresh:lint': () => run('lint', '--fresh'), + 'lint:fresh': () => run('lint', '--fresh'), + 'fix:lint': async () => `putout --fix . && redlint fix`, + 'lint:stream': () => run('lint', '-f stream'), + 'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], + 'test:e2e': () => `tape 'test-e2e/**/*.js'`, + 'test:client': () => `tape 'test/client/**/*.js'`, + 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, + 'wisdom': async () => await run(['lint:all', 'build', 'test'], null, { + CI: 1, + }), + 'wisdom:type': () => 'bin/release.js', + 'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], + 'coverage:report': () => 'c8 report --reporter=lcov', + 'report': () => 'c8 report --reporter=lcov', + '6to5': () => [buildEnv, 'rspack build --config rspack.config.js'], + '6to5:client': () => run('6to5', '--mode production'), + '6to5:client:dev': async () => await run('6to5', '--mode development', { + NODE_ENV: 'development', + }), + 'pre6to5:client': () => 'rimraf dist', + 'pre6to5:client:dev': () => 'rimraf dist-dev', + 'watch:client': () => run('6to5:client', '--watch'), + 'watch:client:dev': () => run('6to5:client:dev', '--watch'), + 'watch:server': () => 'nodemon bin/cloudcmd.js', + 'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`], + 'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`, + 'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`, + 'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`], + 'watch:fix:lint': async () => `nodemon -w client -w server -w test -w common -x '${await run('fix:lint')}'`, + 'build': async () => run('6to5:*'), + 'build:dev': async () => run('build:client:dev'), + 'build:client': () => run('6to5:client'), + 'build:client:dev': () => run('6to5:client:dev'), + 'heroku-postbuild': () => run('6to5:client'), +}; diff --git a/.npmignore b/.npmignore index 3243bfd4..b83b5923 100644 --- a/.npmignore +++ b/.npmignore @@ -1,35 +1,29 @@ +*.spec.* +*.config.* +*.fixture.js* +*.ai +*.cdr +*.eps +*.log +*.lock + .* -*.spec.js + manifest.yml -Dockerfile* docker-compose.yml -test +now.json +app.json +bower.json +deno.json +bin/release.* +img/logo/cloudcmd-hq.png +webpack.config.js + +docker +test* +fixture +fixture-* coverage css html -yarn-error.log -yarn.lock -now.json - -modules/jquery-mouse-wheel - -modules/jquery/dist -modules/jquery/external -modules/jquery/src - -!modules/jquery/dist/jquery.min.js - -modules/fancybox/lib/ -modules/fancybox/demo -modules/fancybox/sprite.psd - -app.json -bower.json -manifest.yml - -bin/release.js - client - -webpack.config.js - diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 00000000..1fe174f3 --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,15 @@ +{ + "checkCoverage": false, + "all": false, + "exclude": [ + "**/*.spec.js", + "**/*.*.js", + "**/*.config.*", + "**/fixture", + "**/test/**" + ], + "branches": 100, + "lines": 100, + "functions": 100, + "statements": 100 +} diff --git a/.putout.json b/.putout.json new file mode 100644 index 00000000..7154775a --- /dev/null +++ b/.putout.json @@ -0,0 +1,60 @@ +{ + "plugins": ["cloudcmd"], + "ignore": [ + "*.md", + "app.json", + "fontello.json", + "html", + "fixture*" + ], + "rules": { + "package-json/add-type": "off" + }, + "match": { + ".filesystem.json": { + "nodejs/rename-file-cjs-to-js": "off" + }, + "base64": { + "types/convert-typeof-to-is-type": "off" + }, + "*.md": { + "nodejs/convert-commonjs-to-esm": "on" + }, + ".webpack": { + "webpack": "on" + }, + "server": { + "nodejs/remove-process-exit": "on" + }, + "server/{server,exit}.js": { + "nodejs/remove-process-exit": "off" + }, + "server/{server,exit,terminal,distribute/log}.{js,mjs}": { + "remove-console": "off" + }, + "client/{client,cloudcmd,load-module}.{js,mjs}": { + "remove-console": "off" + }, + "client": { + "nodejs": "off" + }, + "client/sw": { + "remove-console": "off" + }, + "test/common/cloudfunc.js": { + "remove-console": "off" + }, + "storage.js": { + "promises/remove-useless-async": "off" + }, + "docker.yml": { + "github/set-node-versions": "off" + }, + "vim.js": { + "merge-duplicate-functions": "off" + }, + "common": { + "nodejs/declare": "off" + } + } +} diff --git a/.rspack/css.js b/.rspack/css.js new file mode 100644 index 00000000..10d28250 --- /dev/null +++ b/.rspack/css.js @@ -0,0 +1,38 @@ +import {env} from 'node:process'; +import {rspack} from '@rspack/core'; + +const {CssExtractRspackPlugin} = rspack; +const isDev = env.NODE_ENV === 'development'; + +const plugins = [ + new CssExtractRspackPlugin({ + filename: '[name].css', + }), +]; + +const rules = [{ + test: /\.css$/i, + use: [CssExtractRspackPlugin.loader, { + loader: 'css-loader', + options: { + url: true, + }, + }], +}, { + test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, + type: 'asset/inline', +}]; + +export default { + mode: isDev ? 'development' : 'production', + plugins, + module: { + rules, + }, + optimization: { + minimize: !isDev, + minimizer: [ + new rspack.LightningCssMinimizerRspackPlugin(), + ], + }, +}; diff --git a/.rspack/html.js b/.rspack/html.js new file mode 100644 index 00000000..e90038ac --- /dev/null +++ b/.rspack/html.js @@ -0,0 +1,36 @@ +import {env} from 'node:process'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; + +const isDev = env.NODE_ENV === 'development'; + +export const plugins = [ + new HtmlWebpackPlugin({ + inject: false, + template: 'html/index.html', + minify: !isDev && getMinifyHtmlOptions(), + }), +]; + +function getMinifyHtmlOptions() { + return { + removeComments: true, + removeCommentsFromCDATA: true, + removeCDATASectionsFromCDATA: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + /* оставляем, поскольку у нас + * в элемент fm генерируеться + * таблица файлов + */ + removeEmptyElements: false, + removeOptionalTags: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true, + + minifyJS: true, + }; +} diff --git a/.rspack/js.js b/.rspack/js.js new file mode 100644 index 00000000..251d85f6 --- /dev/null +++ b/.rspack/js.js @@ -0,0 +1,173 @@ +import {resolve, sep} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {env} from 'node:process'; +import {rspack} from '@rspack/core'; + +const resolveModule = (a) => fileURLToPath(import.meta.resolve(a)); + +const { + EnvironmentPlugin, + NormalModuleReplacementPlugin, + ProvidePlugin, +} = rspack; + +const modules = './modules'; +const dirModules = './client/modules'; +const dirCss = './css'; +const dirThemes = `${dirCss}/themes`; +const dirColumns = `${dirCss}/columns`; +const dir = './client'; +const {NODE_ENV} = env; +const isDev = NODE_ENV === 'development'; + +const rootDir = new URL('..', import.meta.url).pathname; +const dist = resolve(rootDir, 'dist'); +const distDev = resolve(rootDir, 'dist-dev'); +const devtool = isDev ? 'eval' : 'source-map'; + +const noParse = (a) => a.endsWith('.spec.js'); + +// codegen.macro is a babel-macro (build-time codegen), not supported by +// Rspack's native SWC transform, so client/sw/sw.js (the only file that +// uses it) keeps going through babel-loader. Everything else uses +// Rspack's built-in SWC loader, which is the main source of the speedup. +const rules = [{ + test: /sw\/sw\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', +}, { + test: /\.[mc]?js$/, + exclude: [/node_modules/, /sw\/sw\.js$/], + loader: 'builtin:swc-loader', + options: { + jsc: { + parser: { + syntax: 'ecmascript', + }, + }, + env: { + targets: 'defaults', + }, + }, +}]; + +const plugins = [ + new NormalModuleReplacementPlugin(/^node:/, (resource) => { + resource.request = resource.request.replace(/^node:/, ''); + }), + new NormalModuleReplacementPlugin(/^putout$/, '@putout/bundle'), + new EnvironmentPlugin({ + NODE_ENV, + }), + new ProvidePlugin({ + process: 'process/browser', + }), +]; + +const splitChunks = { + chunks: 'all', + cacheGroups: { + abcCommon: { + name: 'cloudcmd.common', + chunks: (chunk) => { + const lazyChunks = [ + 'sw', + 'nojs', + 'view', + 'edit', + 'terminal', + 'config', + 'user-menu', + 'help', + 'themes/dark', + 'themes/light', + 'columns/name-size', + 'columns/name-size-date', + 'columns/name-size-time', + 'columns/name-size-date-time', + ]; + + return !lazyChunks.includes(chunk.name); + }, + minChunks: 1, + enforce: true, + priority: -1, + reuseExistingChunk: true, + }, + }, +}; + +export default { + resolve: { + symlinks: false, + alias: { + 'node:process': 'process', + 'node:path': 'path', + }, + fallback: { + path: resolveModule('path-browserify'), + process: resolveModule('process/browser'), + util: resolveModule('util'), + }, + }, + devtool, + optimization: { + splitChunks, + }, + entry: { + 'themes/dark': `${dirThemes}/dark.css`, + 'themes/light': `${dirThemes}/light.css`, + 'columns/name-size': `${dirColumns}/name-size.css`, + 'columns/name-size-date': `${dirColumns}/name-size-date.css`, + 'columns/name-size-date-time': `${dirColumns}/name-size-date-time.css`, + 'nojs': `${dirCss}/nojs.css`, + 'help': `${dirCss}/help.css`, + 'view': `${dirCss}/view.css`, + 'config': `${dirCss}/config.css`, + 'terminal': `${dirCss}/terminal.css`, + 'user-menu': `${dirCss}/user-menu.css`, + 'sw': `${dir}/sw/sw.js`, + 'cloudcmd': `${dir}/cloudcmd.js`, + [`${modules}/edit`]: `${dirModules}/edit.js`, + [`${modules}/edit-file`]: `${dirModules}/edit-file.js`, + [`${modules}/edit-file-vim`]: `${dirModules}/edit-file-vim.js`, + [`${modules}/edit-names`]: `${dirModules}/edit-names.js`, + [`${modules}/edit-names-vim`]: `${dirModules}/edit-names-vim.js`, + [`${modules}/menu`]: `${dirModules}/menu/index.js`, + [`${modules}/view`]: `${dirModules}/view/index.js`, + [`${modules}/help`]: `${dirModules}/help.js`, + [`${modules}/markdown`]: `${dirModules}/markdown.js`, + [`${modules}/config`]: `${dirModules}/config/index.js`, + [`${modules}/contact`]: `${dirModules}/contact.js`, + [`${modules}/upload`]: `${dirModules}/upload.js`, + [`${modules}/operation`]: `${dirModules}/operation/index.js`, + [`${modules}/konsole`]: `${dirModules}/konsole.js`, + [`${modules}/terminal`]: `${dirModules}/terminal.js`, + [`${modules}/terminal-run`]: `${dirModules}/terminal-run.js`, + [`${modules}/cloud`]: `${dirModules}/cloud.js`, + [`${modules}/user-menu`]: `${dirModules}/user-menu/index.js`, + [`${modules}/polyfill`]: `${dirModules}/polyfill.js`, + [`${modules}/command-line`]: `${dirModules}/command-line.js`, + }, + output: { + filename: '[name].js', + path: isDev ? distDev : dist, + pathinfo: isDev, + devtoolModuleFilenameTemplate, + publicPath: '/dist/', + }, + module: { + rules, + noParse, + }, + plugins, + performance: { + maxEntrypointSize: 800_000, + maxAssetSize: 600_000, + }, +}; + +function devtoolModuleFilenameTemplate(info) { + const resource = info.absoluteResourcePath.replace(rootDir + sep, ''); + return `file://cloudcmd/${resource}`; +} diff --git a/.stylelintrc.yml b/.stylelintrc.yml deleted file mode 100644 index ddbd3acf..00000000 --- a/.stylelintrc.yml +++ /dev/null @@ -1,16 +0,0 @@ -extends: stylelint-config-standard -rules: - indentation: 4 - declaration-block-trailing-semicolon: always - declaration-colon-space-before: null - selector-list-comma-newline-after: null - comment-empty-line-before: null - number-leading-zero: null - number-no-trailing-zeros: null - string-quotes: single - function-url-quotes: never - no-eol-whitespace: null - font-family-name-quotes: always-unless-keyword - font-family-no-missing-generic-family-keyword: null - rule-empty-line-before: null - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7211ed53..00000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: node_js -node_js: - - 10 - - 8 - -script: - - npm run lint && npm run build && npm run coverage && npm run report - -notifications: - email: - on_success: never - on_failure: change - -sudo: required - -services: - - docker - -deploy: - provider: script - script: npm run docker-ci - skip_cleanup: true - on: - node: '8' - tags: true - all_branches: false - diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..dc8af253 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[files] +extend-exclude = ["ChangeLog"] diff --git a/.webpack/css.js b/.webpack/css.js deleted file mode 100644 index 2668695f..00000000 --- a/.webpack/css.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const { - basename, - extname, - join, -} = require('path'); - -const {env} = process; -const isDev = env.NODE_ENV === 'development'; - -const ExtractTextPlugin = require('extract-text-webpack-plugin'); - -const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`); -const extractMain = extractCSS('[name]'); - -const cssNames = [ - 'nojs', - 'view', - 'config', - ...getCSSList('columns'), -]; - -const cssPlugins = cssNames.map(extractCSS); - -const plugins = [ - ...cssPlugins, - extractMain, -]; - -const rules = [{ - test: /\.css$/, - exclude: /css\/(nojs|view|config|columns.*)\.css/, - use: extractMain.extract([ - 'css-loader?minimize', - ]), -}, -...cssPlugins.map(extract), { - test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/, - loader: 'url-loader?limit=50000', -}]; - -module.exports = { - plugins, - module: { - rules, - }, -}; - -function getCSSList(dir) { - const base = (a) => basename(a, extname(a)); - const addDir = (name) => `${dir}/${name}`; - const rootDir = join(__dirname, '..'); - - return fs.readdirSync(`${rootDir}/css/${dir}`) - .map(base) - .map(addDir); -} - -function extract(extractPlugin) { - const {filename} = extractPlugin; - const loader = isDev ? 'css-loader' : 'css-loader?minimize'; - - return { - test: RegExp(`css/${filename}`), - use: extractPlugin.extract([ - loader - ]) - }; -} - diff --git a/.webpack/html.js b/.webpack/html.js deleted file mode 100644 index 28e40c65..00000000 --- a/.webpack/html.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const {env} = process; -const isDev = env.NODE_ENV === 'development'; - -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -const plugins = [ - new HtmlWebpackPlugin({ - inject: false, - template: 'html/index.html', - minify: !isDev && getMinifyHtmlOptions(), - }), -]; - -module.exports = { - plugins, -}; - -function getMinifyHtmlOptions() { - return { - removeComments: true, - removeCommentsFromCDATA: true, - removeCDATASectionsFromCDATA: true, - collapseWhitespace: true, - collapseBooleanAttributes: true, - removeAttributeQuotes: true, - removeRedundantAttributes: true, - useShortDoctype: true, - removeEmptyAttributes: true, - /* оставляем, поскольку у нас - * в элемент fm генерируеться - * таблица файлов - */ - removeEmptyElements: false, - removeOptionalTags: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributes: true, - - minifyJS: true, - }; -} - diff --git a/.webpack/js.js b/.webpack/js.js deleted file mode 100644 index 27128537..00000000 --- a/.webpack/js.js +++ /dev/null @@ -1,121 +0,0 @@ -'use strict'; - -const { - resolve, - sep, - join, -} = require('path'); - -const { - EnvironmentPlugin, -} = require('webpack'); - -const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); - -const dir = './client'; -const dirModules = './client/modules'; -const modules = './modules'; - -const {env} = process; -const isDev = env.NODE_ENV === 'development'; - -const rootDir = join(__dirname, '..'); -const dist = resolve(rootDir, 'dist'); -const distDev = resolve(rootDir, 'dist-dev'); -const devtool = isDev ? 'eval' : 'source-map'; - -const notEmpty = (a) => a; -const clean = (array) => array.filter(notEmpty); - -const babelDev = { - babelrc: false, - plugins: [ - 'module:babel-plugin-macros', - ] -}; - -const rules = clean([ - !isDev && { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - isDev && { - test: /sw.js$/, - exclude: /node_modules/, - loader: 'babel-loader', - options: babelDev - }]); - -const plugins = [ - new EnvironmentPlugin(['NODE_ENV']), - new ServiceWorkerWebpackPlugin({ - entry: join(__dirname, '..', 'client', 'sw', 'sw.js'), - }), -]; - -const splitChunks = { - chunks: 'all', - name: 'cloudcmd.common', -}; - -module.exports = { - devtool, - optimization: { - splitChunks, - }, - entry: { - cloudcmd: `${dir}/cloudcmd.js`, - [modules + '/edit']: `${dirModules}/edit.js`, - [modules + '/edit-file']: `${dirModules}/edit-file.js`, - [modules + '/edit-file-vim']: `${dirModules}/edit-file-vim.js`, - [modules + '/edit-names']: `${dirModules}/edit-names.js`, - [modules + '/edit-names-vim']: `${dirModules}/edit-names-vim.js`, - [modules + '/menu']: `${dirModules}/menu.js`, - [modules + '/view']: `${dirModules}/view.js`, - [modules + '/help']: `${dirModules}/help.js`, - [modules + '/markdown']: `${dirModules}/markdown.js`, - [modules + '/config']: `${dirModules}/config.js`, - [modules + '/contact']: `${dirModules}/contact.js`, - [modules + '/upload']: `${dirModules}/upload.js`, - [modules + '/operation']: `${dirModules}/operation/index.js`, - [modules + '/konsole']: `${dirModules}/konsole.js`, - [modules + '/terminal']: `${dirModules}/terminal.js`, - [modules + '/cloud']: `${dirModules}/cloud.js`, - [modules + '/polyfill']: `${dirModules}/polyfill.js`, - }, - output: { - filename: '[name].js', - path: isDev ? distDev : dist, - pathinfo: isDev, - devtoolModuleFilenameTemplate, - publicPath: '/dist/', - }, - externals: [ - externals - ], - module: { - rules, - }, - plugins, -}; - -function externals(context, request, fn) { - if (!isDev) - return fn(); - - const list = [ - 'es6-promise', - ]; - - if (list.includes(request)) - return fn(null, request); - - fn(); -} - -function devtoolModuleFilenameTemplate(info) { - const resource = info.absoluteResourcePath.replace(rootDir + sep, ''); - return `file://cloudcmd/${resource}`; -} - diff --git a/.yaspellerrc b/.yaspellerrc index aa540e49..7e29119e 100644 --- a/.yaspellerrc +++ b/.yaspellerrc @@ -10,6 +10,7 @@ ".md" ], "dictionary":[ + "CloudCmd", "Dev", "Dropbox", "Deepword", @@ -21,17 +22,23 @@ "Iptables", "JitSu", "Node", + "IO", "Olena", "TarZak", "Termux", "Zalitok", "WebSocket", + "auth", + "autostart", + "binded", "cd", "cloudcmd", "coderaiser", "com", - "dev", "deepword", + "dev", + "destructuring", + "dropbox", "dword", "edward", "favicon", @@ -39,12 +46,18 @@ "gz", "io", "js", + "linux", "maintainers", + "markdown", + "microservice", "minification", "mouseup", "named", "nginx", + "npm", "or io", + "patreon", + "rc", "refactor", "sexualized", "sslPort", @@ -53,6 +66,7 @@ "v0", "v1", "v2", - "yml" + "yml", + "systemd" ] } diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 7e77a3a2..0e2522dd 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7c9b914..b6b6b4ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,30 @@ -Commit ---------------- +## Commit + Format of the commit message: **type(scope) subject** **Type**: -- feature(scope) subject -- fix(scope) subject -- docs(scope) subject -- refactor(scope) subject -- test(scope) subject -- chore(scope) subject + +- feature: scope: subject +- fix: scope: subject +- docs: scope: subject +- refactor: scope: subject +- test: scope: subject +- chore: scope: subject **Scope**: Scope could be anything specifying place of the commit change. For example util, console, view, edit, style etc... **Subject text**: + - use imperative, present tense: “change” not “changed” nor “changes” - don't capitalize first letter - no dot (.) at the end -**Message body**: + **Message body**: - just as in use imperative, present tense: “change” not “changed” nor “changes” - includes motivation for the change and contrasts with previous behavior **Examples**: -- [fix(style) .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) -- [fix(console) dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec) + +- [fix: style: .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050) +- [fix: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec) diff --git a/ChangeLog b/ChangeLog index b1a17cbc..c5e0fbca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,2832 @@ +2026.06.15, v19.19.1 + +feature: +- 78b206e0 client: key: Ctrl + L -> Ctrl + Shift + L: Log out (#466) +- b1492885 docker: io: NPM_CONFIG_PACKAGE_LOCK=false +- 76d7bf4d docker: typos, shellcheck: add + +2026.05.26, v19.19.0 + +feature: +- 4ee38db6 client: view: flac: add support + +2026.05.26, v19.18.1 + +feature: +- 40ecef5e cloudcmd: ratelimit: X-Forwarded-For (#437) + +2026.05.26, v19.18.0 + +fix: +- 161bede8 client: menu: @putout/bundle@5.5. +- 447d990f cloudcmd: server: rest: sendError + +feature: +- 5d9628ce cloudcmd: rate limit: add support (#437) +- 2be395e6 cloudcmd: get rid of manifest: 401 basic auth + +2026.05.17, v19.17.0 + +fix: +- d96f6c46 css: query: hide time on mobile +- e7c55e05 docker: io: XDG_CONFIG_HOME + +feature: +- ae1ca0f8 cloudcmd: cssnano-preset-default v8.0.1 +- 94d5096a style: owner, mode: improve +- 807f8346 cloudcmd: superc8 v12.6.0 +- 67a95722 docker: io: cline: add +- 68eacb91 qword: add +- 4acd294b docker: io: tmux +- 05c80043 cloudcmd: @supertape/loader-css v1.0.0 +- 800a6545 cloudcmd: eslint-plugin-n v18.0.1 +- a985bb36 cloudcmd: supertape v13.0.0 +- 4315ec61 docker: io XDG_CONFIG_HOME: /etc -> /usr/local/etc + +2026.05.03, v19.16.0 + +feature: +- d82d0335 client: vim: rr for rename file +- acfa27cf docker: io: nvchad: add +- 14f009f8 docker: io: bash-completion: add +- c6c60146 docker: io: f4 +- 9a6b8935 docker: io: GOPATH +- a9bc22ce cloudcmd: operation: rm useless checks +- 36bacfe9 cloudcmd: client: key: vim: cc, mm +- 591da25c cloudcmd: ponse v8.0.0 +- 1e1b073c docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd + +2026.04.28, v19.15.0 + +feature: +- 9d97343b cloudcmd: operation: rm useless checks +- 46a88cfa cloudcmd: client: key: vim: cc, mm + +2026.04.28, v19.14.0 + +feature: +- 36a8b641 cloudcmd: ponse v8.0.0 +- 1c263c18 docker: io: /usr/src/cloudcmd -> /usr/local/share/cloudcmd +- ea7b828c docker: io: ubuntu: resolute +- cd6c11ac docker: io: far2l +- c4beeec6 docker: io: gdu: add + +2026.04.21, v19.13.1 + +feature: +- d5cd11e8 cloudcmd: montag v2.0.1 +- dc94d2db docker: io: add latest git version +- 1637beee docker: io: /usr/local/src -> /usr/local/share +- b1bc4e73 Docker: io: pv +- 7c0dca60 docker: io: git: master + +2026.04.15, v19.13.0 + +fix: +- 48693d9e docker: io: XDG_CACHE_HOME + +feature: +- 4b2395c5 cloudcmd: Ctrl + L: logout +- 977a8aaa docker: io: strace: add +- 7d0098fd docker: io: XDG_CACHE_HOME=/tmp/cache +- a89e901b actions: docker: io: add + +2026.04.12, v19.12.5 + +feature: +- 1cfc1a6f docker: io: only amd64 + +2026.04.12, v19.12.4 + +feature: +- 3c2b5658 docker: io: haskell +- d37f8cd3 docker: io: palabra: node + +2026.04.12, v19.12.3 + +feature: +- cb6cabd4 docker: io: PALABRA_DIR + +2026.04.11, v19.12.2 + +feature: +- 2ce11fa2 docker: io: get rid of haskell: to slow install +- aa741232 docker: io: rizin, yara + +2026.04.09, v19.12.1 + +feature: +- 5bd03215 docker: io: add PREFIX + +2026.04.09, v19.12.0 + +feature: +- 13b15b7b docker: io: palabra + +2026.04.07, v19.11.14 + +feature: +- a31beab0 docker: io: rustup env + +2026.04.07, v19.11.13 + +feature: +- 6a08479a docker: io: BUN_INSTALL + +2026.04.06, v19.11.12 + +feature: +- 46c65554 docker: io: npm_config_cache + +2026.04.06, v19.11.11 + +feature: +- ae2ce388 docker: io: $DENO_DIR + +2026.04.06, v19.11.10 + +feature: +- 12ea14ac docker: io: nvm: node + +2026.04.06, v19.11.9 + +feature: +- ea96d13f docker: io: net-tools + +2026.04.06, v19.11.8 + +feature: +- 5c08565f docker: io: debian -> ubuntu + +2026.04.05, v19.11.7 + +feature: +- 36cdef37 docker: io: bookworm + +2026.04.05, v19.11.6 + +feature: +- 845f9bd1 cloudcmd: gritty v10.2.0 + +2026.04.05, v19.11.5 + +feature: +- f9c513cc docker: io: hexyl: add + +2026.04.04, v19.11.4 + +feature: +- e7347d25 docker: io: neovim: apt -> github + +2026.04.04, v19.11.3 + +feature: +- 9970ff76 docker: io: btop +- 4395a471 docker: io: $PATH: add $HOME/.local/bin + +2026.04.04, v19.11.2 + +feature: +- c40ae8e4 docker: io: htop +- f463c5c5 docker: io: aptitude: add +- d1032f09 docker: io: remove unused + +2026.04.04, v19.11.1 + +feature: +- 7787bfc2 cloudcmd: user-menu: runFromCDN + +2026.04.04, v19.11.0 + +feature: +- 32f89d38 cloudcmd: user-menu: root + +2026.04.04, v19.10.2 + +feature: +- df4fb517 cloudcmd: aleman v2.0.1 + +2026.04.03, v19.10.1 + +feature: +- ceb7ef4f docker: io: keep /var/lib/apt/lists + +2026.04.02, v19.10.0 + +fix: +- d12e7bd0 distribute: fix event listener leak on socket disconnect (#462) + +2026.04.02, v19.9.24 + +feature: +- c4d26c6a docker: io: apt-get upgrade +- 9ddb8c29 docker: io: get rid of nix +- e4d7d441 docker: io: nix + +2026.04.01, v19.9.23 + +feature: +- 5a3413ce docker: io: nix + +2026.03.31, v19.9.22 + +feature: +- b4345ed4 docker: io: DENO_DIR + +2026.03.31, v19.9.21 + +fix: +- 5c6a9a95 css: columns: name: 40% -> 35% + +2026.03.30, v19.9.20 + +feature: +- cd0b5554 iocmd: io: nvm + +2026.03.30, v19.9.19 + +feature: +- 6a52b11e docker: io: go, rust + +2026.03.30, v19.9.18 + +feature: +- 738059f2 docker: io: fzf +- 3fc8932f docker: io: less, el_GR + +2026.03.30, v19.9.17 + +feature: +- cf424d6c docker: io: command-not-found update + +2026.03.29, v19.9.16 + +feature: +- 19347a2b docker: io: add command-not-found + +2026.03.29, v19.9.15 + +feature: +- ee170552 docker: io: ubuntu +- e04c4594 docker: io: net-tools: add + +2026.03.29, v19.9.14 + +fix: +- 6c7709be docker: io: PS1 + +2026.03.29, v19.9.13 + +fix: +- 439b3710 bin: currify +- e4182841 docker: io: xterm-256color + +2026.03.29, v19.9.12 + +feature: +- 811a47fd cloudcmd: bin: get rid of require + +2026.03.29, v19.9.11 + +feature: +- 1f95f188 cloudcmd: get rid of simport + +2026.03.29, v19.9.10 + +feature: +- f0deb323 docker: io: add ja_JP.UTF-8 + +2026.03.29, v19.9.9 + +fix: +- f671f798 docker: io: PS1 environment variable in Dockerfile + +feature: +- cc7f9dc7 docker: io: use ubuntu + +2026.03.28, v19.9.8 + +feature: +- 4d1cd8cd docker: io: ffmpeg +- 54b56fdc cloudcmd: vim: ESC: use only to enable, do not use to disable + +2026.03.27, v19.9.7 + +feature: +- 110908e2 docker: io: apt-get + +2026.03.26, v19.9.6 + +feature: +- 6450a2f8 docker: io: add UTF-8 + +2026.03.26, v19.9.5 + +fix: +- e761cacb columns: name-size-date-time: 20% -> 19% + +2026.03.26, v19.9.4 + +feature: +- 69498ed6 docker: io: pull.rebase by default + +2026.03.24, v19.9.3 + +feature: +- 8763788b docker: io: healthcheck + +2026.03.23, v19.9.2 + +feature: +- 09a02074 docker: io: git config: add +- c448eaa4 docker: io: buni + +2026.03.23, v19.9.1 + +fix: +- 6e5318fa client: modules: config: input: quote + +2026.03.23, v19.9.0 + +feature: +- a1216cdd cloudcmd: add ability to hide port configuration + +2026.03.23, v19.8.15 + +feature: +- 68b2aa78 docker: io: cloudcmd_vim + +2026.03.23, v19.8.14 + +fix: +- 665ed9c2 docker: io: get back port + +2026.03.23, v19.8.13 + +feature: +- 618a5615 docker: io: PS1 + +2026.03.23, v19.8.12 + +feature: +- 556b0150 docker: io: PS1 + +2026.03.23, v19.8.11 + +fix: +- 97672ef5 docker: io: apt-get install -> apt-get + +feature: +- 67f27d84 docker: io: bun, deno + +2026.03.23, v19.8.10 + +feature: +- 025b005e docker: io: add PS1 + +2026.03.23, v19.8.9 + +fix: +- b052cf22 cloudcmd: no time available: --.--.---- -> --:--:-- (#461) + +2026.03.23, v19.8.8 + +feature: +- 02dbe56d server: user-menu: when error send it + +2026.03.23, v19.8.7 + +feature: +- ecc76e8b docker: io: renamify-cli, runny, redfork + +2026.03.23, v19.8.6 + +fix: +- 53c072ab @putout/plugin-cloudcmd: devDependencies -> dependnecies + +feature: +- 4b9922bf docker: /usr/src: app -> cloudcmd + +2026.03.23, v19.8.5 + +fix: +- 56fc8b83 docker: gritty + +2026.03.22, v19.8.4 + +feature: +- 5000227e docker: vim nvim + +2026.03.22, v19.8.3 + +fix: +- 01677e6a docker: io: slim -> io + +feature: +- 7e35c606 docker: io: curl wget + +2026.03.22, v19.8.2 + +feature: +- e5b221f7 docker: io: add + +2026.03.22, v19.8.1 + +feature: +- 708a4c6b docker: slim: add +- 80613f46 docker: slim: add + +2026.03.20, v19.8.0 + +fix: +- 59037f2c cloudcmd: bin: --show-config + +feature: +- 10934b3a cloudcmd: add ability to show modification time (#230) + +2026.03.18, v19.7.1 + +feature: +- b0c1d36c cloudcmd: @cloudcmd/fileop v9.0.7 (#460) + +2026.03.17, v19.7.0 + +feature: +- daf83875 cloudfunc: override date format (#459) + +2026.03.17, v19.6.9 + +feature: +- b28a070a cloudcmd: redzip v4.6.1 +- 43c5a011 cloudcmd: css-minimizer-webpack-plugin v8.0.0 +- 15dcae5c cloudcmd: webpack-cli v7.0.2 +- 5976da81 cloudcmd: @cloudcmd/fileop v9.0.5 +- 37cb83f2 cloudcmd: redzip v4.6.0 + +2026.02.27, v19.6.8 + +feature: +- 15fab514 cloudcmd: copymitter v10.3.0 (#458) + +2026.02.26, v19.6.7 + +feature: +- 68c7d0be cloudcmd: onezip v7.0.0 + +2026.02.26, v19.6.6 + +feature: +- 3987cc82 cloudcmd: redzip v4.5.1 (#457) + +2026.02.26, v19.6.5 + +feature: +- 964ae989 cloudcmd: redzip v4.5.0 (#457) + +2026.02.25, v19.6.4 + +feature: +- a66eeda3 cloudcmd: copymitter v10.2.0 (coderaiser/cloudcmd#457) +- 4340533a cloudcmd: c8 v11.0.0 +- 0857711f cloudcmd: redzip v4.2.0 (#457) + +2026.02.24, v19.6.3 + +feature: +- 2234f1b4 cloudcmd: redzip v4.2.0 (#475) + +2026.02.24, v19.6.2 + +feature: +- 321a54dd cloudcmd: @cloudcmd/fileop v9.0.2 (#457) + +2026.02.24, v19.6.1 + +feature: +- 7c5ac408 cloudcmd: @cloudcmd/fileop v9.0.1 (#457) + +2026.02.21, v19.6.0 + +feature: +- 6d19bf2e common: object.omit -> omit + +2026.02.18, v19.5.1 + +feature: +- 6e1cf4ef cloudcmd: supermenu v5.0.0 + +2026.02.18, v19.5.0 + +feature: +- b20539ef common: entity: encode {,} +- 7ef134f4 cloudcmd: rendy v5.0.0 + +2026.02.18, v19.4.1 + +feature: +- 1e18d513 cloudcmd: @cloudcmd/fileop v9.0.0 + +2026.02.18, v19.4.0 + +fix: +- 45cf9baf menu: prefix (#456) + +feature: +- 3e647290 cloudcmd: redlint v6.0.0 +- 800ed012 cloudcmd: putout v42.0.5 +- 525c17d4 cloudcmd: madrun v13.0.0 +- 44247499 cloudcmd: eslint-plugin-putout v31.0.0 + +2026.02.15, v19.3.9 + +feature: +- 9ffe3ef1 cloudcmd: copymitter v10.0.0 (#457) + +2026.02.15, v19.3.8 + +fix: +- d274a2b3 spinner (#456) + +2026.02.13, v19.3.7 + +feature: +- 8fd79a27 cloudcmd: win32 v8.0.0 + +2026.02.12, v19.3.6 + +feature: +- ac94eccd cloudcmd: konsole: named +- 144e4a34 cloudcmd: gritty v10.0.0 +- 938f9e76 cloudcmd: console-io v15.0.1 + +2026.02.08, v19.3.5 + +feature: +- fb40bd9c rm cssnano: has no sense for spinner, option disabled by default (https://svgo.dev/docs/plugins/convertPathData/) +- 76125be9 cloudcmd: eslint v10.0.0 + +2026.02.06, v19.3.4 + +feature: +- 66a08c7f cloudcmd: deepword v11.0.0 + +2026.02.05, v19.3.3 + +feature: +- 4a5a56f4 cloudcmd: dword v16.0.0 + +2026.02.04, v19.3.2 + +fix: +- 99d8435e cloudcmd: exports +- a266c145 cloudcmd: default -> named +- 6e3ba271 Closing X in editor disappeared (#455) + +feature: +- 30f42e94 cloudcmd: restafary v13.0.1 +- f84ce853 cloudcmd: edward v16.0.0 + +2026.02.03, v19.3.1 + +fix: +- 5661bc4f Closing X in editor disappeared (#455) + +2026.02.03, v19.3.0 + +feature: +- 4533a25c cloudcmd: migrate to ESM +- e8a81c49 client: dom: events: migrate to ESM +- 071141bc client: terminal-run: migrate to ESM + +2026.02.03, v19.2.0 + +feature: +- bb32f7c4 client: dom: migrate to ESM +- 457c83db client: migrate to ESM +- f8a941bd client: modules: markdown: migrate to ESM +- 9d6cffaf client: polifyll: migrate to ESM +- 5b704d06 client: edit-names: migrate to ESM +- 2c2ca8eb client: edit-file: migrate to ESM +- c9f57c5f client: modules: edit-file-vim: migrate to ESM +- 327ac9de client: modules: help: migrate to ESM +- dc34ee87 cloudcmd: @putout/plugin-cloudcmd v5.0.0 +- 3cd3695b client: modules: edit-names-vim: migrate to ESM +- dfcbfd63 client: modules: terminal: migrate to ESM + +2026.02.02, v19.1.21 + +feature: +- e3ad330e client: konsole: migrate to ESM +- 983fd9af client: edit: migrate to ESM +- ebabad94 common: entity: migrate to ESM +- c3b71653 client: dom: images -> #images +- 8cad7514 common: cloudfunc -> #common/cloudfunc +- 1f174870 client: view: migrate to ESM +- 7173f6cb cloudcmd: smalltalk v5.0.0 + +2026.01.31, v19.1.20 + +feature: +- c1014c9c client: dom: operations: rename-current: migrate to ESM +- 2e486f8b cloudcmd: restafary v13.0.0 +- 6addb29f cloudcmd: redzip v4.0.0 + +2026.01.30, v19.1.19 + +feature: +- 2a0feac7 cloudcmd: @cloudcmd/modal v4.0.0 + +2026.01.30, v19.1.18 + +feature: +- 73fa4961 client: modules: user-menu: migrate to ESM +- ebfdf8c0 client: modules: operation: migrate to ESM +- ad683171 client: modules: menu: migrate to ESM +- 0054cfa3 client: dom: load-remote: migrate to ESM +- e4d0ece0 client: dom: rest: migrate to ESM +- c704ffe4 client: dom: storage: migrate to ESM +- 5182cb81 client: modules: upload: migrate to ESM +- 9543f1ad client: dom: upload-files: migrate to ESM + +2026.01.29, v19.1.17 + +feature: +- f9c28319 client: dom: migrate to ESM +- 9d2c4e4a client: dom: cmd: move out +- 5a2aa70f client: dom: simplify require +- dee50a85 client: dom: files: migrate to ESM +- 23c0d770 client: dom: dom-tree: migrate to ESM +- cf2c6415 client: directory: migrate to ESM +- 0eb802e7 client: dom: dialog: migrate to ESM + +2026.01.28, v19.1.16 + +fix: +- 4c242631 css: spinner: do not minify svg + +2026.01.28, v19.1.15 + +feature: +- 265c0b49 client: key: vim: migrate to ESM +- 3bc49f02 client: set-current-by-char: migrate to ESM + +2026.01.28, v19.1.14 + +feature: +- f8a63b5a client: key: binder: migrate to ESM +- 2cc97f33 client: cloud: migrate to ESM + +2026.01.27, v19.1.13 + +feature: +- 41b5a96f client: load: migrate to ESM + +2026.01.27, v19.1.12 + +feature: +- 27a52d33 client: sw: migrate to ESM +- 6b049d95 client: sw: register: migrate to ESM + +2026.01.25, v19.1.11 + +feature: +- f849b842 client: listeners: migrate to ESM +- 091f9017 client: listeners: simplify +- 3c1a82e7 client: key: view: get rid of mock-require + +2026.01.25, v19.1.10 + +feature: +- dc5867b0 client: key: vim: get rid of mock-require +- 77b70b21 cloudcmd: aleman v2.0.0 + +2026.01.21, v19.1.9 + +feature: +- 75ad4415 cloudcmd: @putout/eslint-flat v4.0.0 +- c5d9bd7c client: key: vim: get rid of mock-require +- f437a52f client: images: migrate to EMS +- 7192a56e client: dom: current-file: migrate to ESM + +2026.01.20, v19.1.8 + +fix: +- 8a769fd5 client: modules: operation: no update after copy + +feature: +- d574a93d client: key: migrate to ESM +- 3b409074 client: modules: operation: migrate to ESM +- 3b6b0b5a client: buffer: migrate to ESM +- 8876f050 cloudcmd: eslint-plugin-putout v30.0.0 + +2026.01.17, v19.1.7 + +feature: +- 23a6a698 client: dom/events -> #dom/events +- 9cebb241 client: dom: events: migrate to ESM +- a94fa0d4 client: cloudcmd: migrate to ESM +- 3bdf47a5 client: migrate to ESM + +2026.01.16, v19.1.6 + +fix: +- a523ef65 tests + +feature: +- 64654e8d common: cloudfunc: migrate to ESM +- add31607 common: cloudfunc: get rid of bas64 +- e36de00c modulas: migrate to ESM + +2026.01.16, v19.1.5 + +feature: +- 450f1461 client: improve testability +- d979e949 server: env: migrate to ESM + +2026.01.15, v19.1.4 + +feature: +- 6e778a35 client: sort: migrate to ESM +- e27ef51d client: sort: migrate to ESM +- 917f5851 client: load-module: migrate to ESM +- 9950caca client: get-json-from-file-table: migrate to ESM + +2026.01.15, v19.1.3 + +feature: +- f903c5c9 cloudcmd: multi-rename v3.0.0 + +2026.01.14, v19.1.2 + +fix: +- 9e2c5ac6 client: edit-names: group rename not renaming (#453) +- f0dcbe94 client: key: config + +feature: +- 6856207d server: env -> env.parse +- dc99417c client: key: get rid of mock-require +- 4bb7d704 client: modules: view: get rid of mock-require + +2026.01.12, v19.1.1 + +feature: +- 5cc6f79d cloudcmd: @cloudcmd/stub v5.0.0 +- 024bc413 cloudcmd: fullstore v4.0.0 +- 53f6f9e7 cloudcmd: globals v17.0.0 +- 6d21c539 cloudcmd: madrun v12.1.0 +- 253389ea cloudcmd: supertape v12.0.0 + +2025.12.31, v19.1.0 + +feature: +- 0ff16314 cloudcmd: redlint v5.0.0 +- 43edba8c cloudcmd: try-to-catch v4.0.0 +- 06f3b782 cloudcmd: try-catch v4.0.4 +- dfcd6557 deno config: add +- ab20a462 server: bun support (oven-sh/bun#25674) + +2025.12.24, v19.0.17 + +feature: +- 0222d177 cloudcmd: gritty v9.0.0 + +2025.12.05, v19.0.16 + +feature: +- 14ec19e8 cloudcmd: find-up v8.0.0 +- e6a00979 cloudcmd: eslint-plugin-putout v29.0.2 +- 5b5352c7 cloudcmd: putout v41.0.0 + +2025.11.28, v19.0.15 + +feature: +- 00676531 cloudcmd: aleman v1.16.5 + +2025.11.27, v19.0.14 + +fix: +- 2a525e9b aleman: copy paste in text editor (#449) + +feature: +- 3ceb9a8c cloudcmd: open v11.0.0 + +2025.09.26, v19.0.13 + +feature: +- 8477f3e4 cloudcmd: aleman v1.16.3 (#446) + +2025.09.25, v19.0.12 + +feature: +- 836e908e cloudcmd: aleman v1.16.2 + +2025.09.24, v19.0.11 + +feature: +- f4386a6f cloudcmd: aleman v1.16.1 + +2025.09.23, v19.0.10 + +feature: +- 2e667ba6 cloudcmd: aleman v1.15.0 + +2025.09.22, v19.0.9 + +feature: +- 60c56164 cloudcmd: aleman v1.14.4 + +2025.09.20, v19.0.8 + +feature: +- efe81320 cloudcmd: aleman v1.14.3 + +2025.09.18, v19.0.7 + +feature: +- 5b972e2e cloudcmd: aleman v1.14.0 + +2025.09.17, v19.0.6 + +feature: +- 39a24028 cloudcmd: aleman v1.13.0 + +2025.09.16, v19.0.5 + +fix: +- 64df81bc cloudcmd: client: listeners: f9: stopPropagation + +feature: +- 38dd5101 cloudcmd: aleman v1.12.4 + +2025.09.15, v19.0.4 + +feature: +- 66db798c cloudcmd: aleman v1.12.3 + +2025.09.15, v19.0.3 + +feature: +- c5aed16f cloudcmd: aleman v1.12.2 + +2025.09.14, v19.0.2 + +feature: +- 511347d3 cloudcmd: aleman v1.11.0 + +2025.09.14, v19.0.1 + +fix: +- fc6304a1 tmpl: config: aleman, supermenu + +feature: +- a05ecdb4 cloudcmd: aleman v1.10.0 + +2025.09.14, v19.0.0 + +feature: +- 50b19dcc cloudcmd: menu: default: supermenu -> aleman +- 5970f10a cloudcmd: drop support of node < 22 + +2025.09.14, v18.8.11 + +feature: +- b0360d8e cloudcmd: aleman v1.9.1 +- 00a20129 cloudcmd: html: importsmap: add + +2025.09.14, v18.8.10 + +feature: +- ddf9e455 cloudcmd: aleman v1.9.0 + +2025.09.14, v18.8.9 + +feature: +- 2e7bdb8a cloudcmd: aleman v1.8.0 + +2025.09.13, v18.8.8 + +feature: +- 03631d95 cloudcmd: aleman v1.7.0 + +2025.09.12, v18.8.7 + +feature: +- 09408af5 cloudcmd: aleman v1.6.1 + +2025.09.12, v18.8.6 + +feature: +- 4fcaf288 cloudcmd: aleman v1.6.0 + +2025.09.10, v18.8.5 + +feature: +- c69ec16e cloudcmd: aleman v1.5.0 + +2025.09.09, v18.8.4 + +feature: +- 08d13c6d cloudcmd: aleman v1.4.9 + +2025.09.04, v18.8.3 + +feature: +- b4792fc3 cloudcmd: aleman v1.4.0 + +2025.09.04, v18.8.2 + +feature: +- a0b3285b cloudcmd: aleman v1.3.0 + +2025.09.04, v18.8.1 + +feature: +- 15b71c14 cloudcmd: aleman v1.2.5 +- d252fe5f robots.txt: add + +2025.09.02, v18.8.0 + +feature: +- 08b5c6b2 client: menu: aleman: add + +2025.08.30, v18.7.4 + +fix: +- a6d18ddb select file: name -> line +- 2077468a client: listeners: click: avoid select on conext menu +- 64e4aba4 client: menu: before show: unsetBind + +2025.07.26, v18.7.3 + +fix: +- 884c83eb client: polyfill (#442) + +2025.07.24, v18.7.2 + +feature: +- 2e775908 cloudcmd: html-looks-like: remove +- bb6a7a28 docker: npm -> bun + +2025.07.06, v18.7.1 + +fix: +- 784bb2eb build: sw + +feature: +- 8f52376d cloudcmd: revert: optimize-css-assets-webpack-plugin -> css-minimizer-webpack-plugin: broken spinner +- 82008749 cloudcmd: optimize-css-assets-webpack-plugin -> css-minimizer-webpack-plugin + +2025.07.05, v18.7.0 + +fix: +- b1e231a5 client: menu: close: ESC + +feature: +- 546d0610 cloudcmd: process v0.11.10 +- 121b114e cloudcmd: path-browserify v1.0.1 +- 8592cedc cloudcmd: mini-css-extract-plugin v2.9.2 +- a53ab67b cloudcmd: webpack-cli v6.0.1 +- de2cedd9 cloudcmd: webpack v5.99.9 +- da545ea4 cloudcmd: style-loader v4.0.0 +- db6e8334 cloudcmd: optimize-css-assets-webpack-plugin v6.0.1 +- 2f0c1a61 cloudcmd: html-webpack-plugin v5.6.3 +- e100dcf6 cloudcmd: extract-text-webpack-plugin v3.0.2 +- 76c40008 cloudcmd: css-loader v7.1.2 +- fb5e5a32 cloudcmd: clean-css-loader v4.2.1 +- 8551cd54 cloudcmd: babel-loader v10.0.0 +- c9380319 webpack 5 +- ddc94adb cloudcmd: eslint-plugin-putout v28.0.0 + +2025.07.04, v18.6.1 + +feature: +- 9eafa189 cloudcmd: http-auth v4.2.1 +- e99d0847 cloudcmd: montag v1.2.1 +- b77e9c91 cloudcmd: pipe-io v4.0.1 +- 4b476a6d cloudcmd: globals v16.3.0 +- 2057065d cloudcmd: @putout/eslint-flat v3.0.1 + +2025.07.02, v18.6.0 + +feature: +- 2eb3dc66 cloudcmd: @iocmd/wait v2.1.0 +- 1679b788 cloudcmd: webpackbar v7.0.0 +- 9a4cf388 cloudcmd: eslint-plugin-putout v27.2.1 +- f4b0f92f cloudcmd: express v5.1.0 +- 4ab4be12 thread-it: get rid (#438) +- 99ad0c21 cloudcmd: rm @putout/babel +- 8ccec23d cloudcmd: help: require -> import +- 2a97ac66 cloudcmd: yargs-parser v22.0.0 +- b26c8bba cloudcmd: thread-it v3.0.0 + +2025.04.10, v18.5.2 + +feature: +- 8450bfa6 cloudcmd: putout v40.0.3 +- 51f51b54 cloudcmd: @putout/plugin-cloudcmd v4.0.0 +- 08ab63d7 cloudcmd: supertape v11.0.4 +- e7cc9b92 cloudcmd: redlint v4.1.1 +- 368c9bb8 cloudcmd: eslint v9.23.0 +- 43fd5ed6 cloudcmd: madrun v11.0.0 +- f774d5b2 cloudcmd: eslint-plugin-putout v26.1.0 +- b0a7fc16 cloudcmd: putout v39.3.0 + +2025.02.03, v18.5.1 + +feature: +- 467f0a79 cloudcmd: webpack-merge v6.0.1 +- 353a1fb6 cloudcmd: putout v38.0.5 +- 8e98b778 cloudcmd: eslint-plugin-putout v24.0.0 + +2025.01.20, v18.5.0 + +fix: +- ad8e55d8 client: themes -> columns (#434) + +feature: +- 2fc503f7 cloudcmd: @putout/babel v3.0.0 + +2024.12.13, v18.4.1 + +feature: +- 100e940e cloudcmd: putout v37.0.1 + +2024.11.22, v18.4.0 + +fix: +- dff02672 cloudcmd: make manifest.json accessible when authentication is enabled (#428) + +2024.11.14, v18.3.0 + +feature: +- 71dc8dd6 cloudcmd: Add support for Progressive Web App (#426) + +2024.11.06, v18.2.1 + +feature: +- a733d814 css: --is-mobile: add +- f22120dc cloudcmd: prevent unselect being fired on panel click when in mobile view (#422) +- 1a0af863 docker: add image source label to dockerfiles (#421) +- 0446a74d docker: add image source label to dockerfiles (#419) + +2024.10.27, v18.2.0 + +feature: +- ac9abbd3 cloudcmd: eslint-plugin-putout v23.1.0 +- 4bc5a783 cloudcmd: add context menu option to toggle file selection (#420) + +2024.08.17, v18.1.0 + +feature: +- ddf4542b cloudcmd: add ability to hide dot files (#307) + +2024.08.16, v18.0.2 + +feature: +- 3d03efbe css: show links in one small screens + +2024.08.16, v18.0.1 + +fix: +- 62ed8411 bin: validateArgs is not a function (#147) + +feature: +- 9ec94dee cloudcmd: chalk v5.3.0 + +2024.08.16, v18.0.0 + +feature: +- 5e93bcca cloudcmd: rimraf v6.0.1 +- 74d1eb7e drop support of node < 20 + +2024.08.16, v17.4.4 + +fix: +- a6aa9bbc revert rimraf v6.0.1 + +feature: +- 282b3d5c cloudcmd: @putout/cli-validate-args v2.0.0 + +2024.07.27, v17.4.3 + +feature: +- 6e8348b8 cloudcmd: rimraf v6.0.1 +- 61ca7f36 cloudcmd: putout v36.0.2 + +2024.07.03, v17.4.2 + +feature: +- ba2d0b36 cloudcmd: just-snake-case v3.2.0 +- 4cc47e30 cloudcmd: just-capitalize v3.2.0 +- d8451e56 cloudcmd: just-pascal-case v3.2.0 +- 6abf327d cloudcmd: package-json v10.0.0 +- 2ae6ad34 docker: Dockerimage update Debian12 (#414) +- 05ef0ae4 cloudcmd: c8 v10.1.2 + +2024.05.06, v17.4.1 + +feature: +- 154b4bd6 cloudcmd: @cloudcmd/move-files v8.0.0 +- c409a2db cloudcmd: copymitter v9.0.0 + +2024.04.17, v17.4.0 + +fix: +- 6fb21020 server: route: path traversal + +feature: +- 37ab7068 publish container image to GHCR (#409) + +2024.04.03, v17.3.3 + +feature: +- b088b84e cloudcmd: deepword v10.0.0 + +2024.03.29, v17.3.2 + +fix: +- f7a6a366 typo: Wraped -> Wrapped + +2024.03.29, v17.3.1 + +feature: +- d7581829 distribute: convert to ESM + +2024.03.29, v17.3.0 + +feature: +- 6bc4f3ec dark theme: add (#332) +- 35622082 route: convert to ESM + +2024.03.29, v17.2.1 + +fix: +- cc134464 client: vim: space + +feature: +- e3f89e88 dark mode: add +- c45b23fe css: vars: add +- b1f74c00 css: add vars + +2024.03.22, v17.2.0 + +feature: +- 3e565109 convert to ESM +- 770a0812 pack: get rid of mock-require +- 25d8faea rest: get rid of mock-require +- 401a669a user-menu: get rid of mock-require +- 4e32241d terminal: get rid of mock-require + +2024.03.21, v17.1.6 + +feature: +- e01ee457 server: route: get rid of mock-require +- c7f90901 root: get rid of mock-require +- fcce26d4 cloudfunc: get rid of mock-require + +2024.03.20, v17.1.5 + +feature: +- bf90bf22 server: validate: get rid of mock-require + +2024.03.20, v17.1.4 + +feature: +- 98d3a4cc server: columns: get rid of mock-require + +2024.03.18, v17.1.3 + +feature: +- e080a540 server: cloudcmd: get rid of mock-require + +2024.03.18, v17.1.2 + +fix: +- 857c9700 docker: alpine + +feature: +- bf614e1d cloudcmd: redlint v3.13.1 + +2024.03.16, v17.1.1 + +feature: +- a92a5a0d cloudcmd: restbox v4.0.0 +- c51ba1d8 docker: drop arm v7 + +2024.03.16, v17.1.0 + +feature: +- 10d6d2e2 cloudcmd: @types/node-fetch v2.6.11 +- 2047cb7a cloudcmd: @cloudcmd/dropbox v5.0.1 +- 6b793cca cloudcmd: docker: alpine +- 5fa9fcc5 cloudcmd: pullout v5.0.0 +- bc617c17 cloudcmd: serve-once v3.0.1 + +2024.03.12, v17.0.7 + +feature: +- 97627dc2 cloudcmd: auto-globals v4.0.0 +- 683c865e cloudcmd: gritty v8.0.0 + +2024.03.11, v17.0.6 + +fix: +- d928c0b8 cloudcmd: ocker: revert alpine (#406) + +2024.03.11, v17.0.5 + +fix: +- 33201dad cloudcmd: docker: alpine (#406) + +2024.02.02, v17.0.4 + +feature: +- 7ce95450 cloudcmd: deepword v9.0.0 +- 1c73e525 cloudcmd: edward v15.0.0 +- da967f08 cloudcmd: dword v15.0.0 +- f0a6109a cloudcmd: restafary v12.0.0 + +2024.02.01, v17.0.3 + +feature: +- aca4119f cloudcmd: inly v5.0.0 + +2024.02.01, v17.0.2 + +feature: +- 5324a41a cloudcmd: supertape v10.0.0 +- d453a1b2 cloudcmd: onezip v6.0.1 +- 71b915be cloudcmd: @cloudcmd/fileop v8.0.0 + +2024.01.25, v17.0.1 + +feature: +- d79a5776 cloudcmd: putout v35.0.0 +- 8d92aa91 cloudcmd: package-json v9.0.0 +- 5ab5576e cloudcmd: open v10.0.3 +- c5cfe68c cloudcmd: c8 v9.1.0 + +2023.12.12, v17.0.0 + +feature: +- 154e3b07 cloudcmd: find-up v7.0.0 +- a02d288d cloudcmd: @putout/babel v2.0.0 +- 3314f9b9 drop support of node < 18 +- a6afa205 cloudcmd: supertape v9.0.0 +- e600c05d cloudcmd: eslint-plugin-putout v22.0.0 +- 99c00954 cloudcmd: madrun v10.0.0 +- afdf7434 cloudcmd: putout v34.0.7 + +2023.12.08, v16.18.0 + +feature: +- 4462f269 cloudcmd: markdown-it v14.0.0 +- 0cc76bd4 cloudcmd: philip v3.0.0: decrease bundle size +- 656ebd87 client: move out set-current + +2023.12.04, v16.17.9 + +fix: +- fb2d0814 github actions + +2023.12.04, v16.17.8 + +feature: +- 93aa7278 yaspeller: rm +- af9c916e cloudcmd: eslint-plugin-putout v21.0.2 +- 01dccbfd cloudcmd: putout v33.13.3 + +2023.10.12, v16.17.7 + +feature: +- 7857fb72 package: rendy v4.1.3 + +2023.09.22, v16.17.6 + +feature: +- 570cb8c0 package: nodemon v3.0.1 +- 828d10a8 package: rimraf v5.0.1 +- 88908b35 package: eslint-plugin-putout v20.0.0 +- 93f4a07e package: putout v32.0.6 +- a5f93523 github actions: use bun + +2023.09.06, v16.17.5 + +feature: +- 3b0941bc actions: docker/login-action@v2 (#396) + +2023.09.05, v16.17.4 + +feature: +- 35dedfdf github: update docker actions (#396) + +2023.09.05, v16.17.3 + +feature: +- 3c93b29b github: docker/build-push-action v4 (#396) + +2023.09.04, v16.17.2 + +fix: +- 621f52da docker + +2023.09.04, v16.17.1 + +fix: +- eb3f037a docker: bun -> node + +2023.09.04, v16.17.0 + +feature: +- 224e5397 docker: use bun instead of node +- 4b9267f3 package: edward v14.2.0 + +2023.08.10, v16.16.3 + +feature: +- 354c137d package: console-io v14.0.0 + +2023.08.09, v16.16.2 + +feature: +- e353fff7 package: redzip v3.0.0 + +2023.08.07, v16.16.1 + +feature: +- d63169cc package: @putout/babel v1.0.5 +- bb9276be package: eslint-plugin-putout v19.0.3 +- 9aed5f75 package: putout v31.0.3 + +2023.07.09, v16.16.0 + +feature: +- c4f56c59 package: memfs v4.2.0 +- 0e663e1d package: @putout/plugin-cloudcmd v3.1.1 +- ff9265b7 package: c8 v8.0.0 +- 22aa337a package: eslint-plugin-n v16.0.1 +- 13350b55 package: eslint-plugin-putout v18.0.0 +- ce196abf package: putout v30.1.1 + +2023.05.17, v16.15.0 + +feature: +- a1bf40bd package: open v9.1.0 +- ef608853 client: self signed certs on Chrome (#393) + +2023.03.21, v16.14.1 + +feature: +- b336a472 package: eslint-plugin-putout v17.1.0 +- 019e15b8 package: @cloudcmd/fileop v7.0.0 +- 64354300 package: copymitter v8.0.1 +- 27437880 package: @cloudcmd/move-files v7.0.0 + +2023.03.08, v16.14.0 + +feature: +- 6b22b241 package: putout v29.0.3 + +2023.01.30, v16.13.1 + +feature: +- client: add DIR_DIST +- client: DIRCLIENT -> DIR_CLIENT + +2023.01.29, v16.13.0 + +feature: +- client: key: vim: open editor with 'e' +- client: vim: add ability to show terminal with 'tt' + +2023.01.29, v16.12.0 + +feature: +- client: vim: add ability to create directory with 'md', and create file with 'mf' + +2023.01.22, v16.11.0 + +fix: +- lint: eslint-plugin-node -> eslint-plugin-n + +feature: +- user-menu: add support of mov +- client: user-menu: navigate: add support of \D + [JK] to speed up vim navigation + +2023.01.19, v16.10.0 + +fix: +- lint: eslint-plugin-node -> eslint-plugin-n + +feature: +- client: user-menu: navigate: add support of \D + [JK] to speed up vim navigation + +2023.01.18, v16.9.1 + +fix: +- static: user-menu: convert flac to mp3 + +2023.01.17, v16.9.0 + +feature: +- package: scroll-into-view-if-needed v3.0.4 +- package: tar-stream v3.0.0 +- static: user-menu: add recipes from Cookbook + +2023.01.16, v16.8.0 + +feature: +- package: @putout/plugin-cloudcmd v2.0.0: CloudCmd.loadDir() -> CloudCmd.changeDir()' + +2023.01.15, v16.7.0 + +feature: +- package: @cloudcmd/modal v3.0.0: add ability to not set cursor when close the modal +- package: auto-globals v3.0.0 +- package: rimraf v4.0.5 +- user-menu: rm border + +2022.10.20, v16.6.1 + +feature: +- package: package-json v8.1.0 +- package: supertape v8.1.0 +- package: putout v28.0.0 + +2022.10.09, v16.6.0 + +feature: +- package: @cloudcmd/fileop v6.0.0 +- package: @cloudcmd/move-files v6.0.0 +- package: copymitter v7.0.0 + +2022.08.06, v16.5.0 + +feature: +- client: add Command Line +- package: add funding +- (package) gritty v7.0.0 + +2022.07.20, v16.4.1 + +feature: +- (package) eslint-plugin-n v15.2.4 +- (package) putout v27.0.1 +- (package) eslint-plugin-putout v16.0.0 + + +2022.07.11, v16.4.0 + +feature: +- (cloudcmd) env: add ability to pass 0 and 1 + + +2022.07.02, v16.3.1 + +fix: +- (client) cloudcmd: rm window.Emitify + + +2022.07.01, v16.3.0 + +feature: +- (cloudcmd) terminal-run: return -1, when not load + + +2022.06.17, v16.2.0 + +feature: +- (package) markdown-it v13.0.1 +- (cloudcmd) server: convert to ESM +- (package) thread-it v2.0.0 + + +2022.05.12, v16.1.1 + +feature: +- (package) edward v14.0.0 +- (package) putout v26.0.1 + + +2022.04.23, v16.1.0 + +feature: +- (cloudcmd) improve support of NBSP + + +2022.04.22, v16.0.1 + +feature: +- (package) dword v14.0.0 +- (package) restafary v11.0.0 +- (package) @cloudcmd/stub v4.0.1 +- (package) win32 v7.0.0 +- (package) eslint-plugin-putout v14.4.0 + + +2022.02.19, v16.0.0 + +feature: +- (cloudcmd) drop support of node < 16 +- (package) supertape v7.1.0 +- (package) eslint-plugin-putout v13.11.0 +- (package) madrun v9.0.0 +- (package) putout v25.0.1 + +2022.01.20, v15.9.15 + +fix: +- (css) icons (#368) + +2022.01.13, v15.9.14 + +fix: +- (cloudcmd) client: edit-names in vim mode +- (docker) images: make dockerfiles use node:lts-buster and node:lts-buster-slim for alpine (#363) +- (docker) images: make dockerfiles use node:lts-buster-slim as base image (#357) +feature: +- (package) eslint-plugin-putout v13.0.1 +- (package) putout v24.0.2 + +2021.12.23, v15.9.13 + +feature: +- (package) putout v23.0.0 + +2021.12.16, v15.9.12 + +fix: +- (client) loadDir: history enabled by default + +2021.12.09, v15.9.11 + +fix: +- (client) load: Upload stuck for zero byte files (#359) + +2021.12.04, v15.9.10 + +fix: +- (client) operation: move and copy not working (#358) +feature: +- (package) eslint-plugin-putout v12.2.0 + +2021.11.22, v15.9.9 + +feature: +- (package) putout v22.0.2 +- (package) yargs-parser v21.0.0 +- (package) yaspeller v8.0.0 +- (package) eslint-plugin-putout v11.1.0 +- (package) putout v21.1.2 +- (package) eslint v8.0.1 +- (package) eslint-plugin-putout v10.3.0 +- (package) find-up v6.1.0 +- (package) putout v20.5.1 +- (package) supertape v6.9.1 +- (package) eslint-plugin-putout v9.0.1 + +2021.08.09, v15.9.8 + +fix: +- (cloudcmd) args: snake case -> camelCase: yargs strip dashed + + +2021.06.27, v15.9.7 + +fix: +- (config) rm broadcast: cannot change config +- (view) close modals using ESC + +feature: +- (package) package-json v7.0.0 +- (config) add more descriptive titles + + +2021.06.13, v15.9.6 + +feature: +- (package) @cloudcmd/move-files v5.0.0 + + +2021.06.08, v15.9.5 + +fix: +- (load) put: replace #: single -> multiple +- (server) user-menu: e.message -> errorFind.message + +feature: +- (package) eslint-plugin-putout v8.0.1 +- (package) es6-promisify v7.0.0 +- (package) putout v18.0.1 +- (package) putout v17.0.0 + + +2021.04.05, v15.9.4 +feature: +- (package) socket.io-client v4.0.1 +- (package) open v8.0.5 +- (package) putout v16.4.0 + +2021.03.17, v15.9.3 + +fix: +- (cloudcmd) Docker latest-alpine (15.9.2) stopped with errors (#337) + + +2021.03.16, v15.9.2 + +fix: +- (route) unable to navigate between folders (#333) + +feature: +- (package) open v8.0.2 +- (package) socket.io-client v4.0.0 +- (package) socket.io v4.0.0 + + +2021.03.01, v15.9.1 + +feature: +- (cloudcmd) icons: add archive icon for small screens +- (cloudcmd) add archive-link image + + +2021.03.01, v15.9.0 + +feature: +- feature(cloudcmd) add archive icon +- feature(client) rm unused loadCurrentTime +- feature(package) win32 v6.0.0 +- feature(package) redzip v2.0.0 + + + +2021.02.27, v15.8.1 + +feature: +- (package) restafary v9.7.0 (#330) + + +2021.02.23, v15.8.0 + +feature: +- (cloudcmd) add ability to suggest option, when wrong provided +- (package) supertape v5.0.0 + + +2021.02.21, v15.7.1 + +fix: +- (cloudcmd) prefix env has no effect (#328) + + +2021.02.19, v15.7.0 + +fix: +- chore(github-actions) add ability to autofix lint + +feature: +- (package) @cloudcmd/move-files v4.0.0 +- (package) putout v15.0.0 +- (package) copymitter v6.0.0 +- (package) @cloudcmd/olark v3.0.2 +- (client) view: unknown file type: method: GET -> HEAD: speed up + + +2021.02.03, v15.6.0 + +feature: +- (client) view: add ability to detect file type when extension is missing (coderaiser/cloudcmd#287) + + +2021.02.02, v15.5.2 + +fix: +- (client) view: isAudio (#322) + + +2021.01.31, v15.5.1 + +fix: +- (cloudcmd) cannot create a directory (#319) + + +2021.01.30, v15.5.0 + +feature: +- (package) style-loader v2.0.0 +- (cloudcmd) improve error handling when viewing or editing a file +- (package) putout v14.0.0 +- (package) eslint-plugin-putout v7.0.0 + + +2021.01.26, v15.4.4 + +feature: +- (package) win32 v5.1.11 + + +2021.01.25, v15.4.3 + +feature: +- (package) restbox v3.0.0 +- (package) ponse v6.0.0 +- (package) files-io v4.0.0 + + +2021.01.21, v15.4.2 + +feature: +- (package) mellow v3.0.0 +- (route) tokenize -> wraptile + + +2021.01.20, v15.4.1 + +feature: +- (route) redzip -> win32 (#317) + + +2021.01.19, v15.4.0 + +feature: +- (cloudcmd) add abilit to parse markdown inside zip archives + + +2021.01.19, v15.3.4 + +fix: +- (docker) images: change alpine images to be part of master image (#316) +- (dom) bug in Firefox with Imagus extension enabled: change getCurrentName to read from `data-name` instead of `title` (#313) + +feature: +- (package) deepword v8.0.0 +- (package) console-io v13.0.0 +- (package) edward v13.0.0 +- (docker) add support of multi-arch builds (#291) + + +2021.01.18, v15.3.3 + +fix: +- (dom) bug in Firefox with Imagus extension enabled: change getCurrentName to read from `data-name` instead of `title` (#313) +- (client) dom: goToDirectory + + +2021.01.18, v15.3.2 + +fix: +- (client) dom: goToDirectory + + +2021.01.17, v15.3.1 + +fix: +- (view) view html, pdf + +feature: +- (package) dword v13.0.0 +- (modules) socket.io v3.1.0 + + +2021.01.17, v15.3.0 + +feature: +- (client) improve vim support: Esc - toggle vim support in editors +- (client) view: add ability to view raw files using Shift + F3 +- (client) view: show markdown without shift +- (key) add ability to toggle global vim config on the time of session + + +2021.01.16, v15.2.0 + +fix: +- (server) distribute: simplify colors + +feature: +- (cloudcmd) add ability to open html files using F3 +- (cloudcmd) add ability to toggle vim hotkes using Esc +- (package) @cloudcmd/fileop v5.0.0 + + +2021.01.07, v15.1.0 + +feature: +- (cloudcmd) add ability to read zip files like directories (#309) + + +2021.01.05, v15.0.4 + +feature: +- (package) flop v9.0.0 +- (package) restafary v9.0.1 + + +2021.01.05, v15.0.3 + +fix: +- (cloudcmd) validateRoot: import -> simport: forEachKey is not a function (#311) + +feature: +- (package) onezip v5.0.0 +- (package) socket.io v3.0.5 (socketio/socket.io#3720) + + +2020.12.31, v15.0.2 + +fix: +- (cloudcmd) criton: crash when password set (#310) + + +2020.12.30, v15.0.1 + +fix: +- (cloudcmd) css: media query for screen size with width more then 1600 (cloudcmd/console-io#6) +- (cloudcmd) client: build: optional chaining +- (cloudcmd) drop support of node < 14 + +feature: +- (package) putout v13.0.0 + + +2020.12.28, v15.0.0 + +fix: +- (cloudcmd) app, help: remove duplicate keys +- chore(cloudcmd) menu: add lint, :lint, test, coverage +- (cloudcmd) importListen prevent server from start (#295) + +feature: +- (package) auto-globals v2.0.0 +- (cloudcmd) convert to EcmaScriptModules +- (package) supertape v4.1.0 +- (package) supertape v3.10.0 +- (package) babel-plugin-macros v3.0.0 +- (package) putout v12.0.0 +- (package) supertape v3.1.3 +- (package) madrun v8.0.0 +- (package) gritty v6.0.0 +- (package) console-io v12.0.0 +- (package) socket.io-client v3.0.1 +- (package) socket.io v3.0.1 +- (package) putout v11.0.2 +- (package) eslint-plugin-putout v6.0.0 +- (package) markdown-it v12.0.0 +- (package) putout v10.0.0 +- (package) gritty v5.0.0 +- (distribute) import: do not import if env variable set +- (package) drop support of node < 12 +- (package) table v6.0.1 + + +2020.08.21, v14.9.3 + +fix: +- (client) dom: buffer: get, set -> getJson, setJson (#295) + +feature: +- (storage) rm unused removeMatch + + +2020.08.19, v14.9.2 + +fix: +- (cloudcmd) client: edit: json files (#294) + + +2020.08.16, v14.9.1 + +feature: +- (cloudcmd) add IO.createDirectory (https://github.com/coderaiser/putout/commit/b54b5486f438804120df261dafa8e6985201a4eb) + + +2020.08.14, v14.9.0 + +feature: +- (key) vim: add ability to navigate to next and previous using w and b +- (key) vim: add ability to navigate using to first and last file using ^ and $ + + +2020.08.14, v14.8.0 + +feature: +- (cloudcmd) IO.cp -> IO.copy +- (cloudcmd) IO.mv -> IO.move +- (package) find-up v5.0.0 + + +2020.08.11, v14.7.2 + +fix: +- (client) key: F3: search appears + + +2020.08.11, v14.7.1 + +fix: +- (view) init config: copy + + +2020.08.10, v14.7.0 + +fix: +- (client) user menu: click on user menu title +- test(cloudcmd) test/server/prefixer -> server/prefixer + +feature: +- (cloudcmd) rest: add rename +- (rest) mv: improved user errors +- (client) io: promisify -> async +- (package) webpack-merge v5.0.8 +- (package) eslint-plugin-putout v5.0.0 +- (package) putout v9.0.0 +- (package) madrun v7.0.0 + + +2020.05.20, v14.6.0 + +feature: +- (package) markdown-it v11.0.0 +- (cloudcmd) client: add ability to handle existing file while rename (#289) +- (package) supertape v2.0.0 +- (package) @cloudcmd/stub v3.0.0 + + +2020.05.06, v14.5.1 + +fix: +- (docker) alpine + + +2020.05.04, v14.5.0 + +fix: +- (user-menu) choose using Enter + +feature: +- (package) eslint v7.0.0 +- (package) eslint-plugin-putout v4.0.0 +- (package) putout v8.0.0 +- (package) madrun v6.0.0 +- (client) directory: add abily to show progress dialog on upload + + +2020.04.27, v14.4.0 + +feature: +- (user-menu) add ability to run selected items without showing dialog + + +2020.04.13, v14.3.10 + +fix: +- (util) getRegExp: multiple dots +- (cloudcmd) crash on check for update (#286) + +feature: +- (package) yaspeller v7.0.0 + + +2020.04.10, v14.3.9 + +fix: +- (docker) node -> node:lts (#280) +- (env) UPPER_CASE first, and then lower_case (#283) + +feature: +- (cloudcmd) promise -> async + + +2020.04.04, v14.3.8 + +feature: +- (client) ajaxLoad: callback -> promise +- (get-json-from-file-table) mv out from client + + +2020.04.03, v14.3.7 + +fix: +- (client) dom: getCurrentType + +feature: +- (package) chalk v4.0.0 + + +2020.03.31, v14.3.6 + +fix: +- (cloudcmd) directoryStorage + + +2020.03.25, v14.3.5 + +feature: +- (package) fullstore v3.0.0 +- (package) html-webpack-plugin v4.0.1 +- (package) codegen.macro v4.0.0 +- (package) url-loader v4.0.0 + + +2020.03.03, v14.3.4 + +feature: +- (package) readjson v2.0.1 +- (package) tryrequire v3.0.0 +- (package) flop v8.0.0 +- (package) writejson v3.0.0 +- (package) try-to-catch v3.0.0 +- (package) try-catch v3.0.0 + + +2020.02.23, v14.3.3 + +feature: +- (key) vim: split DOM and vim + + +2020.02.21, v14.3.2 + +feature: +- (package) serve-once v2.0.0 +- (package) edward v12.0.0 +- (package) dword v12.0.0 +- (package) ponse v5.0.0 + + +2020.02.21, v14.3.1 + +feature: +- (package) supermenu v4.0.1 +- (package) stylelint-config-standard v20.0.0 +- (package) http-auth v4.1.2 + + +2020.01.29, v14.3.0 + +feature: +- (listeners) improve file select with ctrl + click (#269) +- (docker) add Dockerfile for arm based clients such as rpi (#268) +- (package) @cloudcmd/clipboard v2.0.0 +- (package) stylelint v13.0.0 +- (package) emitify v4.0.1 +- (package) nyc v15.0.0 +- (package) eslint-plugin-node v11.0.0 + + +2019.12.21, v14.2.1 + +feature: +- (package) for-each-key v2.0.0 +- (package) jonny v3.0.0 +- (package) jaguar v6.0.0 +- (package) ponse v4.0.0 +- (package) ponse v3.5.1 +- (package) memfs v3.0.1 +- (package) url-loader v3.0.0 +- (package) nodemon v2.0.1 +- (package) stylelint v12.0.0 + + +2019.11.15, v14.2.0 + +feature: +- (cloudcmd) add CLOUDCMD_PREFIX +- (package) chalk v3.0.0 +- (package) eslint-plugin-putout v3.0.0 +- (package) madrun v5.0.0 +- (package) putout v7.0.0 +- (package) madrun v4.0.0 +- (package) try-to-catch v2.0.0 + + +2019.10.16, v14.1.2 + +fix: +- (madrun) client test command: test:client command (#260) + +feature: +- (package) @cloudcmd/fileop v4.0.0 +- (package) inly v4.0.0 +- (package) onezip v4.0.0 +- (package) open v7.0.0 +- (package) multi-rename v2.0.0 +- (package) shortdate v2.0.0 + + +2019.10.09, v14.1.1 + +feature: +- (package) @cloudcmd/fileop v3.0.0 +- (package) @cloudcmd/move-files v3.0.0 +- (package) copymitter v5.0.0 + + +2019.10.02, v14.1.0 + +feature: +- (view) add support of webm (#256) + + +2019.09.25, v14.0.2 + +fix: +- (client) change directory with enabled dirStorage (#255) + + +2019.09.25, v14.0.1 + +fix: +- (cloudcmd) wrong editor root on multi instance + +feature: +- (package) deepword v7.0.0 +- (cloudcmd) rm now (zeit/micro/issues/377) + + +2019.09.22, v14.0.0 + +feature: +- (operation) show progress bar on first operation (#253) +- (package) pullout v4.0.0 +- (package) console-io v11.0.0 +- (package) flop v7.0.0 +- (package) restbox v2.0.0 +- (package) restafary v8.0.0 +- (package) nomine v4.0.0 +- (cloudcmd) drop support of node < 10 +- (cloudcmd) remove --progress option, make always enabled + + +2019.09.21, v13.4.2 + +fix: +- (terminal) on -> onKey + + +2019.09.20, v13.4.1 + +feature: +- (package) @cloudcmd/olark v3.0.0 +- (package) format-io v2.0.0 +- (package) limier v3.0.0 +- (package) currify v4.0.0 +- (package) limier v2.0.1 +- (package) load.js v3.0.0 +- (package) @cloudcmd/create-element v2.0.0 +- (package) supermenu v3.0.0 +- (package) smalltalk v4.0.0 +- (package) @cloudcmd/modal v2.0.0 +- (package) putout v6.0.0 +- (package) stylelint-config-standard v19.0.0 +- (package) stylelint v11.0.0 +- (package) markdown-it v10.0.0 +- (package) rm version-io +- (package) fullstore v2.0.2 +- (package) wraptile v3.0.0 +- (package) rendy v3.0.0 +- (package) eslint-plugin-node v10.0.0 +- (package) restafary v7.0.0 + + +2019.09.03, v13.4.0 + +feature: +- (view) add support of pdf +- (package) madrun v3.0.1 + + +2019.08.28, v13.3.0 + +fix: +- feature(client) remove legacy suffix, use modules as is +- (client) rm additional dialog +- (user-menu) putout path +- (edit-names) hide +- (vim) find + +feature: +- (cloudcmd) decrease bundle size: disable convertion to es5 +- (client) remove legacy suffix, use modules as is +- (package) es6-promisify v6.0.2 +- (menu) rm mute: has no sense with new ponse headers +- (markdown) move out parse to worker + + +2019.08.22, v13.2.1 + +fix: +- (buffer) copy, cut +- (key) chrome event double pass +- (user-menu) processing menu item run error + +feature: +- (user-menu) rm path + + +2019.08.20, v13.2.0 + +fix: +- (menu) autoplay on download in firefox +- (dom) getCurrentData + +feature: +- (user-menu) improve error handling +- (package) eslint-plugin-putout v2.0.0 + + +2019.08.16, v13.1.0 + +fix: +- (user-menu) hide load on error +- (key) keys block + +feature: +- (cloudcmd) add ability to remember position of current file on reload +- (madrun) disable web workers, while testing +- (user-menu) speed up user menu parsing with help of worker threads +- (user-menu) add ability to used EcmaScript Modules, improve error handling +- (server) add compress: speed up client side load + + +2019.08.14, v13.0.1 + +fix: +- (cloudcmd) rm --user-menu: always enabled + +feature: +- (package) rimraf v3.0.0 + + +2019.08.14, v13.0.0 + +fix: +- (set-listeners) can not continue first error operation error +- (view) getMediaElement: play video/audio + +feature: +- (cloudcmd) only user menu on f2 +- (cloudcmd) remove plugins +- (package) putout v5.2.1 +- (package) style-loader v1.0.0 + + +2019.08.02, v12.6.3 + +fix: +- (operation) first copy after startup fails (close #251) +- (key) chrome keydown event double pass +- (client) load +- (cloudcmd) server start when importListen enabled + +feature: +- (cloudcmd) simplify lint +- (package) markdown-it v9.0.0 +- (package) deepmerge v4.0.0 +- (package) yaspeller v6.0.0 +- (package) css-loader v3.0.0 +- (babel) remove unused @babel/plugin-syntax-dynamic-import +- (cloudcmd) rm Promise polyfill +- (package) rm Object.assign polyfill + + +2019.06.06, v12.6.2 + +fix: +- (cloudcmd) config: save -> write (#247) + +feature: +- (package) remove file-loader +- (package) url-loader v2.0.0 + + +2019.06.04, v12.6.1 + +fix: +- (config) configManager: totally move away from singletone + + +2019.05.31, v12.6.0 + +feature: +- (config-manager) add (#208) +- (package) add support of cwd in TerminalRun with help of gritty v4.7.0 +- (user-menu) add ability to ignore private methods + + +2019.05.28, v12.5.0 + +feature: +- (user-menu) add Compare Directories (#220) + + +2019.05.27, v12.4.0 + +fix: +- (terminal-run) resolve before close + +feature: +- (terminal-run) add ability to resolve exit code + + +2019.05.24, v12.3.2 + +fix: +- (user-menu) F1 and F keys overlapping +- (select-by-pattern) crash + + +2019.05.24, v12.3.1 + +fix: +- (cloudcmd) copy + + +2019.05.24, v12.3.0 + +fix: +- (user-menu) prefix (#244) +- (dom) renameCurrent: try-to-promise RESTful.mv + +feature: +- (user-menu) hide scroll when small amount of items +- (user-menu) add ability to run in mobile devices +- (upload) improve style +- (user-menu) imrove error output +- (user-menu) add default user menu +- (dialog) cancel -> tryToCatch +- (user-menu) add default option: create user menu +- (user-menu) add error handling +- (user-menu) add ability to navigate with j, k +- (cloudcmd) deprecate plugins + + +2019.05.13, v12.2.0 + +fix: +- feature(npmignore) add fixture.json +- (operation) set-listeners: alert: noCancel -> cancel +- (cloudcmd) different title + +feature: +- (key) add ability to rename file with Shift + F6 +- (user-menu) add (#221) +- (package) clean-css-loader v2.0.0 +- (package) edward v11.0.0 +- (npmignore) add fixture.json +- (package) dword v11.0.0 +- (package) smalltalk v3.4.0 +- (package) eslint v6.0.0-alpha +- (package) eslint-plugin-node v9.0.1 +- (config) speed up load: series -> parallel +- (cloudcmd) avoid service worker reoload scripts in dev mode +- (package) putout v4.23.0 +- (package) nyc v14.0.0 + + +2019.04.15, v12.1.0 + +fix: +- (heroku) app: add "}" (#236) + +feature: +- (edit-names) add bulk rename (#231) +- (package) stylelint v10.0.0 + + +2019.04.04, v12.0.2 + +fix: +- (listeners) any touch open file (#235) + + +2019.04.04, v12.0.1 + +fix: +- (listeners) unable to navigate into folders (#235) + + +2019.04.01, v12.0.0 + +fix: +- feature(cloudcmd) split prefix and prefixSocket finally + +feature: +- (cloudcmd) drop support of beautify, minify +- (cloudcmd) improved progress of file operations +- (cloudcmd) split prefix and prefixSocket finally +- (cloudcmd) view file on double click or enter +- (cloudcmd) add ability to change directory using F3 + + +2019.03.27, v11.8.6 + +fix: +- (operation) progress +- (terminal) rm IntersectionObserver handling (xtermjs/xterm.js#1929) + +feature: +- (package) opn -> open v6.0.0 +- (package) remove @babel/plugin-proposal-object-rest-spread + + +2019.02.20, v11.8.5 + +fix: +- (plugins) set content-type (#230) + +feature: +- (package) putout v4.0.0 +- (package) madrun v2.0.0 + + +2019.02.15, v11.8.4 + +fix: +- (terminal) chrome does not display anything on first open (#214) +- (rest) can not read ###files (#223) +- (help) add --terminal-auto-restart +- (cloudcmd) read root from custom config (#224) +- refactor(view) viewImage: href, prefixURL -> prefixURL +- docs(help) improve English translation of HELP.md (#211) + +feature: +- (package) tar-stream v2.0.0 +- (package) redrun -> madrun +- (package) unionfs v4.0.0 +- (package) package-json v6.0.0 +- (package) add @putout/eslint-config +- (package) tape -> supertape +- (package) add putout +- (package) add eslint-plugin-tape +- (package) file-loader v3.0.1 +- (package) css-loader v2.0.1 +- (package) kebabToCamelCase -> just-pascal-case +- (package) deepmerge v3.0.0 + + +2018.12.04, v11.8.3 + +fix: +- test(export) defaultConfig: prefix +- (edit) prefix (#207) +- feature(package) :lint: add eslint-remove-line + +feature: +- (modules) socket.io v2.2.0 +- (package) clear-module -> mock-require +- (package) auto-globals v1.7.0 +- (package) fix:lint: add eslint-remove-line +- (cloudcmd) sinon, sinon-called-with-diff -> @cloudcmd/stub + + +2018.11.22, v11.8.2 + +fix: +- (Dockerfile) WORKDIR (#182) + + +2018.11.14, v11.8.1 + +fix: +- (validate) corruption of config when root not found + +feature: +- (package) pullout v3.0.0 +- (validate) root: do not validate dropbox root + + +2018.11.09, v11.8.0 + +fix: +- (dialog) noFiles: reject on close + +feature: +- (package) add dropbox support +- (server) add graceful shutdown +- (package) sinon-called-with-diff v3.0.0 + + +2018.11.01, v11.7.1 + +fix: +- (cloudcmd) register service worker on http connection (#203) +- refactor(cloudcmd) PREFIX -> prefix +- docs(help) example: prefix -> express mount point + +feature: +- (package) criton v2.0.0 +- (package) eslint-plugin-node v8.0.0 + + +2018.10.25, v11.7.0 + +fix: +- feature(cloudcmd) add ability to set prefix for web sockets connections with --prefix-socket (#200) + +feature: +- (cloudcmd) add ability to set prefix for web sockets connections with --prefix-socket (#200) +- (package) deepword v5.0.0 +- (package) dword v9.0.0 +- (package) edward v9.0.0 +- (package) console-io v10.0.0 +- (package) ponse v3.0.0 +- (package) files-io v3.0.0 + + +2018.10.23, v11.6.0 + +feature: +- (cloudcmd) add support of express mounting point (#200) + + +2018.10.23, v11.5.4 + +fix: +- docs(changelog) rm + +feature: +- (package) supermenu v2.1.2 +- (npmignore) add ai, cdr, eps + + +2018.10.19, v11.5.3 + +feature: +- (package) console-io v9.0.0 +- (npmignore) add fixture + + +2018.10.19, v11.5.2 + +fix: +- (server) open error: exit -> warning (#196, #197, #198) + + +2018.10.17, v11.5.1 + +fix: +- (contact) load + +feature: +- (olark) bower -> npm + + +2018.10.16, v11.5.0 + +feature: +- (config) add ability to remove auth change from config with help of --config-auth flag + + +2018.10.16, v11.4.1 + +fix: +- (Docker) cloudcmd_open false (#197) + +feature: +- (package) sinon v7.0.0 + + +2018.10.12, v11.4.0 + +feature: +- (cloudcmd) add env variable CLOUDCMD_OPEN + + +2018.10.12, v11.3.1 + +fix: +- (server) crash on open error (#196) + +feature: +- (cloudcmd) add unhandledRejection listener +- (exit) add ability to receive error as argument + + +2018.10.11, v11.3.0 + +feature: +- (img) add file-link, directory-link +- (cloudcmd) show size as for links +- (cloudcmd) readify: add support of links +- (package) nomine v3.0.0 +- (package) restafary v5.0.0 +- (package) flop v6.0.0 +- (package) currify v3.0.0 + + +2018.10.04, v11.2.3 + +feature: +- (package) rm unused join-io +- (package) apart v2.0.0 +- (server) callback -> await + + +2018.09.28, v11.2.2 + +fix: +- (cloudcmd) prefix with leading slash (#195) + + +2018.09.28, v11.2.1 + +fix: +- (sw) cache favicon.ico from root (#194) + +feature: +- (package) onezip v3.0.0 + + +2018.09.28, v11.2.0 + +feature: +- (package) inly v3.0.0 +- (package) jaguar v5.0.0 +- (cloudcmd) add --terminal-auto-restart + + +2018.09.27, v11.1.0 + +feature: +- (package) redrun v7.0.0 +- (package) @cloudcmd/read-files-sync v2.0.0 +- (cloudcmd) --terminal-command: command to run in terminal (shell by default) +- (package) gritty v4.0.0 +- (package) console-io v8.0.0 + + +2018.09.26, v11.0.0 + +feature: +- (package) babel-eslint v10.0.0 +- (package) deepword v4.0.0 +- (package) edward v8.0.0 +- (package) dword v8.0.0 +- (package) restafary v4.0.0 +- (package) flop v5.0.0 +- (package) @cloudcmd/fileop v2.0.0 +- (package) @cloudcmd/move-files v2.0.1 +- (package) copymitter v4.0.2: add ability to copy symlinks (#191) +- (config) disable online by default +- (config) enable open by default +- (package) set engine to node v8.0.0 LTS +- (package) package-json v5.0.0 +- (server) drop support of VCAP_APP_PORT +- (cloudcmd) showUpdateInfo: rm rendy +- (package) squad v3.0.0 + + +2018.09.21, v10.8.2 + +fix: +- (distribute) export: config +- (sw) register: null -> stub + + +2018.09.20, v10.8.1 + +fix: +- (view) change elements position when open image + + +2018.09.19, v10.8.0 + +feature: +- (package) load -> load.js + @cloudcmd/create/element +- (cloudcmd) rm jquery + + +2018.09.14, v10.7.0 + +fix: +- (cloudcmd) onUpdateFound: doubled prefix for cloudcmd.common.js (#188) + +feature: +- (view) fancybox -> @cloudcmd/modal + + +2018.09.10, v10.6.0 + +fix: +- (view) title: xss +- (sw) navigation error + +feature: +- (events) add ability to remove all added listeners +- (cloudcmd) add ability update cloudcmd without page reload +- (cloudcmd) add --show-file-name to add file name title in view and edit (#183) + + +2018.09.08, v10.5.3 + +fix: +- (webpack) css: font resource ignores prefix setting (#185) +- (sw) no auth dialog when serve by service worker +- (sw) http auth + +feature: +- (package) babel-loader v8.0.0 +- (package) babel v7.0.0 +- (package) table v5.0.2 +- (package) style-loader v0.23.0 +- (package) nyc v13.0.1 +- (package) babel-eslint v9.0.0 + + +2018.08.25, v10.5.2 + +fix: +- (konsole) try to load console.js when console is disabled + + +2018.08.25, v10.5.1 + +fix: +- (log) makeColor: mac os + +feature: +- (export) add config send log + + +2018.08.23, v10.5.0 + +fix: +- (edit-file) multiple f4 +- (config) init: load when configDialog disabled +- (name-size-date) .name width +- (listeners) isNoCurrent: can not go to root when root is empty +- (client) parallel -> series: route + +feature: +- (distribute) add ability to import config from remote export server +- (package) file-loader v2.0.0 +- (package) watch:lint:server +- (package) add watch:test:server +- (package) yaspeller v5.0.0 +- (package) style-loader v0.22.0 +- (package) serviceworker-webpack-plugin v1.0.1 + + +2018.07.18, v10.4.1 + +fix: +- (img) loading: svg minify + + +2018.07.17, v10.4.0 + +fix: +- (sw) cache of 401 +- (polyfill) scrollIntoViewIfNeeded in firefox + +feature: +- (package) engines: node v4.5.0 +- (package) eslint-plugin-node v7.0.0 +- (package) @cloudcmd/clipboard v1.0.2 +- (package) rm unused html-webpack-exclude-assets-plugin +- (package) gritty v3.0.0 +- (package) optimize-css-assets-webpack-plugin v5.0.0 +- (package) @cloudcmd/clipboard v1.0.1 +- (package) clear-module v3.0.0 +- (package) smalltalk v3.1.0 +- (package) css-loader v1.0.0 +- (Dockerfile) mhart/alpine-node -> node:alpine + + +2018.07.02, v10.3.2 + +fix: +- (terminal) prevent init when config disabled +- (operation) next current file after delete +- (key) vim: do not react on navigation after view hide + +feature: +- (package) codegen.macro v3.0.0 + + +2018.06.25, v10.3.1 + +fix: +- (npmignore) rm dist*/modules + +feature: +- (package) eslint v5.0.0 + + +2018.06.22, v10.3.0 + +fix: +- (operation) set next file current when can not delete + +feature: +- (webpack) minimize service worker size +- (terminal) move out terminal.css from view.css +- (package) fast-async v7.0.6 +- (cloudcmd) import -> load.js +- (load-module) add + + 2018.06.21, v10.2.3 fix: @@ -3937,7 +6766,7 @@ fix: - (rest) onDelete: func(null, body) -> func - (rest) onStat: add var - (cloudcmd) change index path -- (server) start: url -> URL +- (server) start: url -> PREFIX - (server) start: SSLPort -> sslPort - (server) start: Port -> port - (dom) getCurrentDirPath: "," -> ";" @@ -4949,7 +7778,7 @@ with clicks on files. - Removed allowed from cache property in config. - Added ability to hide "Upload To" menu item if -no storage modules setted up in modules.json. +no storage modules set up in modules.json. - From now any file minifying only if last modified time was changed. @@ -5155,7 +7984,7 @@ in any position in anyLoadOnLoad function. - Added chainable to Cache object in DOM. - Changed default ip to null so IP would be geted from -config.json only if it setted up. +config.json only if it set up. - Fixed bug with starting node from other then projects dir. @@ -5464,7 +8293,7 @@ time was possible. - Fixed bug with undefined size on root directory of Cloud Commander. Now Cloud Commander writes size 0, if can't get size, and besides will -setted b char: "0b". +set b char: "0b". - Added supporting of Russian language in directory names. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 56045ece..00000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM node:10 -MAINTAINER Coderaiser - -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app - -COPY package.json /usr/src/app/ - -RUN npm config set package-lock false && \ - npm install --production && \ - npm i gritty && \ - npm cache clean --force - -COPY . /usr/src/app - -WORKDIR /root - -ENV cloudcmd_terminal true -ENV cloudcmd_terminal_path gritty - -EXPOSE 8000 - -ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] - diff --git a/Dockerfile.alpine b/Dockerfile.alpine deleted file mode 100644 index 935a8fe8..00000000 --- a/Dockerfile.alpine +++ /dev/null @@ -1,28 +0,0 @@ -FROM mhart/alpine-node:10 -MAINTAINER Coderaiser - -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app - -COPY package.json /usr/src/app/ - -RUN npm config set package-lock false && \ - npm install --production && \ - apk update && \ - apk add --no-cache bash make g++ python && \ - npm i gritty && \ - npm cache clean --force && \ - apk del make g++ python && \ - rm -rf /usr/include /tmp/* /var/cache/apk/* - -COPY . /usr/src/app - -WORKDIR /root - -ENV cloudcmd_terminal true -ENV cloudcmd_terminal_path gritty - -EXPOSE 8000 - -ENTRYPOINT ["/usr/src/app/bin/cloudcmd.js"] - diff --git a/HELP.md b/HELP.md index d652ab6d..2967f0d0 100644 --- a/HELP.md +++ b/HELP.md @@ -1,244 +1,282 @@ -# Cloud Commander v10.2.3 +# Cloud Commander v19.19.1 -### [Main][MainURL] [Blog][BlogURL] Live(![Heroku][Heroku_LIVE_IMG] [Heroku][HerokuURL], ![Now][NOW_LIVE_IMG] [Now][NowURL]) +### [Main][MainURL] [Blog][BlogURL] [Demo][DemoURL] [Deploy](#deploy) -[MainURL]: http://cloudcmd.io "Main" -[BlogURL]: http://blog.cloudcmd.io "Blog" -[HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku" -[NowURL]: https://cloudcmd.now.sh/ "Now" -[HEROKU_LIVE_IMG]: https://status-io.cloudcmd.io/host/cloudcmd.herokuapp.com/img/txt.png "Heroku" -[NOW_LIVE_IMG]: https://status-io.cloudcmd.io/host/cloudcmd.now.sh/img/txt.png "Now" +[MainURL]: https://cloudcmd.io "Main" +[BlogURL]: https://blog.cloudcmd.io "Blog" +[SupportURL]: https://patreon.com/coderaiser "Patreon" +[DemoURL]: https://cloudcmd-zdp6.onrender.com/ +[DWORD]: https://github.com/cloudcmd/dword "Editor based on CodeMirror" +[EDWARD]: https://github.com/cloudcmd/edward "Editor based on Ace" +[DEEPWORD]: https://github.com/cloudcmd/deepword "Editor based on Monaco" +[QWORD]: https://github.com/cloudcmd/qword "Editor based on CodeMirror 6" +[EDWARD_KEYS]: https://github.com/cloudcmd/edward/#hot-keys "Edward Hot keys" +[TERMUX]: https://termux.com "Termux" +[INLY]: https://github.com/coderaiser/node-inly "Extract archive" -[DWORD]: https://github.com/cloudcmd/dword "Editor based on CodeMirror" -[EDWARD]: https://github.com/cloudcmd/edward "Editor based on Ace" -[DEEPWORD]: https://github.com/cloudcmd/deepword "Editor based on Monaco" -[EDWARD_KEYS]: https://github.com/cloudcmd/edward/#hot-keys "Edward Hot keys" -[TERMUX]: https://termux.com "Termux" -[INLY]: https://github.com/coderaiser/node-inly "Extract archive" -**Cloud Commander** is an orthodox web file manager with console and editor. Will help you manage the server and work with files, directories and programs in browser from any computer, mobile or tablet. +[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue +[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd -![Cloud Commander](/img/logo/cloudcmd.png "Cloud Commander") +**Cloud Commander** is a file manager for the web. It includes a command-line console and a text editor. Cloud Commander helps you manage your server and work with files, directories and programs in a web browser from any computer, mobile or tablet. -Benefits ---------------- +![Cloud Commander](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander") -- Open Source (**MIT License**). +## Benefits + +- Open source (**MIT License**). - Has 2 classic panels. - Optional **authorization**. -- Client works in web browser. +- Client works in a web browser. - Server works on **Windows**, **Linux**, **Mac OS** and **Android** (with help of [Termux][TERMUX]). -- Could be used local or remotely. -- Adapting to screen size. -- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] and [Deepword][DEEPWORD]. -- [Console](https://github.com/cloudcmd/console "Console") with support of default OS command line. +- Can be used local or remotely. +- Adapts to screen size. +- **3 built-in editors** with support of **syntax highlighting**: [Dword][DWORD], [Edward][EDWARD] [Deepword][DEEPWORD] and [Qword][QWORD]. +- [Console](https://github.com/cloudcmd/console "Console") with support of the default OS command line. - Written in **JavaScript/Node.js**. - Built-in archives pack: **zip** and **tar.gz**. - Built-in archives extract: **zip**, **tar**, **gz**, **bz2**, **.tar.gz** and **.tar.bz2** (with help of [inly][INLY]). -Install ---------------- +## Installation -The installation of file manager is very simple. +Installation is very simple: -- install latest version of [node.js](https://nodejs.org/ "node.js"). -- install `cloudcmd` via `npm` with: +- Install the latest version of [node.js](https://nodejs.org/ "node.js"). +- Install `cloudcmd` via `npm` with: ```sh npm i cloudcmd -g ``` -When in trouble use: +When in trouble, use: ```sh npm i cloudcmd -g --force ``` -Start ---------------- -For starting just type in console: +## Usage + +To start the server, just run the global *npm* binary from your terminal: ```sh cloudcmd ``` -Cloud Commander supports command line parameters: +Cloud Commander supports the following command-line parameters: -|Parameter |Operation -|:------------------------------|:------------------------------ -| `-h, --help` | display help and exit -| `-v, --version` | display version and exit -| `-s, --save` | save configuration -| `-o, --online` | load scripts from remote servers -| `-a, --auth` | enable authorization -| `-u, --username` | set username -| `-p, --password` | set password -| `-c, --config` | configuration file path -| `--show-config` | show config values -| `--editor` | set editor: "dword", "edward" or "deepword" -| `--packer` | set packer: "tar" or "zip" -| `--root` | set root directory -| `--prefix` | set url prefix -| `--port` | set port number -| `--progress` | show progress of file operations -| `--confirm-copy` | confirm copy -| `--confirm-move` | confirm move -| `--open` | open web browser when server started -| `--name` | set tab name in web browser -| `--one-file-panel` | show one file panel -| `--keys-panel` | show keys panel -| `--contact` | enable contact -| `--config-dialog` | enable config dialog -| `--console` | enable console -| `--sync-console-path` | sync console path -| `--terminal` | enable terminal -| `--terminal-path` | set terminal path -| `--vim` | enable vim hot keys -| `--columns` | set visible columns -| `--no-server` | do not start server -| `--no-auth` | disable authorization -| `--no-online` | load scripts from local server -| `--no-open` | do not open web browser when server started -| `--no-name` | set empty tab name in web browser -| `--no-one-file-panel` | show two file panels -| `--no-keys-panel` | hide keys panel -| `--no-progress` | do not show progress of file operations -| `--no-confirm-copy` | do not confirm copy -| `--no-confirm-move` | do not confirm move -| `--no-contact` | disable contact -| `--no-config-dialog` | disable config dialog -| `--no-console` | disable console -| `--no-sync-console-path` | do not sync console path -| `--no-terminal` | disable terminal -| `--no-vim` | disable vim hot keys -| `--no-columns` | set visible default columns +| Parameter |Operation +|:-----------------------------|:------------------------------ +| `-h, --help` | display help and exit +| `-v, --version` | display version and exit +| `-s, --save` | save configuration +| `-o, --online` | load scripts from remote servers +| `-a, --auth` | enable authorization +| `-u, --username` | set username +| `-p, --password` | set password +| `-c, --config` | configuration file path +| `--show-config` | show config values +| `--show-dot-files` | show dot files +| `--show-file-name` | show file name in view and edit +| `--editor` | set editor: "dword", "edward" or "deepword" +| `--packer` | set packer: "tar" or "zip" +| `--root` | set root directory +| `--prefix` | set url prefix +| `--prefix-socket` | set prefix for url connection +| `--port` | set port number +| `--confirm-copy` | confirm copy +| `--confirm-move` | confirm move +| `--open` | open web browser when server starts +| `--name` | set tab name in web browser +| `--menu` | set menu: "supermenu" or "aleman" +| `--one-file-panel` | show one file panel +| `--keys-panel` | show keys panel +| `--contact` | enable contact +| `--config-dialog` | enable config dialog +| `--config-auth` | enable auth change in config dialog +| `--config-port` | enable port change in config dialog +| `--console` | enable console +| `--sync-console-path` | sync console path +| `--terminal` | enable terminal +| `--terminal-path` | set terminal path +| `--terminal-command` | set command to run in terminal (shell by default) +| `--terminal-auto-restart` | restart command on exit +| `--vim` | enable vim hot keys +| `--columns` | set visible columns +| `--theme` | set theme 'light' or 'dark'" +| `--export` | enable export of config through a server +| `--export-token` | authorization token used by export server +| `--import` | enable import of config +| `--import-token` | authorization token used to connect to export server +| `--import-url` | url of an import server +| `--import-listen` | enable listen on config updates from import server +| `--dropbox` | enable dropbox integration +| `--dropbox-token` | set dropbox token +| `--log` | enable logging +| `--no-show-config` | do not show config values +| `--no-server` | do not start server +| `--no-auth` | disable authorization +| `--no-online` | load scripts from local server +| `--no-open` | do not open web browser when server started +| `--no-name` | set default tab name in web browser +| `--no-keys-panel` | hide keys panel +| `--no-one-file-panel` | show two file panels +| `--no-confirm-copy` | do not confirm copy +| `--no-confirm-move` | do not confirm move +| `--no-config-dialog` | disable config dialog +| `--no-config-auth` | disable auth change in config dialog +| `--no-config-port` | disable port change in config dialog +| `--no-console` | disable console +| `--no-sync-console-path` | do not sync console path +| `--no-contact` | disable contact +| `--no-terminal` | disable terminal +| `--no-terminal-command` | set default shell to run in terminal +| `--no-terminal-auto-restart` | do not restart command on exit +| `--no-vim` | disable vim hot keys +| `--no-themes` | set default visible themes +| `--no-export` | disable export config through a server +| `--no-import` | disable import of config +| `--no-import-listen` | disable listen on config updates from import server +| `--no-show-file-name` | do not show file name in view and edit +| `--no-dropbox` | disable dropbox integration +| `--no-dropbox-token` | unset dropbox token +| `--no-log` | disable logging -If no parameters given Cloud Commander reads information from `~/.cloudcmd.json` and use -port from it (`8000` default). if port variables `PORT` or `VCAP_APP_PORT` isn't exist. +For options not specified by command-line parameters, Cloud Commander then reads configuration data from `~/.cloudcmd.json`. It uses port `8000` by default. -To begin use, type in address bar of your browser: +To begin using the web client, go to this PREFIX in your browser: ``` http://localhost:8000 ``` -Update ---------------- -If you installed Cloud Commander with `npm`, stop application and -re-install it: +## Updating the app + +If you installed Cloud Commander with `npm`, stop the server. Then, reinstall it with: ```sh npm install cloudcmd -g ``` -Then start it again and reload the page. +Then, start the server again with `cloudcmd` and reload the page. -Hot keys ---------------- +## Hot keys -|Key |Operation -|:----------------------|:-------------------------------------------- -| `F1` | help -| `F2` | rename -| `F3` | view -| `Shift + F3` | view as `markdown` -| `F4` | edit -| `Shift + F4` | edit in `vim` mode -| `F5` | copy -| `Alt` + `F5` | pack -| `F6` | rename/move -| `F7` | new directory -| `Shift + F7` | new file -| `F8`, `Delete` | remove -| `Shift + Delete` | remove without prompt -| `F9` | menu -| `Alt` + `F9` | extract -| `F10` | config -| `*` | select/unselect all -| `+` | expand selection -| `-` | shrink selection -| `Ctrl + x` | cut to buffer -| `Ctrl + с` | copy to buffer -| `Ctrl + v` | paste from buffer -| `Ctrl + z` | clear buffer -| `Ctrl + p` | copy path -| `Ctrl + r` | refresh -| `Ctrl + d` | clear local storage -| `Ctrl + a` | select all files in a panel -| `Ctrl + m` | rename selected files in editor -| `Shift + Ctrl + m` | rename selected files in vim mode of editor -| `Ctrl + u` | swap panels -| `Ctrl + F3` | sort by name -| `Ctrl + F5` | sort by date -| `Ctrl + F6` | sort by size -| `Up`, `Down`, `Enter` | file system navigation -| `Alt + Left/Right` | show content of directory under cursor in target panel -| `Alt + g` | go to directory -| `Ctrl + \` | go to the root directory -| `Tab` | move via panels -| `Page Up` | up on one page -| `Page Down` | down on one page -| `Home` | to begin of list -| `End` | to end of list -| `Space` | select current file (and get size of directory) -| `Insert` | select current file (and move to next) -| `F9` | context menu -| `~` | console -| `Ctrl + Click` | open file on new tab +| Key |Operation +|:---------------------|:-------------------------------------------- +| `F1` | help +| `F2` | show `user menu` +| `F3` | view, change directory +| `Shift + F3` | view raw file, change directory +| `F4` | edit +| `F5` | copy +| `Alt` + `F5` | pack +| `F6` | rename/move +| `Shift` + `F6` | rename current file +| `F7` | new directory +| `Shift + F7` | new file +| `F8`, `Delete` | remove +| `Shift + Delete` | remove without prompt +| `F9` | menu +| `Alt` + `F9` | extract +| `F10` | config +| `*` | select/unselect all +| `+` | expand selection +| `-` | shrink selection +| `:` | open Command Line +| `Ctrl + X` | cut to buffer +| `Ctrl + C` | copy to buffer +| `Ctrl + V` | paste from buffer +| `Ctrl + Z` | clear buffer +| `Ctrl + P` | copy path +| `Ctrl + R` | refresh +| `Ctrl + D` | clear local storage +| `Ctrl + A` | select all files in a panel +| `Ctrl + M` | [rename selected files](https://github.com/coderaiser/cloudcmd/releases/tag/v12.1.0) in editor +| `Ctrl + U` | swap panels +| `Ctrl + F3` | sort by name +| `Ctrl + F5` | sort by date +| `Ctrl + F6` | sort by size +| `Ctrl + Shift + L` | logout +| `Ctrl + Command + .` | show/hide dot files +| `Up` | move cursor up +| `Down` | move cursor down +| `Enter` | change directory/view file +| `Alt + Left/Right` | show content of directory under cursor in target panel +| `Alt + G` | go to directory +| `Ctrl + \` | go to the root directory +| `Tab` | move via panels +| `Page Up` | up on one page +| `Page Down` | down on one page +| `Home` | to begin of list +| `End` | to end of list +| `Space` | select current file (and get size of directory) +| `Insert` | select current file (and move to next) +| `F9` | context menu +| `~` | console +| `Esc` | toggle vim hotkeys (`file manager`, `editor`) ### Vim -When `--vim` option provided, or configuration parameter `vim` set, next hot keys become available: +When the `--vim` option is provided, or the configuration parameter `vim` is set, the following hotkeys become available: -|Key |Operation -|:----------------------|:-------------------------------------------- -| `j` | navigate to next file -| `k` | navigate to previous file -| `dd` | remove current file -| `G` | navigate to bottom file -| `gg` | navigate to top file -| `v` | visual mode -| `y` | copy (selected in visual mode files) -| `p` | paste files -| `Esc` | unselect all -| `/` | find file in current directory -| `n` | navigate to next found file -| `N` | navigate to previous found file +| Key |Operation +|:------------|:-------------------------------------------- +| `j` | navigate to next file +| `k` | navigate to previous file +| `dd` | remove current file +| `G` or `$` | navigate to bottom file +| `gg` or `^` | navigate to top file +| `v` | visual mode +| `y` | copy (selected in visual mode files) +| `p` | paste files +| `Esc` | unselect all +| `/` | find file in current directory +| `n` | navigate to next found file +| `N` | navigate to previous found file +| `md` | make directory +| `mf` | make file +| `tt` | show terminal +| `e` | edit file +| `cc` | copy +| `mm` | move +| `rr` | rename file Commands can be joined, for example: -- `5j` will navigate `5` files below current; -- `d5j` will remove next `5` files; + +- `5j` will navigate **5** files below current; +- `d5j` will remove next **5** files; - `dG` will remove all files from current to bottom; -Drag and Drop ---------------- -Next file operations are accessible through `Drag and Drop`. +## Drag and drop + +These file operations are accessible with "drag and drop". | Drag Mouse Button | Key | Origin | Destination |Operation |:------------------|:----------|:----------|:--------------|:------------------ -| `Left` | | `Panel` | `Panel` | copy files -| `Left` | `Shift` | `Panel` | `Panel` | rename/move files -| `Left` | | `Panel` | `Desktop` | download files -| `Left` | | `Desktop` | `Panel` | upload files +| Left | | Panel | Panel | copy files +| Left | `Shift` | Panel | Panel | rename/move files +| Left | | Panel | Desktop | download files +| Left | | Desktop | Panel | upload files + +## View -View ---------------- ![View](/img/screen/view.png "View") ### Features + - View images. - View text files. -- Playing audio. -- Playing video. +- Play audio. +- Play video. -### Hot keys +### Hotkeys |Key |Operation |:----------------------|:-------------------------------------------- | `F3` | open | `Esc` | close -Edit ---------------- +## Edit + ![Edit](/img/screen/edit.png "Edit") ### Hot keys @@ -246,13 +284,13 @@ Edit |Key |Operation |:----------------------|:-------------------------------------------- | `F4` | open -| `Shift + F4` | open in `vim` mode +| `Shift + F4` | open in "vim" mode | `Esc` | close -For more details see [Edward hot keys][EDWARD_KEYS]. +For more details see [Edward hotkeys][EDWARD_KEYS]. + +## Console -Console ---------------- ![Console](/img/screen/console.png "Console") ### Hot keys @@ -260,24 +298,24 @@ Console |Key |Operation |:----------------------|:-------------------------------------------- | `~` | open -| `Ctrl + p` | paste path of current directory +| `Ctrl + P` | paste path of current directory | `Esc` | close For more details see [console hot keys](https://github.com/cloudcmd/console#hot-keys "Console Hot Keys"). -Terminal ---------------- +## Terminal + ![Terminal](/img/screen/terminal.png "Terminal") ### Install -`Terminal` disabled and not installed by default. To use it you should install [gritty](https://github.com/cloudcmd/gritty "Gritty") with: +The terminal is disabled and not installed by default. To use it, you should install [gritty](https://github.com/cloudcmd/gritty "Gritty") with: ```sh npm i gritty -g ``` -And then set the path of a terminal with: +and then set the path for the terminal with: ```sh cloudcmd --terminal --terminal-path `gritty --path` --save @@ -285,17 +323,18 @@ cloudcmd --terminal --terminal-path `gritty --path` --save ### Windows -If you can't install `gritty` on Windows try to install `windows-build-tools` first: +If you can't install `gritty` on Windows, try to install `windows-build-tools` first: ```sh npm install windows-build-tools -g ``` -Then get path of a `gritty` with: +Then get the path of `gritty` with: ```sh gritty --path ``` + It will returns something like: ```sh @@ -308,34 +347,33 @@ Set this path as `--terminal-path` with: cloudcmd --save --terminal --terminal-path "C:\Users\coderaiser\AppData\Roaming\npm\node_modules\gritty" ``` -After that you can use `terminal` in the same way as a `console`. +After that, you can use Cloud Commander's terminal in the same way as a normal shell console. -### Hot keys +### Hotkeys |Key |Operation |:----------------------|:-------------------------------------------- | `Shift` + `~` | open | `Shift` + `Esc` | close -Environment Variables ---------------- +## Environment Variables -Every program executed in `console` or `terminal` has these `environment` variables: +Every program executed in Cloud Commander's terminal has these environment variables: - `ACTIVE_DIR` - directory that contains cursor - `PASSIVE_DIR` - directory with no cursor - `CURRENT_NAME` - name of a file under cursor - `CURRENT_PATH` - path to file under cursor -On `Unix` you can use it this way: +On Unix, you can use it this way: ```sh ~> echo $CURRENT_PATH /home/coderaiser/cloudcmd/bin/cloudcmd.js ``` -Config ---------------- +## Configuration + ![Config](/img/screen/config.png "Config") ### Hot keys @@ -345,74 +383,261 @@ Config | `F10` | open | `Esc` | close -When you change one of options file `~/.cloudcmd.json` would be saved. -It could be edited manually with any text editor. -Here is description of options: +When you change any options, the `~/.cloudcmd.json` file is automatically updated. +It can also be edited manually with any text editor. +Here's a description of all options: -```js +```json { - "name" : "", /* set tab name in web browser */ - "auth" : false, /* enable http authentication */ - "username" : "root", /* username for authentication */ -    "password"         : "toor",   /* password hash for authentication */ - "algo" : "sha512WithRSAEncryption", /* cryptographic algorithm */ - "editor" : "edward", /* default, could be "dword" or "edward" */ - "packer" : "tar", /* default, could be "tar" or "zip" */ - "diff" : true, /* when save - send patch, not whole file */ - "zip" : true, /* zip text before send / unzip before save */ - "buffer" : true, /* buffer for copying files */ - "dirStorage" : true, /* store directory listing */ - "online" : true, /* load js files from cdn or local path */ - "open" : false /* open web browser when server started */ - "oneFilePanel" : false, /* show one file panel */ - "keysPanel" : true, /* show classic panel with buttons of keys */ - "port" : 8000, /* http port */ - "ip" : null, /* ip or null(default) */ - "root" : "/", /* root directory */ - "prefix" : "", /* url prefix */ - "progress" : true, /* show progress of file operations */ - "confirmCopy" : true, /* confirm copy */ - "confirmMove" : true, /* confirm move */ - "showConfig" : false, /* show config at startap */ - "contact" : true, /* enable contact */ - "configDialog" : true, /* enable config dialog */ - "console" : true, /* enable console */ - "syncConsolePath" : false /* do not sync console path */ - "terminal" : false, /* disable terminal */ - "terminalPath" : '', /* path of a terminal */ - "vim" : false, /* disable vim hot keys */ - "columns" : "name-size-date-owner-mode", /* set visible columns */ + "name": "", // set tab name in web browser + "auth": false, // enable http authentication + "username": "root", // username for authentication + "password": "toor", // password hash for authentication + "algo": "sha512WithRSAEncryption", // cryptographic algorithm + "editor": "edward", // default, could be "dword" or "edward" + "packer": "tar", // default, could be "tar" or "zip" + "diff": true, // when save - send patch, not whole file + "zip": true, // zip text before send / unzip before save + "buffer": true, // buffer for copying files + "dirStorage": true, // store directory listing + "online": false, // do not load js files from cdn + "open": true, // open web browser when server started + "oneFilePanel": false, // show one file panel + "keysPanel": true, // show classic panel with buttons of keys + "port": 8000, // http port + "ip": null, // ip or null(default) + "root": "/", // root directory + "prefix": "", // url prefix + "prefixSocket": "", // prefix for socket connection + "confirmCopy": true, // confirm copy + "confirmMove": true, // confirm move + "showConfig": false, // show config at startup + "showDotFiles": true, // show dot files + "showFileName": false, // do not show file name in view and edit + "contact": true, // enable contact + "configDialog": true, // enable config dialog + "configAuth": true, // enable auth change in config dialog + "console": true, // enable console + "syncConsolePath": false, // do not sync console path + "terminal": false, // disable terminal + "terminalPath": "", // path of a terminal + "terminalCommand": "", // set command to run in terminal + "terminalAutoRestart": true, // restart command on exit + "vim": false, // disable vim hot keys + "themes": "name-size-date-owner-mode", // set visible themes + "export": false, // enable export of config through a server + "exportToken": "root", // token used by export server + "import": false, // enable import of config + "import-url": "http://localhost:8000", // url of an export server + "importToken": "root", // token used to connect to export server + "importListen": false, // listen on config updates + "dropbox": false, // disable dropbox integration + "dropboxToken": "", // unset dropbox token + "log": true // logging } ``` ### Environment Variables -Some config options can be overridden with `environment variables` such: +Some config options can be overridden with environment variables, such as: - `CLOUDCMD_NAME` - set tab name in web browser +- `CLOUDCMD_OPEN` - open web browser when server started - `CLOUDCMD_EDITOR` - set editor -- `CLOUDCMD_COLUMNS` - set visible columns +- `CLOUDCMD_COLUMNS` - set visible themes +- `CLOUDCMD_THEME` - set themes "light" or "dark" +- `CLOUDCMD_MENU` - set menu "supermenu" or "aleman" - `CLOUDCMD_CONTACT` - enable contact - `CLOUDCMD_CONFIG_DIALOG` - enable config dialog +- `CLOUDCMD_CONFIG_AUTH` - enable auth change in config dialog +- `CLOUDCMD_CONFIG_PORT` - enable port change in config dialog - `CLOUDCMD_CONSOLE` - enable console - `CLOUDCMD_SYNC_CONSOLE_PATH` - sync console path - `CLOUDCMD_TERMINAL` - enable terminal - `CLOUDCMD_TERMINAL_PATH` - set terminal path +- `CLOUDCMD_TERMINAL_COMMAND` - set command to run in terminal (shell by default) +- `CLOUDCMD_TERMINAL_AUTO_RESTART` - restart command on exit - `CLOUDCMD_KEYS_PANEL` - show keys panel - `CLOUDCMD_ONE_FILE_PANEL` - show one file panel - `CLOUDCMD_AUTH` - enable authentication - `CLOUDCMD_USERNAME` - set username - `CLOUDCMD_PASSWORD` - set password +- `CLOUDCMD_PREFIX` - set url prefix - `CLOUDCMD_ROOT` - set root directory - `CLOUDCMD_VIM` - enable vim hot keys - `CLOUDCMD_CONFIRM_COPY` - confirm copy - `CLOUDCMD_CONFIRM_MOVE` - confirm move +- `CLOUDCMD_SHOW_FILE_NAME` - show file name in view and edit +- `CLOUDCMD_EXPORT` - enable export of config through a server +- `CLOUDCMD_EXPORT_TOKEN` - authorization token used by export server +- `CLOUDCMD_IMPORT` - enable import of config +- `CLOUDCMD_IMPORT_TOKEN` - authorization token used to connect to export server +- `CLOUDCMD_IMPORT_URL` - url of an import server +- `CLOUDCMD_IMPORT_LISTEN` - enable listen on config updates from import server + +### User Menu + +When you press `F2` Cloud Commander will read a file `.cloudcmd.menu.js` by walking up parent directories, if can't read it will try to read `~/.cloudcmd.menu.js`. + +Let's consider example `user menu` works file: + +```js +const RENAME_FILE = 'Rename file'; + +export default { + '__settings': { + select: [RENAME_FILE], + run: false, + }, + [`F2 - ${RENAME_FILE}`]: async ({DOM}) => { + await DOM.renameCurrent(); + }, + 'D - Build Dev': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client:dev', + autoClose: false, // optional + closeMessage: 'Press any button to close Terminal', // optional + }); + + await CloudCmd.refresh(); + }, + 'P - Build Prod': async ({CloudCmd}) => { + await CloudCmd.TerminalRun.show({ + command: 'npm run build:client', + autoClose: true, // optional + }); + + await CloudCmd.refresh(); + }, + 'C - Create User Menu File': async ({DOM, CloudCmd}) => { + const {CurrentInfo} = DOM; + + const {dirPath} = CurrentInfo; + const path = `${dirPath}.cloudcmd.menu.js`; + const {prefix} = CloudCmd; + + const data = await readDefaultMenu({ + prefix, + }); + + await createDefaultMenu({ + path, + data, + DOM, + CloudCmd, + }); + }, +}; + +async function createDefaultMenu({path, data, DOM, CloudCmd}) { + const {IO} = DOM; + + await IO.write(path, data); + await CloudCmd.refresh(); + + DOM.setCurrentByName('.cloudcmd.menu.js'); + + await CloudCmd.EditFile.show(); +} + +async function readDefaultMenu({prefix}) { + const res = await fetch(`${prefix}/api/v1/user-menu/default`); + const data = await res.text(); + + return data; +} +``` + +You will have ability to run one of this 3 commands with help of double click, enter, or binded key (`F2`, `D` or `P` in this example). Also you can run commands in terminal, or execute any built-in function of `Cloud Commander` extended it's interface. You can find more examples in [User Menu Cookbook](https://github.com/coderaiser/cloudcmd/wiki/User-Menu-Cookbook). + +#### User Menu API + +Here you can find `API` that can be used in **User Menu**. **DOM** and **CloudCmd** two main objects you receive in arguments list using destructuring. + +**DOM** contains all base functions of `Cloud Commander` (rename, remove, download etc); + +- `renameCurrent` - shows renames current file dialog, and does renaming. + +**CloudCmd** contains all modules (`Terminal`, `View`, `Edit`, `Config`, `Console` etc); + +- `TerminalRun` - module that shows `Terminal` with a `command` from options and closes terminal when everything is done. + +**IO** Files API + +- `rename(from, to)` - rename `from` into `to` +- `move(from, to, names)` - rename files with a `names` `from` into `to`; +- `copy(from, to, names)` - copy files with a `names` `from` into `to`; +- `createDirectory(path)` - create directory with a `path`; + +### Distribute + +Being able to configure Cloud Commander remotely opens the doors to using it as microservice, and that's what the "distribute" options set out to do. + +There is an **export server** and an **import client**, which are enabled with `--export` and `--import` respectively. There is a "token", which should be the same in `--import-token` and `--export-token`. To use the *import client*, you should provide `--import-url` to the client so it can connect to an *export server*. + +There are two ways that the *import client* can receive configuration from an *export server*: + +- full config at startup (default) +- get every updated option (with help of the `--import-listen` flag) + +#### An example - using the "distribute" options to get configuration from a remote instance: + +Here's an *export server*: + +``` +coderaiser@cloudcmd:~$ cloudcmd --port 1234 --export --export-token=cloudcmd +``` + +...and an *import client*: + +``` +coderaiser@cloudcmd:~$ cloudcmd --name importer --port 4321 --import-url http://127.0.0.1:1234 --import-token=cloudcmd --no-server --save +``` + +Here's the log output from the *export server*: + +``` +url: http://localhost:1234/ +2018.08.23 13:41:45 -> export: try to auth from importer [127.0.0.1:4321] +2018.08.23 13:41:45 -> export: connected to importer [127.0.0.1:4321] +2018.08.23 13:41:45 -> export: config send to importer [127.0.0.1:4321] +2018.08.23 13:41:45 -> export: disconnected importer [127.0.0.1:4321] +``` + +...and the log output from the *import client*: + +``` +2018.08.23 13:47:36 -> import: try to auth to http://127.0.0.1:1234 +2018.08.23 13:47:36 -> import: connected to http://127.0.0.1:1234 +2018.08.23 13:47:36 -> import: config received from http://127.0.0.1:1234 +2018.08.23 13:47:36 -> import: disconnected from http://127.0.0.1:1234 +``` + +When the *import client* uses `--import-listen`, a persistent connection is used, and the *client* receives live updates from the *import server*. + +The *export server* omits the following configuration fields: + +- `auth` +- `username` +- `password` +- `algo` +- `name` +- `ip` +- `port` +- `root` +- `import` +- `importUrl` +- `importToken` +- `export` +- `exportToken` +- `log` +- `configDialog` + +## Menu -Menu ---------------- ![Menu](/img/screen/menu.png "Menu") -Right mouse click button shows context menu with items: +Right-mouse click to show a context menu with these items: - View - Edit @@ -433,22 +658,30 @@ Right mouse click button shows context menu with items: ### Hot keys -|Key |Operation -|:----------------------|:-------------------------------------------- -| `F9` | open -| `Esc` | close +| Key | Operation | +|:-------------|:------------------------| +| `F9` | open | +| `Esc` | close | +| `Up`, `j` | move cursor up | +| `Down`, `k` | move cursor down | +| `Left`, `h` | close submenu | +| `Right`, `l` | open submenu | +| `G` or `$` | navigate to bottom | +| `gg` or `^` | navigate to top | -One file panel ---------------- -Cloud Commander can work in one panel mode when screen size can not accommodate second panel or via `--one-file-panel` options flag. -It could happen when mobile device, tablet or small window size used to work with file manager. +Commands can be joined, for example: + +- `5j` will navigate **5** items below current; + +## One file panel + +Cloud Commander can work in one panel mode when your screen size can't accommodate a second panel (such as on mobile or tablet), or via the `--one-file-panel` options flag. ![One file panel](/img/screen/one-file-panel.png "One file panel") -Using as Middleware ---------------- +## Using as middleware -Cloud Commander could be used as middleware for `node.js` applications based on [socket.io](http://socket.io "Socket.IO") and [express](http://expressjs.com "Express"): +Cloud Commander can be used as middleware for `node.js` applications based on [socket.io](http://socket.io "Socket.IO") and [express](http://expressjs.com "Express"): Init `package.json`: @@ -465,33 +698,30 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -const http = require('http'); -const cloudcmd = require('cloudcmd'); -const io = require('socket.io'); -const app = require('express')(); +import http from 'node:http'; +import {cloudcmd} from 'cloudcmd'; +import {Server} from 'socket.io'; +import express from 'express'; +const app = express(); const port = 1337; -const prefix = '/cloudcmd'; +const prefix = '/'; const server = http.createServer(app); -const socket = io.listen(server, { - path: `${prefix}/socket.io` +const socket = new Server(server, { + path: `${prefix}socket.io`, }); const config = { - prefix // base URL or function which returns base URL (optional) + name: 'cloudcmd :)', }; -const plugins = [ - __dirname + '/plugin.js' -]; - const filePicker = { data: { FilePicker: { - key: 'key' - } - } + key: 'key', + }, + }, }; // override option from json/modules.json @@ -499,32 +729,81 @@ const modules = { filePicker, }; -app.use(cloudcmd({ - socket, // used by Config, Edit (optional) and Console (required) - config, // config data (optional) - plugins, // optional +const { + createConfigManager, + configPath, +} = cloudcmd; + +const configManager = createConfigManager({ + configPath, +}); + +app.use(prefix, cloudcmd({ + socket, // used by Config, Edit (optional) and Console (required) + config, // config data (optional) modules, // optional + configManager, // optional })); server.listen(port); ``` -And you are ready to go. - -### Authorization - -If you want to enable `authorization` you can pass credentials in a `config`. -To generate password you can install `criton` with `npm i criton --save` and use it -or any other way to generate a `hash` of a `password`. +Here is example with two `Config Managers`: ```js -const criton = require('criton'); -const algo = 'sha512WithRSAEncryption'; // default +import http from 'node:http'; +import cloudcmd from 'cloudcmd'; +import {Server} from 'socket.io'; +import express from 'express'; -// you can generate hash dynamically +const app = express(); +const port = 8000; +const prefix1 = '/1'; +const prefix2 = '/2'; + +const {createConfigManager} = cloudcmd; + +const server = http.createServer(app); +const socket1 = new Server(server, { + path: `${prefix1}/socket.io`, +}); + +const socket2 = new Server(server, { + path: `${prefix2}/socket.io`, +}); + +const configManager1 = createConfigManager(); + +configManager1('name', '1'); + +const configManager2 = createConfigManager(); +configManager2('name', '2'); + +app.use(prefix1, cloudcmd({ + socket: socket1, + configManager: configManager1, +})); + +app.use(prefix2, cloudcmd({ + socket: socket2, + configManager: configManager2, +})); + +server.listen(port); +``` + +If you want to enable authorization, you can pass credentials to Cloud Commander with a config. To generate a password, you can install `criton` with `npm i criton --save`, and use it (or any other way) to generate a hash of a password. + +```js +import criton from 'criton'; + +const algo = 'sha512WithRSAEncryption'; + +// default +// you can generate a hash dynamically const password = criton('root', algo); -// or use pregenerated hash as well +// or use a pregenerated hash as well '2b64f2e..ca5d9a9'; const auth = true; @@ -534,16 +813,27 @@ const config = { algo, // optional auth, username, - pasword, -} + password, +}; ``` -Server ---------------- -Standard practices say no non-root process gets to talk to -the Internet on a port less than 1024. Anyway I suggest you -to start Cloud Commander as non-root. How it could be solved? -There is a couple easy and fast ways. One of them is port forwarding. +Now you're ready to go! + +## Authorization + +`~/.cloudcmd.json` contains [password hash](https://github.com/coderaiser/cloudcmd/blob/v11.8.3/json/config.json#L5) because of security reason, if someone steal your config, he wouldn't know your password, because hash is [very strong](https://github.com/coderaiser/cloudcmd/blob/v11.8.3/json/config.json#L6) and can be customized. + +You should never write your password as plain text to `~/.cloudcmd.json`, you can generate password using `cloudcmd` itself: + +``` +cloudcmd --username name --password password --auth --save --no-server +``` + +This command will create hash of your password and write it to `~/.cloudcmd.json`. + +## Server + +Standard practices dictate that no non-root process get to talk to the internet on a port less than 1024. Despite this, **I suggest you start Cloud Commander as a non-root process**. How can we get around this limitation? There's a couple of fast & easy ways. One of them is port forwarding: ### Iptables @@ -556,15 +846,15 @@ iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 4430 iptables -t nat -L # look rules after ``` -You should see something like this ( **8000** and **4430** should be in config as **port** and **sslPort** ) +You should see something like this (**8000** and **4430** should be in config as **port** and **sslPort**) ```sh target prot opt source destination REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports 8000 REDIRECT tcp -- anywhere anywhere tcp dpt:https redir ports 4430 ``` -If you would want to get things back just clear rules ( **1** and **2** it's rule numbers, -in your list they could differ). + +If you would want to get things back just clear rules (rule numbers **1** and **2**; in your list they could differ). ```sh iptables -t nat -D PREROUTING 2 @@ -572,14 +862,15 @@ iptables -t nat -D PREROUTING 1 ``` ### nginx -Get [nginx](http://nginx.org/ "nginx"). On Linux it could be done this way: + +Get [nginx](http://nginx.org/ "nginx"). On Linux, you can run: ```sh sudo apt-get install nginx #for ubuntu and debian ``` -Then make host file **/etc/nginx/sites-available/io.cloudcmd.io** -( *io.cloudcmd.io* is your domain name) with content: +Then, make a host file **/etc/nginx/sites-available/io.cloudcmd.io** +(where, for example, *io.cloudcmd.io* is your domain name) with these contents: ```sh server { @@ -593,7 +884,7 @@ server { } ``` -If you want add **SSL**, add a couple lines to server block: +**If you want add SSL**, add a couple lines to the `server` block: ```sh server { @@ -610,7 +901,7 @@ server { } ``` -For WebSocket support (nginx >= v1.3.13) modify server block: +For WebSocket support, (nginx >= v1.3.13) modify the `server` block like so: ```sh location / { @@ -622,8 +913,7 @@ For WebSocket support (nginx >= v1.3.13) modify server block: } ``` - -If you need redirection from **http** to **https**, it's simple: +If you need redirection from **http** to **https**, simply use: ```sh server { @@ -635,30 +925,29 @@ server { ``` ```sh -# create symlink of this file +# create a symlink of this file ln -s ./sites-available/io.cloudcmd.io ./sites-enabled # restart nginx /etc/init.d/nginx restart ``` -Deploy ---------------- -`Cloud Commander` could be easily deployed to [Heroku](https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy to Heroku"). +## Deploy -[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png "Deploy to Heroku")]( https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd) +`Cloud Commander` can be easily deployed to [InstaPods][DeployInstaPodsURL]. After deploy you receive email with credentials. -Docker ---------------- -`Cloud Commander` could be used as [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: +[![Deploy on InstaPods](https://instapods.com/deploy-button.svg)](https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd) + +## Docker + +`Cloud Commander` can be used as [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") like so: ```sh -docker run -t --rm -v ~:/root -v /:/mnt/fs -p 8000:8000 coderaiser/cloudcmd +docker run -t --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd ``` -Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`, -`8000` port would be exposed to hosts port. +Configuration will be read from the home directory, the host's root file system will be mounted to `/mnt/fs`, and port `8000` port will exposed to the host's port. -Also you could use [docker compose](https://docs.docker.com/compose/ "Docker Compose") with `docker-compose.yml`: +Also, you can use [docker compose](https://docs.docker.com/compose/ "Docker Compose") with `docker-compose.yml`: ```yml version: '2' @@ -672,23 +961,524 @@ services: image: coderaiser/cloudcmd ``` -When you create this file run: +When you create this file, run: ```sh docker-compose up ``` -Get involved ---------------- +# Dropbox -There is a lot ways to be involved in `Cloud Commander` development: +Dropbox support is integrated into Cloud Commander, and you can switch from your local file system to a Dropbox account. +All you need to do is set `--dropbox` option and [generate a dropbox token](https://blogs.dropbox.com/developers/2014/05/generate-an-access-token-for-your-own-account/) for your account. -- if you find a bug or got idea to share [create issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); -- if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); -- if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); +This could look like this: -Version history ---------------- +```sh +cloudcmd --dropbox --dropbox-token your-dropbox-token +``` + +While using Dropbox remember that there is no remote support for the console/terminal. Seeing the progress of file operations is also not supported. There's only basic support, but you can do the following things with files and directories: + +- create +- remove +- rename/move +- view +- edit + +## Automatically start cloudcmd on boot for `systemd` + +First, locate the command to run cloudcmd + +```sh +which cloudcmd +``` + +take note of the result and create a systemd entry by executing + +```sh +sudo nano /etc/systemd/system/cloudcmd.service +``` + +and use this template + +``` +[Unit] +Description = Cloud Commander + +[Service] +TimeoutStartSec = 0 +Restart = always +ExecStart = THE RESULT OF which cloudcmd WE'VE EXECUTED EARLIER +User = YOUR_USER + +[Install] +WantedBy = multi-user.target +``` + +Don't forget to change the line for `ExecStart` and `User` + +Save the changes and exit editor. + +You may now enable cloudcmd and set it to autostart on boot by running: + +```sh +sudo systemctl enable --now cloudcmd +``` + +## Automatically start cloudcmd on boot for `FreeBSD` + +First, locate the command to run cloudcmd + +``` +which cloudcmd +``` + +take note of the result and create a rc script + +``` +vi /usr/local/etc/rc.d/cloudcmd +``` + +and use this template + +``` +!/bin/sh +# +# PROVIDE: cloudcmd +# REQUIRE: LOGIN +# KEYWORD: shutdown + +# Author: IhatemyISP (ihatemyisp.net) +# Version: 1.0.0 + +# Description: +# This script runs Cloud Commander as a service under the supplied user on boot + +# 1) Place file in /usr/local/etc/rc.d/ +# 2) Add cloudcmd_enable="YES" to /etc/rc.conf +# 3) (Optional) To run as non-root, add cloudcmd_runAs="user" to /etc/rc.conf +# 4) (Optional) To pass Cloud Commander args, add cloudcmd_args="" to /etc/rc.conf + +# Freebsd rc library +. /etc/rc.subr + +# General Info +name="cloudcmd" # Safe name of program +program_name="cloudcmd" # Name of exec +title="CloudCommander" # Title to display in top/htop + +# RC.config vars +load_rc_config $name # Loading rc config vars +: ${cloudcmd_enable="NO"} # Default: Do not enable Cloud Commander +: ${cloudcmd_runAs="root"} # Default: Run Cloud Commander as root + +# Freebsd Setup +rcvar=cloudcmd_enable # Enables the rc.conf YES/NO flag +pidfile="/var/run/${program_name}.pid" # PID file location + +# Env Setup +export HOME=$( getent passwd "$cloudcmd_runAs" | cut -d: -f6 ) # Gets the home directory of the runAs user + +# Command Setup +exec_path="/usr/local/bin/${program_name}" # Path to the cloudcmd exec, /usr/local/bin/ when installed globally +output_file="/var/log/${program_name}.log" # Path to Cloud Commander output file + +# Command +command="/usr/sbin/daemon" +command_args="-r -t ${title} -u ${cloudcmd_runAs} -o ${output_file} -P ${pidfile} ${exec_path} ${cloudcmd_args}" + +# Loading Config +load_rc_config ${name} +run_rc_command "$1" +``` + +Enable autostart + +``` +echo cloudcmd_enable="YES" >> /etc/rc.conf +``` + +(Optional) Set user to run Cloud Commander as (default is root) + +``` +echo cloudcmd_runAs="user" >> /etc/rc.conf +``` + +Start the service (or just reboot) + +``` +service cloudcmd start +``` + +## Get involved + +There are a lot of ways to be involved in `Cloud Commander` development: + +- support project on patreon: https://patreon.com/coderaiser; +- if you find a bug or have an idea to share, [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); +- if you fixed a bug, typo or implemented a new feature, [create a pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); +- if you know a language not currently translated, or would like to improve an existing translation, you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); + +## Version history + +- *2026.06.15*, **[v19.19.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.1)** +- *2026.05.26*, **[v19.19.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.19.0)** +- *2026.05.26*, **[v19.18.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.1)** +- *2026.05.26*, **[v19.18.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.18.0)** +- *2026.05.17*, **[v19.17.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.17.0)** +- *2026.05.03*, **[v19.16.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.16.0)** +- *2026.04.28*, **[v19.15.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.15.0)** +- *2026.04.28*, **[v19.14.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.14.0)** +- *2026.04.21*, **[v19.13.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.1)** +- *2026.04.15*, **[v19.13.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.13.0)** +- *2026.04.12*, **[v19.12.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.5)** +- *2026.04.12*, **[v19.12.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.4)** +- *2026.04.12*, **[v19.12.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.3)** +- *2026.04.11*, **[v19.12.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.2)** +- *2026.04.09*, **[v19.12.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.1)** +- *2026.04.09*, **[v19.12.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.12.0)** +- *2026.04.07*, **[v19.11.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.14)** +- *2026.04.07*, **[v19.11.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.13)** +- *2026.04.06*, **[v19.11.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.12)** +- *2026.04.06*, **[v19.11.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.11)** +- *2026.04.06*, **[v19.11.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.10)** +- *2026.04.06*, **[v19.11.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.9)** +- *2026.04.06*, **[v19.11.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.8)** +- *2026.04.05*, **[v19.11.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.7)** +- *2026.04.05*, **[v19.11.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.6)** +- *2026.04.05*, **[v19.11.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.5)** +- *2026.04.04*, **[v19.11.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.4)** +- *2026.04.04*, **[v19.11.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.3)** +- *2026.04.04*, **[v19.11.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.2)** +- *2026.04.04*, **[v19.11.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.1)** +- *2026.04.04*, **[v19.11.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.11.0)** +- *2026.04.04*, **[v19.10.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.2)** +- *2026.04.03*, **[v19.10.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.1)** +- *2026.04.02*, **[v19.10.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.10.0)** +- *2026.04.02*, **[v19.9.24](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.24)** +- *2026.04.01*, **[v19.9.23](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.23)** +- *2026.03.31*, **[v19.9.22](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.22)** +- *2026.03.31*, **[v19.9.21](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.21)** +- *2026.03.30*, **[v19.9.20](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.20)** +- *2026.03.30*, **[v19.9.19](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.19)** +- *2026.03.30*, **[v19.9.18](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.18)** +- *2026.03.30*, **[v19.9.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.17)** +- *2026.03.29*, **[v19.9.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.16)** +- *2026.03.29*, **[v19.9.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.15)** +- *2026.03.29*, **[v19.9.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.14)** +- *2026.03.29*, **[v19.9.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.13)** +- *2026.03.29*, **[v19.9.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.12)** +- *2026.03.29*, **[v19.9.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.11)** +- *2026.03.29*, **[v19.9.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.10)** +- *2026.03.29*, **[v19.9.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.9)** +- *2026.03.28*, **[v19.9.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.8)** +- *2026.03.27*, **[v19.9.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.7)** +- *2026.03.26*, **[v19.9.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.6)** +- *2026.03.26*, **[v19.9.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.5)** +- *2026.03.26*, **[v19.9.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.4)** +- *2026.03.24*, **[v19.9.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.3)** +- *2026.03.23*, **[v19.9.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.2)** +- *2026.03.23*, **[v19.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.1)** +- *2026.03.23*, **[v19.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.9.0)** +- *2026.03.23*, **[v19.8.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.15)** +- *2026.03.23*, **[v19.8.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.14)** +- *2026.03.23*, **[v19.8.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.13)** +- *2026.03.23*, **[v19.8.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.12)** +- *2026.03.23*, **[v19.8.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.11)** +- *2026.03.23*, **[v19.8.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.10)** +- *2026.03.23*, **[v19.8.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.9)** +- *2026.03.23*, **[v19.8.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.8)** +- *2026.03.23*, **[v19.8.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.7)** +- *2026.03.23*, **[v19.8.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.6)** +- *2026.03.23*, **[v19.8.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.5)** +- *2026.03.22*, **[v19.8.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.4)** +- *2026.03.22*, **[v19.8.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.3)** +- *2026.03.22*, **[v19.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.2)** +- *2026.03.22*, **[v19.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.1)** +- *2026.03.20*, **[v19.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.8.0)** +- *2026.03.18*, **[v19.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.7.1)** +- *2026.03.17*, **[v19.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.7.0)** +- *2026.03.17*, **[v19.6.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.9)** +- *2026.02.27*, **[v19.6.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.8)** +- *2026.02.26*, **[v19.6.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.7)** +- *2026.02.26*, **[v19.6.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.6)** +- *2026.02.26*, **[v19.6.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.5)** +- *2026.02.25*, **[v19.6.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.4)** +- *2026.02.24*, **[v19.6.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.3)** +- *2026.02.24*, **[v19.6.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.2)** +- *2026.02.24*, **[v19.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.1)** +- *2026.02.21*, **[v19.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.6.0)** +- *2026.02.18*, **[v19.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.5.1)** +- *2026.02.18*, **[v19.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.5.0)** +- *2026.02.18*, **[v19.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.4.1)** +- *2026.02.18*, **[v19.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.4.0)** +- *2026.02.15*, **[v19.3.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.9)** +- *2026.02.15*, **[v19.3.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.8)** +- *2026.02.13*, **[v19.3.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.7)** +- *2026.02.12*, **[v19.3.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.6)** +- *2026.02.08*, **[v19.3.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.5)** +- *2026.02.06*, **[v19.3.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.4)** +- *2026.02.05*, **[v19.3.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.3)** +- *2026.02.04*, **[v19.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.2)** +- *2026.02.03*, **[v19.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.1)** +- *2026.02.03*, **[v19.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.3.0)** +- *2026.02.03*, **[v19.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.2.0)** +- *2026.02.02*, **[v19.1.21](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.21)** +- *2026.01.31*, **[v19.1.20](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.20)** +- *2026.01.30*, **[v19.1.19](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.19)** +- *2026.01.30*, **[v19.1.18](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.18)** +- *2026.01.29*, **[v19.1.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.17)** +- *2026.01.28*, **[v19.1.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.16)** +- *2026.01.28*, **[v19.1.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.15)** +- *2026.01.28*, **[v19.1.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.14)** +- *2026.01.27*, **[v19.1.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.13)** +- *2026.01.27*, **[v19.1.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.12)** +- *2026.01.25*, **[v19.1.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.11)** +- *2026.01.25*, **[v19.1.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.10)** +- *2026.01.21*, **[v19.1.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.9)** +- *2026.01.20*, **[v19.1.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.8)** +- *2026.01.17*, **[v19.1.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.7)** +- *2026.01.16*, **[v19.1.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.6)** +- *2026.01.16*, **[v19.1.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.5)** +- *2026.01.15*, **[v19.1.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.4)** +- *2026.01.15*, **[v19.1.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.3)** +- *2026.01.14*, **[v19.1.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.2)** +- *2026.01.12*, **[v19.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.1)** +- *2025.12.31*, **[v19.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.1.0)** +- *2025.12.24*, **[v19.0.17](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.17)** +- *2025.12.05*, **[v19.0.16](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.16)** +- *2025.11.28*, **[v19.0.15](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.15)** +- *2025.11.27*, **[v19.0.14](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.14)** +- *2025.09.26*, **[v19.0.13](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.13)** +- *2025.09.25*, **[v19.0.12](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.12)** +- *2025.09.24*, **[v19.0.11](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.11)** +- *2025.09.23*, **[v19.0.10](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.10)** +- *2025.09.22*, **[v19.0.9](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.9)** +- *2025.09.20*, **[v19.0.8](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.8)** +- *2025.09.18*, **[v19.0.7](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.7)** +- *2025.09.17*, **[v19.0.6](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.6)** +- *2025.09.16*, **[v19.0.5](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.5)** +- *2025.09.15*, **[v19.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.4)** +- *2025.09.15*, **[v19.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.3)** +- *2025.09.14*, **[v19.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.2)** +- *2025.09.14*, **[v19.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.1)** +- *2025.09.14*, **[v19.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v19.0.0)** +- *2025.09.14*, **[v18.8.11](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.11)** +- *2025.09.14*, **[v18.8.10](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.10)** +- *2025.09.14*, **[v18.8.9](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.9)** +- *2025.09.13*, **[v18.8.8](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.8)** +- *2025.09.12*, **[v18.8.7](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.7)** +- *2025.09.12*, **[v18.8.6](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.6)** +- *2025.09.10*, **[v18.8.5](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.5)** +- *2025.09.09*, **[v18.8.4](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.4)** +- *2025.09.04*, **[v18.8.3](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.3)** +- *2025.09.04*, **[v18.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.2)** +- *2025.09.04*, **[v18.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.1)** +- *2025.09.02*, **[v18.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.8.0)** +- *2025.08.30*, **[v18.7.4](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.4)** +- *2025.07.26*, **[v18.7.3](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.3)** +- *2025.07.24*, **[v18.7.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.2)** +- *2025.07.06*, **[v18.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.1)** +- *2025.07.05*, **[v18.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.7.0)** +- *2025.07.04*, **[v18.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.6.1)** +- *2025.07.02*, **[v18.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.6.0)** +- *2025.04.10*, **[v18.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.2)** +- *2025.02.03*, **[v18.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.1)** +- *2025.01.20*, **[v18.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.5.0)** +- *2024.12.13*, **[v18.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.4.1)** +- *2024.11.22*, **[v18.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.4.0)** +- *2024.11.14*, **[v18.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.3.0)** +- *2024.11.06*, **[v18.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.2.1)** +- *2024.10.27*, **[v18.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.2.0)** +- *2024.08.17*, **[v18.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.1.0)** +- *2024.08.16*, **[v18.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.2)** +- *2024.08.16*, **[v18.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.1)** +- *2024.08.16*, **[v18.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v18.0.0)** +- *2024.08.16*, **[v17.4.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.4)** +- *2024.07.27*, **[v17.4.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.3)** +- *2024.07.03*, **[v17.4.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.2)** +- *2024.05.06*, **[v17.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.1)** +- *2024.04.17*, **[v17.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.4.0)** +- *2024.04.03*, **[v17.3.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.3)** +- *2024.03.29*, **[v17.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.2)** +- *2024.03.29*, **[v17.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.1)** +- *2024.03.29*, **[v17.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.3.0)** +- *2024.03.29*, **[v17.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.2.1)** +- *2024.03.22*, **[v17.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.2.0)** +- *2024.03.21*, **[v17.1.6](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.6)** +- *2024.03.20*, **[v17.1.5](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.5)** +- *2024.03.20*, **[v17.1.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.4)** +- *2024.03.18*, **[v17.1.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.3)** +- *2024.03.18*, **[v17.1.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.2)** +- *2024.03.16*, **[v17.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.1)** +- *2024.03.16*, **[v17.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.1.0)** +- *2024.03.12*, **[v17.0.7](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.7)** +- *2024.03.11*, **[v17.0.6](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.6)** +- *2024.03.11*, **[v17.0.5](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.5)** +- *2024.02.02*, **[v17.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.4)** +- *2024.02.01*, **[v17.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.3)** +- *2024.02.01*, **[v17.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.2)** +- *2024.01.25*, **[v17.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.1)** +- *2023.12.12*, **[v17.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v17.0.0)** +- *2023.12.08*, **[v16.18.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.18.0)** +- *2023.12.04*, **[v16.17.9](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.9)** +- *2023.12.04*, **[v16.17.8](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.8)** +- *2023.10.12*, **[v16.17.7](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.7)** +- *2023.09.22*, **[v16.17.6](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.6)** +- *2023.09.06*, **[v16.17.5](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.5)** +- *2023.09.05*, **[v16.17.4](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.4)** +- *2023.09.05*, **[v16.17.3](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.3)** +- *2023.09.04*, **[v16.17.2](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.2)** +- *2023.09.04*, **[v16.17.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.1)** +- *2023.09.04*, **[v16.17.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.17.0)** +- *2023.08.10*, **[v16.16.3](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.3)** +- *2023.08.09*, **[v16.16.2](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.2)** +- *2023.08.07*, **[v16.16.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.1)** +- *2023.07.09*, **[v16.16.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.16.0)** +- *2023.05.17*, **[v16.15.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.15.0)** +- *2023.03.21*, **[v16.14.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.14.1)** +- *2023.03.08*, **[v16.14.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.14.0)** +- *2023.01.30*, **[v16.13.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.13.1)** +- *2023.01.29*, **[v16.13.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.13.0)** +- *2023.01.29*, **[v16.12.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.12.0)** +- *2023.01.22*, **[v16.11.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.11.0)** +- *2023.01.19*, **[v16.10.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.10.0)** +- *2023.01.18*, **[v16.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.9.1)** +- *2023.01.17*, **[v16.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.9.0)** +- *2023.01.16*, **[v16.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.8.0)** +- *2023.01.15*, **[v16.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.7.0)** +- *2022.10.20*, **[v16.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.6.1)** +- *2022.10.09*, **[v16.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.6.0)** +- *2022.08.06*, **[v16.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.5.0)** +- *2022.07.20*, **[v16.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.4.1)** +- *2022.07.11*, **[v16.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.4.0)** +- *2022.07.02*, **[v16.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.3.1)** +- *2022.07.01*, **[v16.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.3.0)** +- *2022.06.17*, **[v16.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.2.0)** +- *2022.05.12*, **[v16.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.1.1)** +- *2022.04.23*, **[v16.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.1.0)** +- *2022.04.22*, **[v16.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v16.0.1)** +- *2022.02.19*, **[v16.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v16.0.0)** +- *2022.01.20*, **[v15.9.15](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.15)** +- *2022.01.13*, **[v15.9.14](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.14)** +- *2021.12.23*, **[v15.9.13](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.13)** +- *2021.12.16*, **[v15.9.12](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.12)** +- *2021.12.09*, **[v15.9.11](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.11)** +- *2021.12.04*, **[v15.9.10](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.10)** +- *2021.11.22*, **[v15.9.9](//github.com/coderaiser/cloudcmd/releases/tag/v15.9.9)** +- *2021.02.03*, **[v15.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.6.0)** +- *2021.02.02*, **[v15.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.2)** +- *2021.01.31*, **[v15.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.1)** +- *2021.01.30*, **[v15.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.5.0)** +- *2021.01.26*, **[v15.4.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.4)** +- *2021.01.25*, **[v15.4.3](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.3)** +- *2021.01.21*, **[v15.4.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.2)** +- *2021.01.20*, **[v15.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.1)** +- *2021.01.19*, **[v15.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.4.0)** +- *2021.01.19*, **[v15.3.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.4)** +- *2021.01.17*, **[v15.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.1)** +- *2021.01.17*, **[v15.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.3.0)** +- *2021.01.16*, **[v15.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.2.0)** +- *2021.01.07*, **[v15.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.1.0)** +- *2021.01.05*, **[v15.0.4](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.4)** +- *2020.01.05*, **[v15.0.3](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.3)** +- *2020.12.31*, **[v15.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.2)** +- *2020.12.30*, **[v15.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.1)** +- *2020.12.28*, **[v15.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v15.0.0)** +- *2020.08.21*, **[v14.9.3](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.3)** +- *2020.08.19*, **[v14.9.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.2)** +- *2020.08.16*, **[v14.9.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.1)** +- *2020.08.14*, **[v14.9.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.9.0)** +- *2020.08.14*, **[v14.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.8.0)** +- *2020.08.11*, **[v14.7.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.2)** +- *2020.08.11*, **[v14.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.1)** +- *2020.08.10*, **[v14.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.7.0)** +- *2020.05.20*, **[v14.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.6.0)** +- *2020.05.06*, **[v14.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.5.1)** +- *2020.05.04*, **[v14.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.5.0)** +- *2020.04.27*, **[v14.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.4.0)** +- *2020.04.13*, **[v14.3.10](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.10)** +- *2020.04.10*, **[v14.3.9](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.9)** +- *2020.04.04*, **[v14.3.8](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.8)** +- *2020.04.03*, **[v14.3.7](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.7)** +- *2020.03.31*, **[v14.3.6](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.6)** +- *2020.03.25*, **[v14.3.5](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.5)** +- *2020.03.03*, **[v14.3.4](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.4)** +- *2020.02.23*, **[v14.3.3](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.3)** +- *2020.02.21*, **[v14.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.2)** +- *2020.02.21*, **[v14.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.1)** +- *2020.01.29*, **[v14.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.3.0)** +- *2019.12.21*, **[v14.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.2.1)** +- *2019.11.15*, **[v14.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.2.0)** +- *2019.10.16*, **[v14.1.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.1.2)** +- *2019.10.09*, **[v14.1.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.1.1)** +- *2019.10.02*, **[v14.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.1.0)** +- *2019.09.25*, **[v14.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v14.0.2)** +- *2019.09.25*, **[v14.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v14.0.1)** +- *2019.09.22*, **[v14.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v14.0.0)** +- *2019.09.21*, **[v13.4.2](//github.com/coderaiser/cloudcmd/releases/tag/v13.4.2)** +- *2019.09.20*, **[v13.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v13.4.1)** +- *2019.09.03*, **[v13.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v13.4.0)** +- *2019.08.28*, **[v13.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v13.3.0)** +- *2019.08.22*, **[v13.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v13.2.1)** +- *2019.08.20*, **[v13.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v13.2.0)** +- *2019.08.16*, **[v13.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v13.1.0)** +- *2019.08.14*, **[v13.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v13.0.1)** +- *2019.08.14*, **[v13.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v13.0.0)** +- *2019.08.02*, **[v12.6.3](//github.com/coderaiser/cloudcmd/releases/tag/v12.6.3)** +- *2019.06.06*, **[v12.6.2](//github.com/coderaiser/cloudcmd/releases/tag/v12.6.2)** +- *2019.06.04*, **[v12.6.1](//github.com/coderaiser/cloudcmd/releases/tag/v12.6.1)** +- *2019.05.31*, **[v12.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.6.0)** +- *2019.05.28*, **[v12.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.5.0)** +- *2019.05.27*, **[v12.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.4.0)** +- *2019.05.24*, **[v12.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v12.3.2)** +- *2019.05.24*, **[v12.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v12.3.1)** +- *2019.05.24*, **[v12.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.3.0)** +- *2019.05.13*, **[v12.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.2.0)** +- *2019.04.15*, **[v12.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.1.0)** +- *2019.04.04*, **[v12.0.2](//github.com/coderaiser/cloudcmd/releases/tag/v12.0.2)** +- *2019.04.04*, **[v12.0.1](//github.com/coderaiser/cloudcmd/releases/tag/v12.0.1)** +- *2019.04.01*, **[v12.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v12.0.0)** +- *2019.03.27*, **[v11.8.6](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.6)** +- *2019.02.20*, **[v11.8.5](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.5)** +- *2019.02.15*, **[v11.8.4](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.4)** +- *2018.12.04*, **[v11.8.3](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.3)** +- *2018.11.22*, **[v11.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.2)** +- *2018.11.14*, **[v11.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.1)** +- *2018.11.09*, **[v11.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.8.0)** +- *2018.11.01*, **[v11.7.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.7.1)** +- *2018.10.25*, **[v11.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.7.0)** +- *2018.10.23*, **[v11.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.6.0)** +- *2018.10.23*, **[v11.5.4](//github.com/coderaiser/cloudcmd/releases/tag/v11.5.4)** +- *2018.10.19*, **[v11.5.3](//github.com/coderaiser/cloudcmd/releases/tag/v11.5.3)** +- *2018.10.19*, **[v11.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v11.5.2)** +- *2018.10.17*, **[v11.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.5.1)** +- *2018.10.16*, **[v11.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.5.0)** +- *2018.10.16*, **[v11.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.4.1)** +- *2018.10.12*, **[v11.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.4.0)** +- *2018.10.12*, **[v11.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.3.1)** +- *2018.10.11*, **[v11.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.3.0)** +- *2018.10.04*, **[v11.2.3](//github.com/coderaiser/cloudcmd/releases/tag/v11.2.3)** +- *2018.09.28*, **[v11.2.2](//github.com/coderaiser/cloudcmd/releases/tag/v11.2.2)** +- *2018.09.28*, **[v11.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v11.2.1)** +- *2018.09.28*, **[v11.2.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.2.0)** +- *2018.09.27*, **[v11.1.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.1.0)** +- *2018.09.26*, **[v11.0.0](//github.com/coderaiser/cloudcmd/releases/tag/v11.0.0)** +- *2018.09.21*, **[v10.8.2](//github.com/coderaiser/cloudcmd/releases/tag/v10.8.2)** +- *2018.09.20*, **[v10.8.1](//github.com/coderaiser/cloudcmd/releases/tag/v10.8.1)** +- *2018.09.19*, **[v10.8.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.8.0)** +- *2018.09.14*, **[v10.7.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.7.0)** +- *2018.09.10*, **[v10.6.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.6.0)** +- *2018.09.08*, **[v10.5.3](//github.com/coderaiser/cloudcmd/releases/tag/v10.5.3)** +- *2018.08.25*, **[v10.5.2](//github.com/coderaiser/cloudcmd/releases/tag/v10.5.2)** +- *2018.08.25*, **[v10.5.1](//github.com/coderaiser/cloudcmd/releases/tag/v10.5.1)** +- *2018.08.23*, **[v10.5.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.5.0)** +- *2018.07.18*, **[v10.4.1](//github.com/coderaiser/cloudcmd/releases/tag/v10.4.1)** +- *2018.07.17*, **[v10.4.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.4.0)** +- *2018.07.02*, **[v10.3.2](//github.com/coderaiser/cloudcmd/releases/tag/v10.3.2)** +- *2018.06.25*, **[v10.3.1](//github.com/coderaiser/cloudcmd/releases/tag/v10.3.1)** +- *2018.06.22*, **[v10.3.0](//github.com/coderaiser/cloudcmd/releases/tag/v10.3.0)** - *2018.06.21*, **[v10.2.3](//github.com/coderaiser/cloudcmd/releases/tag/v10.2.3)** - *2018.06.15*, **[v10.2.2](//github.com/coderaiser/cloudcmd/releases/tag/v10.2.2)** - *2018.06.13*, **[v10.2.1](//github.com/coderaiser/cloudcmd/releases/tag/v10.2.1)** @@ -964,12 +1754,11 @@ Version history - *2012.07.11*, **[v0.1.1](//github.com/cloudcmd/archive/raw/master/cloudcmd-v0.1.1.zip)** - *2012.07.09*, **[v0.1.0](//github.com/cloudcmd/archive/raw/master/cloudcmd-v0.1.0.zip)** -Special Thanks ---------------- -- [Olena Zalitok](https://zalitok.github.io/ "Olena Zalitok") for **logo** and **favicon**. -- [TarZak](https://github.com/tarzak "TarZak") - - Russian and Ukrainian translations; - - config template and style; - - change order of directories and files; - - add ability do not hide path and header when files are scrolling; +## Special Thanks +- [Olena Zalitok](https://www.linkedin.com/in/ozalitok-ux-ui/ "Olena Zalitok") for **logo** and **favicon**. +- [TarZak](https://github.com/tarzak "TarZak") + - Russian and Ukrainian translations; + - config template and style; + - change order of directories and files; + - add ability to keep path and header when files are scrolling; diff --git a/LICENSE b/LICENSE index 37f32f80..a0d7436b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2012-2018 Coderaiser +Copyright (c) 2012-2025 Coderaiser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index a008b59d..dfe8d8c6 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,24 @@ -# Cloud Commander v10.2.3 [![License][LicenseIMGURL]][LicenseURL] [![NPM version][NPMIMGURL]][NPMURL] [![Dependency Status][DependencyStatusIMGURL]][DependencyStatusURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Now status][BuildAppveyorIMGURL]][BuildAppveyorURL] [![Package Quality][PackageQualityIMGURL]][PackageQualityURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![OpenCollective](https://opencollective.com/cloudcmd/backers/badge.svg)](#backers) [![OpenCollective](https://opencollective.com/cloudcmd/sponsors/badge.svg)](#sponsors) +# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] -### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL], [Now][NowURL]) +### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] -[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png -[MainURL]: http://cloudcmd.io "Main" -[BlogURL]: http://blog.cloudcmd.io "Blog" -[HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku" -[NowURL]: https://cloudcmd.now.sh/ "Now" -[NPMURL]: https://npmjs.org/package/cloudcmd "npm" -[NPMIMGURL]: https://img.shields.io/npm/v/cloudcmd.svg?style=flat -[LicenseURL]: https://tldrlegal.com/license/mit-license "MIT License" -[LicenseIMGURL]: https://img.shields.io/badge/license-MIT-317BF9.svg?style=flat -[DependencyStatusURL]: https://david-dm.org/coderaiser/cloudcmd -[DependencyStatusIMGURL]: https://img.shields.io/david/coderaiser/cloudcmd.svg?style=flat -[BuildStatusURL]: https://travis-ci.org/coderaiser/cloudcmd "Build Status" -[BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/cloudcmd.svg?style=flat +[MainURL]: https://cloudcmd.io "Main" +[BlogURL]: https://blog.cloudcmd.io "Blog" +[SupportURL]: https://patreon.com/coderaiser "Patreon" +[DemoURL]: https://cloudcmd-zdp6.onrender.com +[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png +[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" +[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg +[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd +[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 +[GitterURL]: https://gitter.im/cloudcmd/hello +[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg +[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" +[DeployIMG]: https://www.herokucdn.com/deploy/button.png +[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue +[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd -[BuildAppveyorURL]: https://ci.appveyor.com/project/coderaiser/cloudcmd -[BuildAppveyorIMGURL]: https://ci.appveyor.com/api/projects/status/tse6sc8dxrqxqehi?svg=true - -[PackageQualityURL]: http://packagequality.com/#?package=cloudcmd "Package Quality" -[PackageQualityIMGURL]: http://packagequality.com/shield/cloudcmd.svg - -[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd -[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 - -[GitterURL]: https://gitter.im/cloudcmd/hello -[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg - -[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" -[DeployIMG]: https://www.herokucdn.com/deploy/button.png - -**Cloud Commander** is an orthodox web file manager with console and editor. +**Cloud Commander** a file manager for the web with console and editor. ![Cloud Commander](https://cloudcmd.io/img/logo/cloudcmd.png "Cloud Commander") @@ -40,6 +27,7 @@ ``` npm i cloudcmd -g ``` + ## Start For starting just type in console: @@ -57,8 +45,8 @@ Open url `http://localhost:8000` in browser. You will see something similar to this. ![View](https://cloudcmd.io/img/screen/view.png "View") - ## Deploy + `Cloud Commander` could be easily deployed to [Heroku][DeployURL]. [![Deploy][DeployIMG]][DeployURL] @@ -82,33 +70,31 @@ npm i cloudcmd express socket.io -S And create `index.js`: ```js -const http = require('http'); -const cloudcmd = require('cloudcmd'); -const io = require('socket.io'); -const app = require('express')(); +import http from 'node:http'; +import {cloudcmd} from 'cloudcmd'; +import {Server} from 'socket.io'; +import express from 'express'; + +const app = express(); const port = 1337; -const prefix = '/cloudcmd'; +const prefix = '/'; const server = http.createServer(app); -const socket = io.listen(server, { - path: `${prefix}/socket.io` +const socket = new Server(server, { + path: `${prefix}socket.io`, }); const config = { - prefix // base URL or function which returns base URL (optional) + name: 'cloudcmd :)', }; -const plugins = [ - __dirname + '/plugin.js' -]; - const filePicker = { data: { FilePicker: { key: 'key', - } - } + }, + }, }; // override option from json/modules.json @@ -116,22 +102,40 @@ const modules = { filePicker, }; -app.use(cloudcmd({ - socket, // used by Config, Edit (optional) and Console (required) - config, // config data (optional) - plugins, // optional +const { + createConfigManager, + configPath, +} = cloudcmd; + +const configManager = createConfigManager({ + configPath, +}); + +app.use(prefix, cloudcmd({ + socket, // used by Config, Edit (optional) and Console (required) + config, // config data (optional) modules, // optional + configManager, // optional })); server.listen(port); ``` -Docker ---------------- +## Docker + +The docker images are provided for multiple architectures and types. The following list shows all existing images: + +| Architecture | Type | +|----------------|--------------| +| amd64 | linux | +| arm64 (arm/v8) | linux | +| amd64 | linux-alpine | +| arm64 (arm/v8) | linux-alpine | + `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way: ```sh -docker run -t --rm -v ~:/root -v /:/mnt/fs -p 8000:8000 coderaiser/cloudcmd +docker run -it --rm -v ~:/root -v /:/mnt/fs -w=/root -p 8000:8000 coderaiser/cloudcmd ``` Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`, @@ -157,84 +161,19 @@ When you create this file run: docker-compose up ``` -Get involved ---------------- +## Documentation + +More documentation you can find on https://cloudcmd.io/. + +## Get involved There is a lot ways to be involved in `Cloud Commander` development: +- support project on patreon: https://patreon.com/coderaiser; - if you find a bug or got idea to share [create an issue](https://github.com/coderaiser/cloudcmd/issues/new "Create issue"); - if you fixed a bug, typo or implemented new feature [create pull request](https://github.com/coderaiser/cloudcmd/compare "Create pull request"); - if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki"); -## Backers -Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/cloudcmd#backer)] - -[![](https://opencollective.com/cloudcmd/backer/0/avatar.svg)](https://opencollective.com/cloudcmd/backer/0/website) -[![](https://opencollective.com/cloudcmd/backer/1/avatar.svg)](https://opencollective.com/cloudcmd/backer/1/website) -[![](https://opencollective.com/cloudcmd/backer/2/avatar.svg)](https://opencollective.com/cloudcmd/backer/2/website) -[![](https://opencollective.com/cloudcmd/backer/3/avatar.svg)](https://opencollective.com/cloudcmd/backer/3/website) -[![](https://opencollective.com/cloudcmd/backer/4/avatar.svg)](https://opencollective.com/cloudcmd/backer/4/website) -[![](https://opencollective.com/cloudcmd/backer/5/avatar.svg)](https://opencollective.com/cloudcmd/backer/5/website) -[![](https://opencollective.com/cloudcmd/backer/6/avatar.svg)](https://opencollective.com/cloudcmd/backer/6/website) -[![](https://opencollective.com/cloudcmd/backer/7/avatar.svg)](https://opencollective.com/cloudcmd/backer/7/website) -[![](https://opencollective.com/cloudcmd/backer/8/avatar.svg)](https://opencollective.com/cloudcmd/backer/8/website) -[![](https://opencollective.com/cloudcmd/backer/9/avatar.svg)](https://opencollective.com/cloudcmd/backer/9/website) -[![](https://opencollective.com/cloudcmd/backer/10/avatar.svg)](https://opencollective.com/cloudcmd/backer/10/website) -[![](https://opencollective.com/cloudcmd/backer/11/avatar.svg)](https://opencollective.com/cloudcmd/backer/11/website) -[![](https://opencollective.com/cloudcmd/backer/12/avatar.svg)](https://opencollective.com/cloudcmd/backer/12/website) -[![](https://opencollective.com/cloudcmd/backer/13/avatar.svg)](https://opencollective.com/cloudcmd/backer/13/website) -[![](https://opencollective.com/cloudcmd/backer/14/avatar.svg)](https://opencollective.com/cloudcmd/backer/14/website) -[![](https://opencollective.com/cloudcmd/backer/15/avatar.svg)](https://opencollective.com/cloudcmd/backer/15/website) -[![](https://opencollective.com/cloudcmd/backer/16/avatar.svg)](https://opencollective.com/cloudcmd/backer/16/website) -[![](https://opencollective.com/cloudcmd/backer/17/avatar.svg)](https://opencollective.com/cloudcmd/backer/17/website) -[![](https://opencollective.com/cloudcmd/backer/18/avatar.svg)](https://opencollective.com/cloudcmd/backer/18/website) -[![](https://opencollective.com/cloudcmd/backer/19/avatar.svg)](https://opencollective.com/cloudcmd/backer/19/website) -[![](https://opencollective.com/cloudcmd/backer/20/avatar.svg)](https://opencollective.com/cloudcmd/backer/20/website) -[![](https://opencollective.com/cloudcmd/backer/21/avatar.svg)](https://opencollective.com/cloudcmd/backer/21/website) -[![](https://opencollective.com/cloudcmd/backer/22/avatar.svg)](https://opencollective.com/cloudcmd/backer/22/website) -[![](https://opencollective.com/cloudcmd/backer/23/avatar.svg)](https://opencollective.com/cloudcmd/backer/23/website) -[![](https://opencollective.com/cloudcmd/backer/24/avatar.svg)](https://opencollective.com/cloudcmd/backer/24/website) -[![](https://opencollective.com/cloudcmd/backer/25/avatar.svg)](https://opencollective.com/cloudcmd/backer/25/website) -[![](https://opencollective.com/cloudcmd/backer/26/avatar.svg)](https://opencollective.com/cloudcmd/backer/26/website) -[![](https://opencollective.com/cloudcmd/backer/27/avatar.svg)](https://opencollective.com/cloudcmd/backer/27/website) -[![](https://opencollective.com/cloudcmd/backer/28/avatar.svg)](https://opencollective.com/cloudcmd/backer/28/website) -[![](https://opencollective.com/cloudcmd/backer/29/avatar.svg)](https://opencollective.com/cloudcmd/backer/29/website) - -## Sponsors -Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/cloudcmd#sponsor)] - -[![](https://opencollective.com/cloudcmd/sponsor/0/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/0/website) -[![](https://opencollective.com/cloudcmd/sponsor/1/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/1/website) -[![](https://opencollective.com/cloudcmd/sponsor/2/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/2/website) -[![](https://opencollective.com/cloudcmd/sponsor/3/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/3/website) -[![](https://opencollective.com/cloudcmd/sponsor/4/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/4/website) -[![](https://opencollective.com/cloudcmd/sponsor/5/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/5/website) -[![](https://opencollective.com/cloudcmd/sponsor/6/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/6/website) -[![](https://opencollective.com/cloudcmd/sponsor/7/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/7/website) -[![](https://opencollective.com/cloudcmd/sponsor/8/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/8/website) -[![](https://opencollective.com/cloudcmd/sponsor/9/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/9/website) -[![](https://opencollective.com/cloudcmd/sponsor/10/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/10/website) -[![](https://opencollective.com/cloudcmd/sponsor/11/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/11/website) -[![](https://opencollective.com/cloudcmd/sponsor/12/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/12/website) -[![](https://opencollective.com/cloudcmd/sponsor/13/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/13/website) -[![](https://opencollective.com/cloudcmd/sponsor/14/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/14/website) -[![](https://opencollective.com/cloudcmd/sponsor/15/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/15/website) -[![](https://opencollective.com/cloudcmd/sponsor/16/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/16/website) -[![](https://opencollective.com/cloudcmd/sponsor/17/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/17/website) -[![](https://opencollective.com/cloudcmd/sponsor/18/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/18/website) -[![](https://opencollective.com/cloudcmd/sponsor/19/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/19/website) -[![](https://opencollective.com/cloudcmd/sponsor/20/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/20/website) -[![](https://opencollective.com/cloudcmd/sponsor/21/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/21/website) -[![](https://opencollective.com/cloudcmd/sponsor/22/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/22/website) -[![](https://opencollective.com/cloudcmd/sponsor/23/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/23/website) -[![](https://opencollective.com/cloudcmd/sponsor/24/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/24/website) -[![](https://opencollective.com/cloudcmd/sponsor/25/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/25/website) -[![](https://opencollective.com/cloudcmd/sponsor/26/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/26/website) -[![](https://opencollective.com/cloudcmd/sponsor/27/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/27/website) -[![](https://opencollective.com/cloudcmd/sponsor/28/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/28/website) -[![](https://opencollective.com/cloudcmd/sponsor/29/avatar.svg)](https://opencollective.com/cloudcmd/sponsor/29/website) - ## License MIT - diff --git a/app.json b/app.json index 56197d23..3dbe05a0 100644 --- a/app.json +++ b/app.json @@ -11,6 +11,7 @@ "folder", "orthodox" ], + "stack": "heroku-18", "env": { "NPM_CONFIG_PRODUCTION": { "description": "Keep false to install devDependencies and build frontend", @@ -32,6 +33,11 @@ "value": "false", "required": false }, + "CLOUDCMD_CONFIG_AUTH": { + "description": "disable auth change in config dialog", + "value": "false", + "required": false + }, "CLOUDCMD_CONSOLE": { "description": "enable console", "value": "true", @@ -52,9 +58,9 @@ "value": "gritty", "required": false }, - "CLOUDCMD_CONFIG_DIALOG": { - "description": "show config dialog", - "value": "false", + "CLOUDCMD_TERMINAL_COMMAND": { + "description": "set command to run in terminal (shell by default)", + "value": "", "required": false }, "CLOUDCMD_ONE_FILE_PANEL": { @@ -102,6 +108,11 @@ "value": "name-size-date-owner-mode", "required": false }, + "CLOUDCMD_OPEN": { + "description": "open web browser when server started", + "value": "false", + "required": false + }, "CLOUDCMD_CONFIRM_COPY": { "description": "confirm copy", "value": "true", @@ -111,6 +122,41 @@ "description": "confirm move", "value": "true", "required": false + }, + "CLOUDCMD_SHOW_FILE_NAME": { + "description": "show file name in view and edit", + "value": "false", + "required": false + }, + "CLOUDCMD_EXPORT": { + "description": "enable export of config through a server", + "value": "false", + "required": false + }, + "CLOUDCMD_EXPORT_TOKEN": { + "description": "authorization token used by export server", + "value": "root", + "required": false + }, + "CLOUDCMD_IMPORT": { + "description": "enable import of config", + "value": "false", + "required": false + }, + "CLOUDCMD_IMPORT_TOKEN": { + "description": "authorization token used to connect to export server", + "value": "root", + "required": false + }, + "CLOUDCMD_IMPORT_URL": { + "description": "url of an import server", + "value": "http://localhost:8000", + "required": false + }, + "CLOUDCMD_IMPORT_LISTEN": { + "description": "enable listen on config updates from import server", + "value": "false", + "required": false } } } diff --git a/bin/cloudcmd.js b/bin/cloudcmd.js index e2dd3e8f..6ca6716c 100755 --- a/bin/cloudcmd.js +++ b/bin/cloudcmd.js @@ -1,23 +1,48 @@ #!/usr/bin/env node -'use strict'; +import process from 'node:process'; +import {promisify} from 'node:util'; +import {tryToCatch} from 'try-to-catch'; +import parse from 'yargs-parser'; +import exit from '../server/exit.js'; +import {createConfig, configPath} from '../server/config.js'; +import * as env from '../server/env.js'; +import prefixer from '../server/prefixer.js'; +import * as validate from '../server/validate.js'; +import Info from '../package.json' with { + type: 'json', +}; -const Info = require('../package'); -const DIR_SERVER = '../server/'; +process.on('unhandledRejection', exit); -const exit = require(DIR_SERVER + 'exit'); -const config = require(DIR_SERVER + 'config'); -const env = require(DIR_SERVER + 'env'); +const isUndefined = (a) => typeof a === 'undefined'; const choose = (a, b) => { - if (a === undefined) + if (isUndefined(a)) return b; return a; }; -const argv = process.argv; -const args = require('minimist')(argv.slice(2), { +const config = createConfig({ + configPath, +}); + +const maybeRoot = (a) => { + if (a === '.') + return process.cwd(); + + return a; +}; + +const yargsOptions = { + configuration: { + 'strip-aliased': true, + 'strip-dashed': true, + }, + coerce: { + root: maybeRoot, + }, string: [ 'name', 'port', @@ -28,8 +53,16 @@ const args = require('minimist')(argv.slice(2), { 'packer', 'root', 'prefix', + 'prefix-socket', 'terminal-path', + 'terminal-command', 'columns', + 'menu', + 'theme', + 'import-url', + 'import-token', + 'export-token', + 'dropbox-token', ], boolean: [ 'auth', @@ -38,61 +71,91 @@ const args = require('minimist')(argv.slice(2), { 'server', 'online', 'open', - 'progress', 'config-dialog', + 'config-auth', + 'config-port', 'console', 'sync-console-path', 'contact', 'terminal', + 'terminal-auto-restart', 'one-file-panel', 'confirm-copy', 'confirm-move', 'show-config', + 'show-dot-files', + 'show-file-name', 'vim', 'keys-panel', + 'color', + 'export', + 'import', + 'import-listen', + 'log', + 'zip', + 'dropbox', ], default: { - server : true, - name : choose(env('name'), config('name')), - auth : choose(env('auth'), config('auth')), - port : config('port'), - online : config('online'), - open : config('open'), - editor : env('editor') || config('editor'), - packer : config('packer') || 'tar', - zip : config('zip'), - username : env('username') || config('username'), - root : choose(env('root'), config('root')), - prefix : config('prefix'), - progress : config('progress'), - console : choose(env.bool('console'), config('console')), - contact : choose(env.bool('contact'), config('contact')), - terminal : choose(env.bool('terminal'), config('terminal')), + 'server': true, + 'name': choose(env.parse('name'), config('name')), + 'auth': choose(env.bool('auth'), config('auth')), + 'port': config('port'), + 'online': config('online'), + 'open': choose(env.bool('open'), config('open')), + 'editor': env.parse('editor') || config('editor'), + 'menu': env.parse('menu') || config('menu'), + 'packer': config('packer') || 'tar', + 'zip': config('zip'), + 'username': env.parse('username') || config('username'), + 'root': choose(env.parse('root'), config('root')), + 'prefix': choose(env.parse('prefix'), config('prefix')), + 'console': choose(env.bool('console'), config('console')), + 'contact': choose(env.bool('contact'), config('contact')), + 'terminal': choose(env.bool('terminal'), config('terminal')), + 'columns': env.parse('columns') || config('columns') || '', + 'theme': env.parse('theme') || config('theme') || '', + 'vim': choose(env.bool('vim'), config('vim')), + 'log': choose(env.bool('log'), config('log')), + 'import-url': env.parse('import_url') || config('importUrl'), + 'import-listen': choose(env.bool('import_listen'), config('importListen')), + 'import': choose(env.bool('import'), config('import')), + 'export': choose(env.bool('export'), config('export')), + + 'prefix-socket': config('prefixSocket'), + 'show-dot-files': choose(env.bool('show_dot_files'), config('showDotFiles')), + 'show-file-name': choose(env.bool('show_file_name'), config('showFileName')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), - 'terminal-path': env('terminal_path') || config('terminalPath'), + 'config-auth': choose(env.bool('config_auth'), config('configAuth')), + 'config-port': choose(env.bool('config_port'), config('configPort')), + 'terminal-path': env.parse('terminal_path') || config('terminalPath'), + 'terminal-command': env.parse('terminal_command') || config('terminalCommand'), + 'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')), 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), - 'vim': choose(env.bool('vim'), config('vim')), - 'columns': env('columns') || config('columns') || '', 'keys-panel': env.bool('keys_panel') || config('keysPanel'), + 'import-token': env.parse('import_token') || config('importToken'), + 'export-token': env.parse('export_token') || config('exportToken'), + + 'dropbox': config('dropbox'), + 'dropbox-token': config('dropboxToken') || '', }, alias: { - v: 'version', - h: 'help', - p: 'password', - o: 'online', - u: 'username', - s: 'save', - a: 'auth', - c: 'config' + version: 'v', + help: 'h', + password: 'p', + online: 'o', + username: 'u', + save: 's', + auth: 'a', + config: 'c', }, - unknown: (cmd) => { - exit('\'%s\' is not a cloudcmd option. See \'cloudcmd --help\'.', cmd); - } -}); +}; + +const {argv} = process; +const args = parse(argv.slice(2), yargsOptions); if (args.version) version(); @@ -101,11 +164,22 @@ else if (args.help) else main(); -function main() { - if (args.repl) - repl(); +async function main() { + const {validateArgs} = await import('@putout/cli-validate-args'); - checkUpdate(); + const error = await validateArgs(args, [ + ...yargsOptions.boolean, + ...yargsOptions.string, + ]); + + if (error) + return exit(error); + + if (args.repl) + await repl(); + + validate.columns(args.columns); + validate.theme(args.theme); port(args.port); @@ -114,59 +188,88 @@ function main() { config('online', args.online); config('open', args.open); config('username', args.username); - config('progress', args.progress); config('console', args.console); - config('syncConsolePath', args['sync-console-path']); + config('syncConsolePath', args.syncConsolePath); + config('showDotFiles', args.showDotFiles); + config('showFileName', args.showFileName); config('contact', args.contact); config('terminal', args.terminal); - config('terminalPath', args['terminal-path']); + config('terminalPath', args.terminalPath); + config('terminalCommand', args.terminalCommand); + config('terminalAutoRestart', args.terminalAutoRestart); config('editor', args.editor); - config('prefix', args.prefix); - config('root', args.root); + config('menu', args.menu); + config('prefix', prefixer(args.prefix)); + config('prefixSocket', prefixer(args.prefixSocket)); + config('root', args.root || '/'); config('vim', args.vim); + config('theme', args.theme); config('columns', args.columns); - config('confirmCopy', args['confirm-copy']); - config('confirmMove', args['confirm-move']); - config('oneFilePanel', args['one-file-panel']); - config('configDialog', args['config-dialog']); - config('keysPanel', args['keys-panel']); + config('log', args.log); + config('confirmCopy', args.confirmCopy); + config('confirmMove', args.confirmMove); + config('oneFilePanel', args.oneFilePanel); + config('configDialog', args.configDialog); + config('configAuth', args.configAuth); + config('configPort', args.configPort); + config('keysPanel', args.keysPanel); + config('export', args.export); + config('exportToken', args.exportToken); + config('import', args.import); + config('importToken', args.importToken); + config('importListen', args.importListen); + config('importUrl', args.importUrl); - readConfig(args.config); + config('dropbox', args.dropbox); + config('dropboxToken', args.dropboxToken); + + await readConfig(args.config); const options = { - root: args.root || '/', /* --no-root */ - editor: args.editor, - packer: args.packer, - prefix: args.prefix || '', /* --no-prefix */ - columns: args.columns, + root: config('root'), + editor: config('editor'), + packer: config('packer'), + prefix: config('prefix'), + prefixSocket: config('prefixSocket'), + columns: config('columns'), + theme: config('theme'), + menu: config('menu'), }; - const password = env('password') || args.password; + const password = env.parse('password') || args.password; if (password) - config('password', getPassword(password)); + config('password', await getPassword(password)); - validateRoot(options.root); + validateRoot(options.root, config); - if (args['show-config']) - showConfig(); + if (args.showConfig) + await showConfig(); - if (!args.save) - return start(options); + const {distributeImport} = await import('../server/distribute/import.js'); + const importConfig = promisify(distributeImport); - config.save(() => { - start(options); - }); + await start(options, config); + + if (args.save) + config.write(); + + await tryToCatch(checkUpdate); + await importConfig(config); } -function validateRoot(root) { - const validate = require(DIR_SERVER + 'validate'); - validate.root(root, console.log); +function validateRoot(root, config) { + validate.root(root, config); + + if (root === '/') + return; + + if (config('log')) + console.log(`root: ${root}`); } -function getPassword(password) { - const criton = require('criton'); - +async function getPassword(password) { + const {default: criton} = await import('criton'); return criton(password, config('algo')); } @@ -174,11 +277,12 @@ function version() { console.log('v' + Info.version); } -function start(config) { - const SERVER = DIR_SERVER + 'server'; +async function start(options, config) { + if (!args.server) + return; - if (args.server) - require(SERVER)(config); + const {default: server} = await import('../server/server.js'); + server(options, config); } function port(arg) { @@ -190,40 +294,38 @@ function port(arg) { exit('cloudcmd --port: should be a number'); } -function showConfig() { - const show = require('../server/show-config'); - const data = show(config('*')); +async function showConfig() { + const {showConfig} = await import('../server/show-config.js'); + const data = showConfig(config('*')); console.log(data); } -function readConfig(name) { +async function readConfig(name) { if (!name) return; - const fs = require('fs'); - const tryCatch = require('try-catch'); - const jju = require('jju'); - const forEachKey = require('for-each-key'); + const {default: forEachKey} = await import('for-each-key'); - const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), { - mode: 'json' + const data = await import(name, { + with: { + type: 'json', + }, }); - const result = tryCatch(readjsonSync, name); - const error = result[0]; - const data = result[1]; - - if (error) - return exit(error.message); - forEachKey(config, data); } -function help() { - const bin = require('../json/help'); - const forEachKey = require('for-each-key'); - const currify = require('currify/legacy'); +async function help() { + const {default: bin} = await import('../json/help.json', { + with: { + type: 'json', + }, + }); + + const {default: forEachKey} = await import('for-each-key'); + const {default: currify} = await import('currify'); + const usage = 'Usage: cloudcmd [options]'; const url = Info.homepage; const log = currify((a, b, c) => console.log(a, b, c)); @@ -234,36 +336,27 @@ function help() { console.log('\nGeneral help using Cloud Commander: <%s>', url); } -function repl() { +async function repl() { console.log('REPL mode enabled (telnet localhost 1337)'); - require(DIR_SERVER + 'repl'); + await import('../server/repl.js'); } -function checkUpdate() { - const load = require('package-json'); - const noop = () => {}; +async function checkUpdate() { + const {default: load} = await import('package-json'); + const {version} = await load(Info.name, 'latest'); - load(Info.name, 'latest') - .then(showUpdateInfo) - .catch(noop); + await showUpdateInfo(version); } -function showUpdateInfo(data) { - const version = data.version; +async function showUpdateInfo(version) { + if (version === Info.version) + return; - if (version !== Info.version) { - const chalk = require('chalk'); - const rendy = require('rendy'); - - const latest = rendy('update available: {{ latest }}', { - latest: chalk.green.bold('v' + version), - }); - - const current = chalk.dim(rendy('(current: v{{ current }})', { - current: Info.version - })); - - console.log('%s %s', latest, current); - } + const {default: chalk} = await import('chalk'); + + const latestVersion = chalk.green.bold(`v${version}`); + const latest = `update available: ${latestVersion}`; + const current = chalk.dim(`(current: v${Info.version})`); + + console.log('%s %s', latest, current); } - diff --git a/bin/cloudcmd.spec.js b/bin/cloudcmd.spec.js new file mode 100644 index 00000000..d09541b9 --- /dev/null +++ b/bin/cloudcmd.spec.js @@ -0,0 +1,26 @@ +import {spawnSync} from 'node:child_process'; +import {test} from 'supertape'; +import info from '../package.json' with { + type: 'json', +}; + +const cliPath = new URL('cloudcmd.js', import.meta.url).pathname; + +test('cloudcmd: bin: cli: -h', (t) => { + const {stdout} = spawnSync(cliPath, ['-h'], { + encoding: 'utf8', + }); + + t.match(stdout, `Options`); + t.end(); +}); + +test('cloudcmd: bin: cli: -v', (t) => { + const {version} = info; + const {stdout} = spawnSync(cliPath, ['-v'], { + encoding: 'utf8', + }); + + t.match(stdout, `v${version}`); + t.end(); +}); diff --git a/bin/release.js b/bin/release.js index 60a2b4ec..86e1d714 100755 --- a/bin/release.js +++ b/bin/release.js @@ -1,67 +1,72 @@ #!/usr/bin/env node -'use strict'; +import {promisify} from 'node:util'; +import process from 'node:process'; +import {tryToCatch} from 'try-to-catch'; +import minor from 'minor'; +import _place from 'place'; +import {rendy} from 'rendy'; +import shortdate from 'shortdate'; +import Info from '../package.json' with { + type: 'json', +}; -const DIR = '../'; -const Info = require(DIR + 'package'); +const place = promisify(_place); -const minor = require('minor'); -const place = require('place'); -const rendy = require('rendy'); -const shortdate = require('shortdate'); +await main(); -const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major'); - -main((error) => { +async function main() { + const history = '## Version history\n\n'; + const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; + + const template = '- ' + + '*{{ date }}*, ' + + '**[v{{ version }}]' + + '(' + + link + + 'v{{ version }})**\n'; + + const {version} = Info; + + const [error, versionNew] = await tryToCatch(cl); + if (error) - console.error(error.message); -}); - -function main(callback) { - const history = 'Version history\n---------------\n'; - const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; - const template = '- *{{ date }}*, ' + - '**[v{{ version }}]' + - '(' + link + 'v{{ version }})**\n'; + return console.error(error); - const version = Info.version; + await replaceVersion('README.md', version, versionNew); + await replaceVersion('HELP.md', version, versionNew); - cl((error, versionNew) => { - if (error) - return callback(error); - - replaceVersion('README.md', version, versionNew, callback); - replaceVersion('HELP.md', version, versionNew, function() { - const historyNew = history + rendy(template, { - date : shortdate(), - version : versionNew - }); - - replaceVersion('HELP.md', history, historyNew, callback); + const historyNew = history + + rendy(template, { + date: shortdate(), + version: versionNew, }); - }); + + await replaceVersion('HELP.md', history, historyNew); } -function replaceVersion(name, version, versionNew, callback) { - place(name, version, versionNew, (error) => { - if (error) - return callback(error); - - callback(null, 'done: ' + name); - }); +async function replaceVersion(name, version, versionNew) { + const [error] = await tryToCatch(place, name, version, versionNew); + + if (error) + return console.error(error); + + console.log(`done: ${name}`); } -function cl(callback) { - const argv = process.argv; +async function cl() { + const {argv} = process; const length = argv.length - 1; const last = process.argv[length]; const regExp = /^--(major|minor|patch)$/; const [, match] = last.match(regExp) || []; - if (!regExp.test(last)) - return callback(ERROR); + console.log(last); - callback(null, getVersionNew(last, match)); + if (!regExp.test(last)) + throw Error('ERROR: version is missing. release --patch|--minor|--major'); + + return getVersionNew(last, match); } function getVersionNew(last, match) { @@ -70,4 +75,3 @@ function getVersionNew(last, match) { return last.substr(3); } - diff --git a/bower.json b/bower.json deleted file mode 100644 index d4333e5e..00000000 --- a/bower.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "cloudcmd", - "homepage": "https://github.com/coderaiser/cloudcmd", - "authors": [ - "coderaiser " - ], - "description": "orthodox web file manager with console and editor", - "main": "bin/cloudcmd.js", - "keywords": [ - "console", - "editor", - "file", - "file manager", - "folder", - "orthodox", - "view", - "copy", - "rename", - "move", - "delete", - "file operation" - ], - "license": "MIT", - "private": true, - "ignore": [ - "node_modules", - "modules", - "test" - ], - "dependencies": { - "olark": "^1.0.0", - "jquery": "3.3.1", - "fancybox": "^2.1.6" - } -} diff --git a/client/client.js b/client/client.js index 0217cd0f..a0e3cb07 100644 --- a/client/client.js +++ b/client/client.js @@ -1,70 +1,59 @@ -'use strict'; - -/* global Util, DOM */ - -const itype = require('itype/legacy'); -const Emitify = require('emitify/legacy'); -const inherits = require('inherits'); -const rendy = require('rendy/legacy'); -const wraptile = require('wraptile/legacy'); -const exec = require('execon'); - -const Images = require('./dom/images'); -const { - unregisterSW, -} = require('./sw/register'); - -const join = require('join-io/www/join'); -const jonny = require('jonny/legacy'); -const currify = require('currify/legacy'); - -const bind = (f, ...a) => () => f(...a); -const noop = () => {}; - -const { +import process from 'node:process'; +import Emitify from 'emitify'; +import inherits from 'inherits'; +import {rendy} from 'rendy'; +import load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {addSlashToEnd} from 'format-io'; +import pascalCase from 'just-pascal-case'; +import currify from 'currify'; +import { apiURL, formatMsg, buildFromJSON, -} = require('../common/cloudfunc'); +} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {unregisterSW} from './sw/register.js'; +import {getJsonFromFileTable} from './get-json-from-file-table.js'; +import {Key} from './key/index.js'; +import {loadModule} from './load-module.js'; -/* global Util, DOM */ +const noJS = (a) => a.replace(/.js$/, ''); + +const isDev = process.env.NODE_ENV === 'development'; inherits(CloudCmdProto, Emitify); -module.exports = new CloudCmdProto(Util, DOM); +export const createCloudCmd = ({DOM, Listeners}) => { + return new CloudCmdProto({ + DOM, + Listeners, + }); +}; -function CloudCmdProto(Util, DOM) { - let Key; - let Debug; - let Listeners; - - const log = (str) => { - if (!Debug) - return; - - console.log(str); - }; - +load.addErrorListener((e, src) => { + const msg = `file ${src} could not be loaded`; + Images.show.error(msg); +}); + +function CloudCmdProto({DOM, Listeners}) { Emitify.call(this); const CloudCmd = this; const Info = DOM.CurrentInfo; - const Storage = DOM.Storage; - const Files = DOM.Files; - this.log = log; - this.PREFIX = ''; - this.PREFIX_URL = ''; - this.DIRCLIENT = '/dist/'; - this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/'; + const {Storage, Files} = DOM; - this.MIN_ONE_PANEL_WIDTH = 1155; - this.HOST = location.origin || - location.protocol + '//' + location.host; - - const TITLE = 'Cloud Commander'; - this.TITLE = TITLE; + this.log = () => { + if (!isDev) + return; + }; + this.prefix = ''; + this.prefixSocket = ''; + this.prefixURL = ''; + this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width'); + this.HOST = location.origin || location.protocol + '//' + location.host; this.sort = { left: 'name', right: 'name', @@ -75,127 +64,51 @@ function CloudCmdProto(Util, DOM) { right: 'asc', }; - log.enable = () => { - Debug = true; - }; - - log.disable = () => { - Debug = false; - }; - - const kebabToCamelCase = Util.kebabToCamelCase; - - /** - * Функция привязываеться ко всем ссылкам и - * загружает содержимое каталогов - * - * @param params - { - * paramLink - ссылка - * needRefresh - необходимость обязательной загрузки данных с сервера - * panel - * } - * @param callback - */ - this.loadDir = (params, callback) => { - const p = params; - - const refresh = p.isRefresh; - const panel = p.panel; - const history = p.history; - const noCurrent = p.noCurrent; - const currentName = p.currentName; + this.changeDir = async (path, overrides = {}) => { + const { + isRefresh, + panel, + history = true, + noCurrent, + currentName, + } = overrides; + const refresh = isRefresh; let panelChanged; - if (!noCurrent) - if (panel && panel !== Info.panel) { - DOM.changePanel(); - panelChanged = true; - } + + if (!noCurrent && panel && panel !== Info.panel) { + DOM.changePanel(); + panelChanged = true; + } let imgPosition; + if (panelChanged || refresh || !history) imgPosition = 'top'; Images.show.load(imgPosition, panel); /* загружаем содержимое каталога */ - ajaxLoad(p.path, { + await ajaxLoad(addSlashToEnd(path), { refresh, history, noCurrent, currentName, - }, panel, callback); + showDotFiles: CloudCmd.config('showDotFiles'), + }, panel); }; - /** - * function load modules - * @params = {name, path, func, dobefore, arg} - */ - function loadModule(params) { - if (!params) - return; - - let path = params.path; - const name = params.name || path && kebabToCamelCase(path); - const func = params.func; - const funcName = params.funcName; - const doBefore = params.dobefore; - - const isContain = /\.js/.test(path); - - if (!isContain) - path += '.js'; - - if (CloudCmd[name]) - return; - - CloudCmd[name] = (...args) => { - const prefix = CloudCmd.PREFIX; - const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path; - - exec(doBefore); - - const done = (error) => { - const Proto = CloudCmd[name]; - - if (error || !itype.function(Proto)) - return; - - CloudCmd[name] = new Proto(...args); - }; - - return DOM.load.js(pathFull, func || done); - }; - - CloudCmd[name][funcName] = CloudCmd[name]; - } - /** * Конструктор CloudClient, который * выполняет весь функционал по * инициализации */ - this.init = (prefix, config) => { - const func = bind(exec.parallel, [ - initModules, - baseInit, - loadPlugins, - loadStyle, - exec.with(CloudCmd.route, location.hash), - ], noop); - - const { - load, - loadJquery, - } = DOM; - - const funcBefore = (callback) => { - const src = prefix + CloudCmd.DIRCLIENT_MODULES + 'polyfill.js'; - loadJquery(wraptile(load.js, src, callback)); - }; - - CloudCmd.PREFIX = prefix; - CloudCmd.PREFIX_URL = prefix + apiURL; + this.init = async (prefix, config) => { + CloudCmd.prefix = prefix; + CloudCmd.prefixURL = `${prefix}${apiURL}`; + CloudCmd.prefixSocket = config.prefixSocket; + CloudCmd.DIR_DIST = `${prefix}/dist`; + CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`; CloudCmd.config = (key) => config[key]; CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a)); @@ -204,7 +117,6 @@ function CloudCmdProto(Util, DOM) { * should be called from config.js only * after successful update on server */ - if (key === 'password') return; @@ -214,34 +126,13 @@ function CloudCmdProto(Util, DOM) { if (config.oneFilePanel) CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; - exec.if(document.body.scrollIntoViewIfNeeded, func, funcBefore); - }; - - function loadStyle(callback) { - const prefix = CloudCmd.PREFIX; - const name = prefix + '/dist/cloudcmd.common.css'; + if (!document.body.scrollIntoViewIfNeeded) + await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`); - DOM.load.css(name, callback); - } - - function loadPlugins(callback) { - const prefix = CloudCmd.PREFIX; - const plugins = prefix + '/plugins.js'; + await initModules(); + await baseInit(); - DOM.load.js(plugins, callback); - } - - this.join = (urls) => { - const prefix = CloudCmd.PREFIX; - - if (!Array.isArray(urls)) - throw Error('urls should be array!'); - - const noPrefixUrls = urls.map((url) => { - return url.replace(prefix, ''); - }); - - return prefix + join(noPrefixUrls); + CloudCmd.route(location.hash); }; this.route = (path) => { @@ -251,96 +142,87 @@ function CloudCmdProto(Util, DOM) { return; const [kebabModule] = query; - const module = kebabToCamelCase(kebabModule.slice(1)); + const module = noJS(pascalCase(kebabModule.slice(1))); - const file = query[1]; + const [, file] = query; const current = DOM.getCurrentByName(file); if (file && !current) { const msg = formatMsg('set current file', file, 'error'); CloudCmd.log(msg); + return; } DOM.setCurrentFile(current); - CloudCmd.execFromModule(module, 'show'); }; - this.logOut = () => { - const url = CloudCmd.PREFIX + '/logout'; + this.logOut = async () => { + const url = CloudCmd.prefix + '/logout'; const error = () => document.location.reload(); + const {prefix} = CloudCmd; - DOM.Storage.clear(); - unregisterSW(); + await DOM.Storage.clear(); + unregisterSW(prefix); DOM.load.ajax({ url, error, }); }; - function initModules(callback) { - exec.if(CloudCmd.Key, () => { - Key = new CloudCmd.Key(); - CloudCmd.Key = Key; - Key.bind(); - }, (func) => { - /* привязываем клавиши к функциям */ - const path = 'key.js'; - - loadModule({ - path, - func - }); - }); + const initModules = async () => { + CloudCmd.Key = Key; + CloudCmd.Key.bind(); - Files.get('modules', (error, modules) => { - const showLoad = Images.show.load; - - const doBefore = { - 'edit': showLoad, - 'menu': showLoad, - }; - - const load = (name, path, dobefore) => { - const isTmpl = path === 'template'; - const funcName = isTmpl ? 'get' : 'show'; - - loadModule({ - name, - path, - dobefore, - funcName, - }); - }; - - if (!modules) - modules = []; - - modules.local.forEach((module) => { - load(null, module, doBefore[module]); + const [, modules] = await tryToCatch(Files.get, 'modules'); + const showLoad = Images.show.load; + + const doBefore = { + edit: showLoad, + menu: showLoad, + }; + + const load = (name, path, dobefore) => { + loadModule({ + name, + path, + dobefore, }); - - callback(); - }); + }; + + if (!modules) + return; + + for (const module of modules.local) { + load(null, module, doBefore[module]); + } + }; + + async function saveCurrentName(currentName) { + await Storage.set('current-name', currentName); } - function baseInit(callback) { + async function baseInit() { const files = DOM.getFiles(); CloudCmd.on('current-file', DOM.updateCurrentInfo); + CloudCmd.on('current-name', saveCurrentName); + + const name = await Storage.get('current-name'); + const currentFile = name && DOM.getCurrentByName(name) || files[0]; /* выделяем строку с первым файлом */ if (files) - DOM.setCurrentFile(files[0], { + DOM.setCurrentFile(currentFile, { // when hash is present // it should be handled with this.route // overwre otherwise - history: !location.hash + history: !location.hash, }); const dirPath = DOM.getCurrentDirPath(); - Listeners = CloudCmd.Listeners; + Listeners.init(); const panels = getPanels(); @@ -349,15 +231,12 @@ function CloudCmdProto(Util, DOM) { Listeners.initKeysPanel(); if (!CloudCmd.config('dirStorage')) - return callback(); + return; - Storage.get(dirPath, (error, data) => { - if (!data) { - data = getJSONfromFileTable(); - Storage.set(dirPath, data); - } - callback(); - }); + const data = await Storage.get(dirPath); + + if (!data) + await Storage.setJson(dirPath, getJsonFromFileTable()); } function getPanels() { @@ -372,40 +251,29 @@ function CloudCmdProto(Util, DOM) { ]; } - this.execFromModule = (moduleName, funcName, ...args) => { - const obj = CloudCmd[moduleName]; - const isObj = itype.object(obj); + this.execFromModule = async (moduleName, funcName, ...args) => { + await CloudCmd[moduleName](); - exec.if(isObj, () => { - const obj = CloudCmd[moduleName]; - const func = obj[funcName]; - - func(...args); - }, obj); + const func = CloudCmd[moduleName][funcName]; + func(...args); }; - this.refresh = (options = {}, callback) => { - if (!callback && typeof options === 'function') { - callback = options; - options = {}; - } - - const panel = options.panel || Info.panel; + this.refresh = async (options = {}) => { + const {panel = Info.panel, currentName} = options; + const path = DOM.getCurrentDirPath(panel); const isRefresh = true; const history = false; - const noCurrent = options ? options.noCurrent : false; - const currentName = options.currentName; + const noCurrent = options?.noCurrent; - CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { isRefresh, history, panel, noCurrent, currentName, - }, callback); + }); }; /** @@ -415,176 +283,132 @@ function CloudCmdProto(Util, DOM) { * @param options * { refresh, history } - необходимость обновить данные о каталоге * @param panel - * @param callback * */ - function ajaxLoad(path, options, panel, callback) { - const create = (error, json) => { - const RESTful = DOM.RESTful; - const name = options.currentName || Info.name; - const obj = jonny.parse(json); - const isRefresh = options.refresh; - const noCurrent = options.noCurrent; - - if (!isRefresh && json) - return createFileTable(obj, panel, options, callback); - - const position = DOM.getPanelPosition(panel); - const sort = CloudCmd.sort[position]; - const order = CloudCmd.order[position]; - - const query = rendy('?sort={{ sort }}&order={{ order }}', { - sort, - order, - }); - - RESTful.read(path + query, 'json', (error, obj) => { - if (error) - return; - - options.sort = sort; - options.order = order; - - createFileTable(obj, panel, options, () => { - if (isRefresh && !noCurrent) - DOM.setCurrentByName(name); - - exec(callback); - }); - - if (!CloudCmd.config('dirStorage')) - return; - - Storage.set(path, obj); - }); - }; + async function ajaxLoad(path, options = {}, panel) { + const {RESTful} = DOM; - if (!options) - options = {}; + CloudCmd.log(`reading dir: "${path}";`); - CloudCmd.log('reading dir: "' + path + '";'); + const dirStorage = CloudCmd.config('dirStorage'); + const json = dirStorage && await Storage.getJson(path); + + const name = options.currentName || Info.name; + const {noCurrent, refresh} = options; + + if (!refresh && json) + return await createFileTable(json, panel, options); + + const position = DOM.getPanelPosition(panel); + const sort = CloudCmd.sort[position]; + const order = CloudCmd.order[position]; + + const query = rendy('?sort={{ sort }}&order={{ order }}', { + sort, + order, + }); + + const [, newObj] = await RESTful.read(path + query, 'json'); + + if (!newObj) + // that's OK, error handled by RESTful + return; + + options.sort = sort; + options.order = order; + + await createFileTable(newObj, panel, options); + + if (refresh && !noCurrent) + DOM.setCurrentByName(name); if (!CloudCmd.config('dirStorage')) - return create(); + return; - Storage.get(path, create); + Storage.setJson(path, newObj); } /** * Функция строит файловую таблицу - * @param json - данные о файлах + * @param data - данные о файлах * @param panelParam - * @param history - * @param callback + * @param options - history, noCurrent, showDotFiles */ - function createFileTable(json, panelParam, options, callback) { + async function createFileTable(data, panelParam, options) { const { history, noCurrent, + showDotFiles, } = options; - const names = ['file', 'path', 'link', 'pathLink']; + const names = [ + 'file', + 'path', + 'link', + 'pathLink', + ]; - Files.get(names, (error, templFile, templPath, templLink, templPathLink) => { - const Dialog = DOM.Dialog; - const panel = panelParam || DOM.getPanel(); + const [error, [ + file, + path, + link, + pathLink, + ]] = await tryToCatch(Files.get, names); + + if (error) + return DOM.Dialog.alert(error.responseText); + + const panel = panelParam || DOM.getPanel(); + const {prefix} = CloudCmd; + + const {dir, name} = Info; + + const {childNodes} = panel; + let i = childNodes.length; + + while (i--) + panel.removeChild(panel.lastChild); + + panel.innerHTML = buildFromJSON({ + sort: options.sort, + order: options.order, + data, + id: panel.id, + prefix, + showDotFiles, + template: { + file, + path, + pathLink, + link, + }, + }); + + Listeners.setOnPanel(panel); + + if (!noCurrent) { + let current; - const { - dir, - name, - } = Info; + if (name === '..' && dir !== '/') + current = DOM.getCurrentByName(dir); - if (error) - return Dialog.alert(TITLE, error.responseText); + if (!current) + [current] = DOM.getFiles(panel); - const childNodes = panel.childNodes; - let i = childNodes.length; - - while (i--) - panel.removeChild(panel.lastChild); - - panel.innerHTML = buildFromJSON({ - sort : options.sort, - order : options.order, - data : json, - id : panel.id, - prefix : CloudCmd.PREFIX, - template : { - file : templFile, - path : templPath, - pathLink : templPathLink, - link : templLink - } + DOM.setCurrentFile(current, { + history, }); - Listeners.setOnPanel(panel); - - if (!noCurrent) { - let current; - - if (name === '..' && dir !== '/') - current = DOM.getCurrentByName(dir); - - if (!current) - current = DOM.getFiles(panel)[0]; - - DOM.setCurrentFile(current, { - history, - }); - - CloudCmd.emit('active-dir', Info.dirPath); - } - - exec(callback); - }); + CloudCmd.emit('active-dir', Info.dirPath); + } } - /** - * Функция генерирует JSON из html-таблицы файлов и - * используеться при первом заходе в корень - */ - function getJSONfromFileTable() { - const path = DOM.getCurrentDirPath(); - const infoFiles = Info.files || []; - - const notParent = (current) => { - const name = DOM.getCurrentName(current); - return name !== '..'; - }; - - const parse = (current) => { - const name = DOM.getCurrentName(current); - const size = DOM.getCurrentSize(current); - const owner = DOM.getCurrentOwner(current); - const mode = DOM.getCurrentMode(current); - const date = DOM.getCurrentDate(current); - - return { - name, - size, - mode, - owner, - date, - }; - }; - - const files = infoFiles - .filter(notParent) - .map(parse); - - const fileTable = { - path, - files, - }; - - return fileTable; - } - - this.goToParentDir = () => { - const dir = Info.dir; + this.goToParentDir = async () => { const { + dir, dirPath, parentDirPath, + panel, } = Info; if (dirPath === parentDirPath) @@ -592,15 +416,13 @@ function CloudCmdProto(Util, DOM) { const path = parentDirPath; - CloudCmd.loadDir({path}, () => { - const {panel} = Info; - const current = DOM.getCurrentByName(dir); - const first = DOM.getFiles(panel)[0]; - - DOM.setCurrentFile(current || first, { - history - }); + await CloudCmd.changeDir(path); + + const current = DOM.getCurrentByName(dir); + const [first] = DOM.getFiles(panel); + + DOM.setCurrentFile(current || first, { + history, }); }; } - diff --git a/client/cloudcmd.js b/client/cloudcmd.js index c88db38e..f2416fd4 100644 --- a/client/cloudcmd.js +++ b/client/cloudcmd.js @@ -1,45 +1,74 @@ -'use strict'; +import '../css/main.css'; +import process from 'node:process'; +import wraptile from 'wraptile'; +import load from 'load.js'; +import * as Util from '#common/util'; +import * as CloudFunc from '#common/cloudfunc'; +import DOM from '#dom'; +import {registerSW, listenSW} from './sw/register.js'; +import {initSortPanel, sortPanel} from './sort.js'; +import {createCloudCmd} from './client.js'; +import * as Listeners from './listeners/index.js'; -require('../css/main.css'); -require('../css/nojs.css'); -require('../css/columns/name-size-date.css'); -require('../css/columns/name-size.css'); +const isDev = process.env.NODE_ENV === 'development'; -const { - registerSW, -} = require('./sw/register'); +export default init; -// prevent additional loading of emitify -window.Emitify = require('emitify/legacy'); +globalThis.CloudCmd = init; -module.exports = window.CloudCmd = (config) => { - window.Promise = window.Promise || require('es6-promise'); - window.Util = require('../common/util'); - window.CloudFunc = require('../common/cloudfunc'); +async function init(config) { + globalThis.CloudCmd = createCloudCmd({ + DOM, + Listeners, + }); + globalThis.DOM = DOM; + globalThis.Util = Util; + globalThis.CloudFunc = CloudFunc; - const DOM = require('./dom'); - - window.DOM = DOM; - window.CloudCmd = require('./client'); - - registerSW(config.prefix); + await register(config); + initSortPanel(); + globalThis.CloudCmd.sortPanel = sortPanel; const prefix = getPrefix(config.prefix); - require('./listeners'); - require('./key'); - require('./sort'); + globalThis.CloudCmd.init(prefix, config); - window.CloudCmd.init(prefix, config); -}; + if (globalThis.CloudCmd.config('menu') === 'aleman') + setTimeout(() => { + import('https://esm.sh/@putout/processor-html'); + import('https://esm.sh/@putout/bundle@5.5'); + }, 100); +} function getPrefix(prefix) { if (!prefix) return ''; - + if (!prefix.indexOf('/')) return prefix; - return '/' + prefix; + return `/${prefix}`; } +const onUpdateFound = wraptile(async (config) => { + if (isDev) + return; + + const {DOM} = globalThis; + const prefix = getPrefix(config.prefix); + + await load.js(`${prefix}/dist/cloudcmd.common.js`); + await load.js(`${prefix}/dist/cloudcmd.js`); + + console.log('cloudcmd: sw: updated'); + + DOM.Events.removeAll(); + globalThis.CloudCmd(config); +}); + +async function register(config) { + const {prefix} = config; + const sw = await registerSW(prefix); + + listenSW(sw, 'updatefound', onUpdateFound(config)); +} diff --git a/client/dom/buffer.js b/client/dom/buffer.js index 39ce58ae..c6d20ccd 100644 --- a/client/dom/buffer.js +++ b/client/dom/buffer.js @@ -1,133 +1,124 @@ -'use strict'; +/* global CloudCmd*/ +import * as Storage from '#dom/storage'; +import tryToPromiseAll from '../../common/try-to-promise-all.js'; -/* global CloudCmd */ +const CLASS = 'cut-file'; +const COPY = 'copy'; +const CUT = 'cut'; -const jonny = require('jonny/legacy'); -const exec = require('execon'); - -const Storage = require('./storage'); -const DOM = require('./'); - -module.exports = new BufferProto(); - -function BufferProto() { - const Info = DOM.CurrentInfo; - const CLASS = 'cut-file'; - const COPY = 'copy'; - const CUT = 'cut'; - const TITLE = 'Buffer'; - - const Buffer = { - cut : callIfEnabled.bind(null, cut), - copy : callIfEnabled.bind(null, copy), - clear : callIfEnabled.bind(null, clear), - paste : callIfEnabled.bind(null, paste) - }; - - function showMessage(msg) { - DOM.Dialog.alert(TITLE, msg); - } - - function getNames() { - const files = DOM.getActiveFiles(); - const names = DOM.getFilenames(files); - - return names; - } - - function addCutClass() { - const files = DOM.getActiveFiles(); - - files.forEach((element) => { - element.classList.add(CLASS); - }); - } - - function rmCutClass() { - const files = DOM.getByClassAll(CLASS); - - [...files].forEach((element) => { - element.classList.remove(CLASS); - }); - } - - function callIfEnabled(callback) { - const is = CloudCmd.config('buffer'); - - if (is) - return callback(); - - showMessage('Buffer disabled in config!'); - } - - function copy() { - const names = getNames(); - const from = Info.dirPath; - - clear(); - - if (!names.length) - return; - - Storage.remove(CUT) - .set(COPY, { - from, - names, - }); - } - - function cut() { - const names = getNames(); - const from = Info.dirPath; - - clear(); - - if (!names.length) - return; - - addCutClass(); - - Storage - .set(CUT, { - from, - names, - }); - } - - function clear() { - Storage.remove(COPY) - .remove(CUT); - - rmCutClass(); - } - - function paste() { - const copy = Storage.get.bind(Storage, COPY); - const cut = Storage.get.bind(Storage, CUT); - - exec.parallel([copy, cut], (error, cp, ct) => { - const opStr = cp ? 'copy' : 'move'; - const opData = cp || ct; - const Operation = CloudCmd.Operation; - const msg = 'Path is same!'; - const path = Info.dirPath; - - if (!error && !cp && !ct) - error = 'Buffer is empty!'; - - if (error) - return showMessage(error); - - const data = jonny.parse(opData); - data.to = path; - - if (data.from === path) - return showMessage(msg); - - Operation.show(opStr, data); - clear(); - }); - } - - return Buffer; +function showMessage(msg) { + globalThis.DOM.Dialog.alert(msg); } + +function getNames() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); + + return DOM.getFilenames(files); +} + +function addCutClass() { + const {DOM} = globalThis; + const files = DOM.getActiveFiles(); + + for (const element of files) { + element.classList.add(CLASS); + } +} + +function rmCutClass() { + const {DOM} = globalThis; + const files = DOM.getByClassAll(CLASS); + + for (const element of files) { + element.classList.remove(CLASS); + } +} + +const checkEnabled = (fn) => () => { + const is = CloudCmd.config('buffer'); + + if (is) + return fn(); + + showMessage('Buffer disabled in config!'); +}; + +async function readBuffer() { + const [e, cp, ct] = await tryToPromiseAll([ + Storage.getJson(COPY), + Storage.getJson(CUT), + ]); + + return [ + e, + cp, + ct, + ]; +} + +export const copy = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + await Storage.remove(CUT); + await Storage.setJson(COPY, { + from, + names, + }); +}); + +export const cut = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const names = getNames(); + const from = Info.dirPath; + + await clear(); + + if (!names.length) + return; + + addCutClass(); + + await Storage.setJson(CUT, { + from, + names, + }); +}); + +export const clear = checkEnabled(async () => { + await Storage.remove(COPY); + await Storage.remove(CUT); + + rmCutClass(); +}); + +export const paste = checkEnabled(async () => { + const Info = globalThis.DOM.CurrentInfo; + const [error, cp, ct] = await readBuffer(); + + if (error || !cp && !ct) + return showMessage(error || 'Buffer is empty!'); + + const opStr = cp ? 'copy' : 'move'; + const data = cp || ct; + const {Operation} = CloudCmd; + const msg = 'Path is same!'; + const to = Info.dirPath; + + if (data.from === to) + return showMessage(msg); + + Operation.show(opStr, { + ...data, + to, + }); + + await clear(); +}); diff --git a/client/dom/cmd.js b/client/dom/cmd.js new file mode 100644 index 00000000..d228b315 --- /dev/null +++ b/client/dom/cmd.js @@ -0,0 +1,83 @@ +/* global DOM */ +const SELECTED_FILE = 'selected-file'; +const Cmd = { + getSelectedFiles, + isSelected, + unselectFile, + selectFile, + selectAllFiles, + toggleSelectedFile, + toggleAllSelectedFiles, +}; + +/** + * selected file check + * + * @param currentFile + */ +export function isSelected(currentFile) { + if (!currentFile) + return false; + + return DOM.isContainClass(currentFile, SELECTED_FILE); +} + +/** + * select current file + * @param currentFile + */ +export function selectFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.add(SELECTED_FILE); + + return Cmd; +} + +export function unselectFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + current.classList.remove(SELECTED_FILE); + + return Cmd; +} + +export function toggleSelectedFile(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const name = DOM.getCurrentName(current); + + if (name === '..') + return Cmd; + + current.classList.toggle(SELECTED_FILE); + + return Cmd; +} + +export function toggleAllSelectedFiles() { + DOM + .getAllFiles() + .map(DOM.toggleSelectedFile); + + return Cmd; +} + +export function selectAllFiles() { + DOM + .getAllFiles() + .map(DOM.selectFile); + + return Cmd; +} + +/** + * unified way to get selected files + * + * @currentFile + */ +export function getSelectedFiles() { + const panel = DOM.getPanel(); + const selected = DOM.getByClassAll(SELECTED_FILE, panel); + + return Array.from(selected); +} diff --git a/client/dom/current-file.js b/client/dom/current-file.js index ed2aa790..b8113054 100644 --- a/client/dom/current-file.js +++ b/client/dom/current-file.js @@ -1,25 +1,16 @@ -'use strict'; - /* global DOM */ /* global CloudCmd */ - -const btoa = require('../../common/btoa'); - -const { - encode, - decode, -} = require('../../common/entity'); - -const { - getTitle, - FS, -} = require('../../common/cloudfunc'); +import createElement from '@cloudcmd/create-element'; +import {getTitle, FS} from '#common/cloudfunc'; +import {encode, decode} from '#common/entity'; let Title; const CURRENT_FILE = 'current-file'; -const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); -const SPACE = ' '; +const encodeNBSP = (a) => a?.replace('\xa0', ' '); +const decodeNBSP = (a) => a?.replace(' ', '\xa0'); + +export const _CURRENT_FILE = CURRENT_FILE; /** * set name from current (or param) file @@ -27,18 +18,18 @@ const SPACE = ' '; * @param name * @param current */ -module.exports.setCurrentName = (name, current) => { +export const setCurrentName = (name, current) => { const Info = DOM.CurrentInfo; const {link} = Info; - const {PREFIX} = CloudCmd; - const dir = PREFIX + FS + Info.dirPath; + const {prefix} = CloudCmd; + const dir = prefix + FS + Info.dirPath; const encoded = encode(name); link.title = encoded; link.href = dir + encoded; link.innerHTML = encoded; - current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name))); + current.setAttribute('data-name', createNameAttribute(name)); CloudCmd.emit('current-file', current); return link; @@ -49,28 +40,48 @@ module.exports.setCurrentName = (name, current) => { * * @param currentFile */ -module.exports.getCurrentName = (currentFile) => { +export const getCurrentName = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); if (!current) return ''; - - const link = DOM.getCurrentLink(current); - if (!link) - return ''; - - return decode(link.title) - .replace(NBSP_REG, SPACE); + return parseNameAttribute(current.getAttribute('data-name')); }; /** - * get current direcotory path + * Generate a `data-name` attribute for the given filename + * @param name The string name to encode */ -module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { +const createNameAttribute = (name) => { + const encoded = btoa(encodeURI(name)); + return `js-file-${encoded}`; +}; + +/** + * Parse a `data-name` attribute string back into the original filename + * @param attribute The string we wish to decode + */ +const parseNameAttribute = (attribute) => { + attribute = attribute.replace('js-file-', ''); + return decodeNBSP(decodeURI(atob(attribute))); +}; + +export const _parseNameAttribute = parseNameAttribute; + +const parseHrefAttribute = (prefix, attribute) => { + attribute = attribute.replace(RegExp('^' + prefix + FS), ''); + return decode(decodeNBSP(attribute)); +}; + +export const _parseHrefAttribute = parseHrefAttribute; + +/** + * get current directory path + */ +export const getCurrentDirPath = (panel = DOM.getPanel()) => { const path = DOM.getByDataName('js-path', panel); - return path.textContent - .replace(NBSP_REG, SPACE); + return path.textContent; }; /** @@ -78,36 +89,31 @@ module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => { * * @param currentFile - current file by default */ -module.exports.getCurrentPath = (currentFile) => { +export const getCurrentPath = (currentFile) => { const current = currentFile || DOM.getCurrentFile(); - const element = DOM.getByTag('a', current)[0]; - const prefix = CloudCmd.PREFIX; + const [element] = DOM.getByTag('a', current); + const {prefix} = CloudCmd; - const path = element - .getAttribute('href') - .replace(RegExp('^' + prefix + FS), '') - .replace(NBSP_REG, SPACE); - - return decode(path); + return parseHrefAttribute(prefix, element.getAttribute('href')); }; /** - * get current direcotory name + * get current directory name */ -module.exports.getCurrentDirName = () => { - const href = DOM.getCurrentDirPath() +export const getCurrentDirName = () => { + const href = DOM + .getCurrentDirPath() .replace(/\/$/, ''); - const substr = href.substr(href, href.lastIndexOf('/')); - const ret = href.replace(substr + '/', '') || '/'; + const substr = href.substr(href, href.lastIndexOf('/')); - return ret; + return href.replace(`${substr}/`, '') || '/'; }; /** - * get current direcotory path + * get current directory path */ -module.exports.getParentDirPath = (panel) => { +export const getParentDirPath = (panel) => { const path = DOM.getCurrentDirPath(panel); const dirName = DOM.getCurrentDirName() + '/'; const index = path.lastIndexOf(dirName); @@ -119,11 +125,11 @@ module.exports.getParentDirPath = (panel) => { }; /** - * get not current direcotory path + * get not current directory path */ -module.exports.getNotCurrentDirPath = () => { +export const getNotCurrentDirPath = () => { const panel = DOM.getPanel({ - active: false + active: false, }); return DOM.getCurrentDirPath(panel); @@ -134,20 +140,20 @@ module.exports.getNotCurrentDirPath = () => { * * @currentFile */ -module.exports.getCurrentFile = () => { +export const getCurrentFile = () => { return DOM.getByClass(CURRENT_FILE); }; /** * get current file by name */ -module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { - const dataName = 'js-file-' + btoa(encodeURI(name)); +export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { + const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); return DOM.getByDataName(dataName, panel); }; /** - * private function thet unset currentfile + * private function that unset currentFile * * @currentFile */ @@ -163,9 +169,8 @@ function unsetCurrentFile(currentFile) { /** * unified way to set current file */ -module.exports.setCurrentFile = (currentFile, options) => { +export const setCurrentFile = (currentFile, options) => { const o = options; - const CENTER = true; const currentFileWas = DOM.getCurrentFile(); if (!currentFile) @@ -193,13 +198,15 @@ module.exports.setCurrentFile = (currentFile, options) => { * but it should be false * to prevent default behavior */ - if (!o || o.history !== false) { + if (!o || o.history) { const historyPath = path === '/' ? path : FS + path; DOM.setHistory(historyPath, null, historyPath); } } /* scrolling to current file */ + const CENTER = true; + DOM.scrollIntoViewIfNeeded(currentFile, CENTER); CloudCmd.emit('current-file', currentFile); @@ -209,13 +216,18 @@ module.exports.setCurrentFile = (currentFile, options) => { return DOM; }; +export const setCurrentByName = (name) => { + const current = DOM.getCurrentByName(name); + return DOM.setCurrentFile(current); +}; + /* * set current file by position * * @param layer - element * @param - position {x, y} */ -module.exports.getCurrentByPosition = ({x, y}) => { +export const getCurrentByPosition = ({x, y}) => { const element = document.elementFromPoint(x, y); const getEl = (el) => { @@ -247,7 +259,7 @@ module.exports.getCurrentByPosition = ({x, y}) => { * * @param currentFile */ -module.exports.isCurrentFile = (currentFile) => { +export const isCurrentFile = (currentFile) => { if (!currentFile) return false; @@ -259,18 +271,41 @@ module.exports.isCurrentFile = (currentFile) => { * * @param name */ - -module.exports.setTitle = (name) => { +export const setTitle = (name) => { if (!Title) - Title = DOM.getByTag('title')[0] || - DOM.load({ - name : 'title', - innerHTML : name, - parentElement : document.head - }); + Title = DOM.getByTag('title')[0] || createElement('title', { + innerHTML: name, + parent: document.head, + }); Title.textContent = name; return DOM; }; +/** + * check is current file is a directory + * + * @param currentFile + */ +export const isCurrentIsDir = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const path = DOM.getCurrentPath(current); + const fileType = DOM.getCurrentType(current); + + const isZip = path.endsWith('.zip'); + const isDir = /^directory(-link)?/.test(fileType); + + return isDir || isZip; +}; + +export const getCurrentType = (currentFile) => { + const current = currentFile || DOM.getCurrentFile(); + const el = DOM.getByDataName('js-type', current); + + const type = el.className + .split(' ') + .pop(); + + return type; +}; diff --git a/client/dom/current-file.spec.js b/client/dom/current-file.spec.js index 0b25c589..85b18a29 100644 --- a/client/dom/current-file.spec.js +++ b/client/dom/current-file.spec.js @@ -1,128 +1,114 @@ -'use strict'; +import {test, stub} from 'supertape'; +import {create} from 'auto-globals'; +import wraptile from 'wraptile'; +import * as currentFile from './current-file.js'; -const test = require('tape'); -const diff = require('sinon-called-with-diff'); -const sinon = diff(require('sinon')); const id = (a) => a; -const wraptile = require('wraptile'); -const returns = wraptile(id); -const currentFile = require('./current-file'); +const returns = wraptile(id); +const {_CURRENT_FILE} = currentFile; test('current-file: setCurrentName: setAttribute', (t) => { - const { - DOM, - CloudCmd, - } = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); - const setAttribute = sinon.stub(); - const current = { - setAttribute - }; + const current = create(); + const {setAttribute} = current; currentFile.setCurrentName('hello', current); - t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute'); - - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + t.calledWith(setAttribute, [ + 'data-name', + 'js-file-aGVsbG8=', + ], 'should call setAttribute'); t.end(); }); test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { - const { - DOM, - CloudCmd, - } = global; + const {DOM, CloudCmd} = globalThis; - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd(); + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); - const setAttribute = sinon.stub(); - const current = { - setAttribute - }; + const current = create(); + const {setAttribute} = current; currentFile.setCurrentName('ай', current); - t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute'); + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; - global.DOM = DOM; - global.CloudCmd = CloudCmd; + t.calledWith(setAttribute, [ + 'data-name', + 'js-file-JUQwJUIwJUQwJUI5', + ], 'should call setAttribute'); + t.end(); +}); + +test('current-file: getCurrentName', (t) => { + const current = create(); + current.getAttribute.returns('js-file-Ymlu'); + const result = currentFile.getCurrentName(current); + + t.equal(result, 'bin'); t.end(); }); test('current-file: emit', (t) => { - const { - DOM, - CloudCmd, - } = global; + const {DOM, CloudCmd} = globalThis; - const emit = sinon.stub(); - const setAttribute = sinon.stub(); + const emit = stub(); - global.DOM = getDOM(); - global.CloudCmd = getCloudCmd({ + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd({ emit, }); - const current = { - setAttribute, - }; + const current = create(); currentFile.setCurrentName('hello', current); - t.ok(emit.calledWith('current-file', current), 'should call emit'); - - global.DOM = DOM; - global.CloudCmd = CloudCmd; + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + t.calledWith(emit, ['current-file', current], 'should call emit'); t.end(); }); test('current-file: setCurrentName: return', (t) => { - const { - DOM, - CloudCmd, - } = global; + const {DOM, CloudCmd} = globalThis; - const setAttribute = sinon.stub(); const link = {}; - global.DOM = getDOM({ - link + globalThis.DOM = getDOM({ + link, }); - global.CloudCmd = getCloudCmd(); - - const current = { - setAttribute, - }; + globalThis.CloudCmd = getCloudCmd(); + const current = create(); const result = currentFile.setCurrentName('hello', current); + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + t.equal(result, link, 'should return link'); - - global.DOM = DOM; - global.CloudCmd = CloudCmd; - t.end(); }); test('current-file: getParentDirPath: result', (t) => { - const { - DOM, - } = global; + const {DOM} = globalThis; const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirName = returns('+++favorite films'); - global.DOM = getDOM({ + globalThis.DOM = getDOM({ getCurrentDirPath, getCurrentDirName, }); @@ -130,33 +116,193 @@ test('current-file: getParentDirPath: result', (t) => { const result = currentFile.getParentDirPath(); const expected = '/D/Films/'; - global.DOM = DOM; + globalThis.DOM = DOM; t.equal(result, expected, 'should return parent dir path'); t.end(); }); -function getCloudCmd({emit} = {}) { - return { - PREFIX: '', - emit: emit || sinon.stub(), - }; -} +test('current-file: isCurrentFile: no', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); + + const result = currentFile.isCurrentFile(); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.notOk(result); + t.end(); +}); -function getDOM({ - link = {}, - getCurrentDirPath = sinon.stub(), - getCurrentDirName = sinon.stub(), - getByDataName = sinon.stub(), -} = {}) { +test('current-file: isCurrentFile', (t) => { + const {DOM, CloudCmd} = globalThis; + + const isContainClass = stub(); + + globalThis.DOM = getDOM({ + isContainClass, + }); + + globalThis.CloudCmd = getCloudCmd(); + + const current = {}; + currentFile.isCurrentFile(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass'); + t.end(); +}); + +test('current-file: getCurrentType', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); + + const {getByDataName} = globalThis.DOM; + + getByDataName.returns({ + className: 'mini-icon directory', + }); + + const current = create(); + + currentFile.getCurrentType(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.calledWith(getByDataName, ['js-type', current]); + t.end(); +}); + +test('current-file: isCurrentIsDir: getCurrentType', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); + + const {getCurrentType} = globalThis.DOM; + + const current = create(); + + currentFile.isCurrentIsDir(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.calledWith(getCurrentType, [current]); + t.end(); +}); + +test('current-file: isCurrentIsDir: directory', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM({ + getCurrentType: stub().returns('directory'), + }); + + globalThis.CloudCmd = getCloudCmd(); + + const current = create(); + + const result = currentFile.isCurrentIsDir(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.ok(result); + t.end(); +}); + +test('current-file: isCurrentIsDir: directory-link', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM({ + getCurrentType: stub().returns('directory-link'), + }); + + globalThis.CloudCmd = getCloudCmd(); + + const current = create(); + + const result = currentFile.isCurrentIsDir(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.ok(result); + t.end(); +}); + +test('current-file: isCurrentIsDir: file', (t) => { + const {DOM, CloudCmd} = globalThis; + + globalThis.DOM = getDOM({ + getCurrentType: stub().returns('file'), + }); + + globalThis.CloudCmd = getCloudCmd(); + + const current = create(); + + const result = currentFile.isCurrentIsDir(current); + + globalThis.DOM = DOM; + globalThis.CloudCmd = CloudCmd; + + t.notOk(result); + t.end(); +}); + +const getCloudCmd = ({emit} = {}) => ({ + prefix: '', + emit: emit || stub(), +}); + +test('current-file: parseNameAttribute', (t) => { + const result = currentFile._parseNameAttribute('js-file-aGVsbG8mbmJzcDt3b3JsZA=='); + const expected = 'hello\xa0world'; + + t.equal(result, expected); + t.end(); +}); + +test('current-file: parseHrefAttribute', (t) => { + const prefix = '/api/v1'; + const result = currentFile._parseHrefAttribute(prefix, '/api/v1/fs/hello world'); + const expected = '/hello\xa0world'; + + t.equal(result, expected); + t.end(); +}); + +function getDOM(overrides = {}) { + const { + link = {}, + getCurrentDirPath = stub(), + getCurrentDirName = stub(), + getByDataName = stub(), + isContainClass = stub(), + getCurrentType = stub(), + getCurrentPath = stub().returns(''), + } = overrides; + return { getCurrentDirPath, getCurrentDirName, + getCurrentPath, getByDataName, + isContainClass, + getCurrentType, CurrentInfo: { link, dirPath: '/', - } + }, }; } - diff --git a/client/dom/dialog.js b/client/dom/dialog.js index 97a65db2..b1791d92 100644 --- a/client/dom/dialog.js +++ b/client/dom/dialog.js @@ -1,19 +1,18 @@ +import {tryToCatch} from 'try-to-catch'; +import * as smalltalk from 'smalltalk'; -'use strict'; +const title = 'Cloud Commander'; -const { - alert, - prompt, - confirm, -}= require('smalltalk'); +export const alert = (...a) => smalltalk.alert(title, ...a, { + cancel: false, +}); -module.exports = { - alert, - prompt, - confirm, +export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a); +export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a); +export const progress = (...a) => smalltalk.progress(title, ...a); + +alert.noFiles = () => { + return smalltalk.alert(title, 'No files selected!', { + cancel: false, + }); }; - -module.exports.alert.noFiles = (title) => { - return alert(title, 'No files selected!'); -}; - diff --git a/client/dom/directory.js b/client/dom/directory.js index 7a58025b..efbca119 100644 --- a/client/dom/directory.js +++ b/client/dom/directory.js @@ -1,33 +1,29 @@ -/* global CloudCmd */ +/* global DOM, CloudCmd */ +import philip from 'philip'; +import * as Dialog from '#dom/dialog'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; -'use strict'; - -const philip = require('philip'); - -const Images = require('./images'); -const {FS} = require('../../common/cloudfunc'); -const DOM = require('.'); - -const { - getCurrentDirPath: getPathWhenRootEmpty, -} = DOM; - -module.exports = (items) => { - const Dialog = DOM.Dialog; - +export const uploadDirectory = (items) => { if (items.length) Images.show('top'); - const entries = [...items].map((item) => { - return item.webkitGetAsEntry(); + const entries = Array + .from(items) + .map((item) => item.webkitGetAsEntry()); + + const dirPath = DOM.getCurrentDirPath(); + const path = dirPath.replace(/\/$/, ''); + + const progress = Dialog.progress('Uploading...'); + + progress.catch(() => { + Dialog.alert('Upload aborted'); + uploader.abort(); }); - const dirPath = getPathWhenRootEmpty(); - const path = dirPath - .replace(/\/$/, ''); - const uploader = philip(entries, (type, name, data, i, n, callback) => { - const prefixURL = CloudCmd.PREFIX_URL; + const {prefixURL} = CloudCmd; const full = prefixURL + FS + path + name; let upload; @@ -49,7 +45,7 @@ module.exports = (items) => { const max = next - current; const value = current + percent(count, 100, max); - setProgress(value); + progress.setProgress(value); }); }); @@ -58,25 +54,11 @@ module.exports = (items) => { uploader.abort(); }); - uploader.on('progress', setProgress); uploader.on('end', CloudCmd.refresh); }; -function percent(i, n, per = 100) { - return Math.round(i * per / n); -} +const percent = (i, n, per = 100) => Math.round(i * per / n); -function setProgress(count) { - DOM.Images - .setProgress(count) - .show('top'); -} - -function uploadFile(url, data) { - return DOM.load.put(url, data); -} - -function uploadDir(url) { - return DOM.load.put(url + '?dir'); -} +const uploadFile = (url, data) => DOM.load.put(url, data); +const uploadDir = (url) => DOM.load.put(`${url}?dir`); diff --git a/client/dom/dom-tree.js b/client/dom/dom-tree.js index 0c468116..fad5ad40 100644 --- a/client/dom/dom-tree.js +++ b/client/dom/dom-tree.js @@ -1,6 +1,15 @@ -'use strict'; +import currify from 'currify'; -const DOM = module.exports; +const DOM = { + show, + hide, + getByClass, + getByClassAll, + getByDataName, + getById, + getByTag, + isContainClass, +}; /** * check class of element @@ -8,71 +17,76 @@ const DOM = module.exports; * @param element * @param className */ -module.exports.isContainClass = (element, className) => { +export function isContainClass(element, className) { if (!element) throw Error('element could not be empty!'); if (!className) throw Error('className could not be empty!'); - const classList = element.classList; - const ret = classList.contains(className); + if (Array.isArray(className)) + return className.some(currify( + isContainClass, + element, + )); - return ret; -}; + const {classList} = element; + + return classList.contains(className); +} /** * Function search element by tag * @param tag - className * @param element - element */ -module.exports.getByTag = (tag, element = document) => { +export function getByTag(tag, element = document) { return element.getElementsByTagName(tag); -}; +} /** * Function search element by id - * @param Id - id + * @param id + * @param element */ -module.exports.getById = (id, element = document) => { - return element.querySelector('#' + id); -}; +export function getById(id, element = document) { + return element.querySelector(`#${id}`); +} /** * Function search first element by class name * @param className - className * @param element - element */ -module.exports.getByClass = (className, element = document) => { +export function getByClass(className, element = document) { return DOM.getByClassAll(className, element)[0]; -}; +} -module.exports.getByDataName = (attribute, element = document) => { - const selector = '[' + 'data-name="' + attribute + '"]'; +export function getByDataName(attribute, element = document) { + const selector = '[' + 'data-name="' + attribute + '"]'; return element.querySelector(selector); -}; +} /** * Function search element by class name - * @param pClass - className - * @param element - element + * @param className + * @param element */ -module.exports.getByClassAll = (className, element) => { +export function getByClassAll(className, element) { return (element || document).getElementsByClassName(className); -}; +} /** * add class=hidden to element * * @param element */ -module.exports.hide = (element) => { +export function hide(element) { element.classList.add('hidden'); return DOM; -}; +} -module.exports.show = (element) => { +export function show(element) { element.classList.remove('hidden'); return DOM; -}; - +} diff --git a/client/dom/dom-tree.spec.js b/client/dom/dom-tree.spec.js new file mode 100644 index 00000000..53b376c7 --- /dev/null +++ b/client/dom/dom-tree.spec.js @@ -0,0 +1,152 @@ +import {test, stub} from 'supertape'; +import {create} from 'auto-globals'; +import {tryCatch} from 'try-catch'; +import { + isContainClass, + getByTag, + getById, + getByClass, + getByDataName, + getByClassAll, + hide, + show, +} from './dom-tree.js'; + +test('dom: isContainClass: no element', (t) => { + const [e] = tryCatch(isContainClass); + + t.equal(e.message, 'element could not be empty!', 'should throw when no element'); + t.end(); +}); + +test('dom: isContainClass: no className', (t) => { + const [e] = tryCatch(isContainClass, {}); + + t.equal(e.message, 'className could not be empty!', 'should throw when no element'); + t.end(); +}); + +test('dom: isContainClass: contains', (t) => { + const el = create(); + const {contains} = el.classList; + + const className = 'hello'; + isContainClass(el, className); + + t.calledWith(contains, [className], 'should call contains'); + t.end(); +}); + +test('dom: isContainClass: contains: array', (t) => { + const el = create(); + const {contains} = el.classList; + + const className = 'hello'; + isContainClass(el, ['world', className, 'hello']); + + t.calledWith(contains, [className], 'should call contains'); + t.end(); +}); + +test('dom: getByTag', (t) => { + const getElementsByTagName = stub(); + const element = { + getElementsByTagName, + }; + + getByTag('div', element); + + t.calledWith(getElementsByTagName, ['div'], 'should call getElementsByTagName'); + t.end(); +}); + +test('dom: getById', (t) => { + const querySelector = stub(); + const element = { + querySelector, + }; + + getById('my-id', element); + + t.calledWith(querySelector, ['#my-id'], 'should call querySelector with id selector'); + t.end(); +}); + +test('dom: getByClassAll', (t) => { + const getElementsByClassName = stub(); + const element = { + getElementsByClassName, + }; + + getByClassAll('my-class', element); + + t.calledWith(getElementsByClassName, ['my-class'], 'should call getElementsByClassName'); + t.end(); +}); + +test('dom: getByClass: calls getByClassAll', (t) => { + const element = { + getElementsByClassName: stub().returns(['first']), + }; + + const result = getByClass('my-class', element); + + t.equal(result, 'first', 'should return first element from class list'); + t.end(); +}); + +test('dom: getByDataName', (t) => { + const querySelector = stub(); + const element = { + querySelector, + }; + + getByDataName('hello', element); + + t.calledWith(querySelector, ['[data-name="hello"]'], 'should call querySelector with data-name selector'); + t.end(); +}); + +test('dom: hide', (t) => { + const add = stub(); + const element = { + classList: { + add, + }, + }; + + hide(element); + + t.calledWith(add, ['hidden'], 'should add hidden class'); + t.end(); +}); + +test('dom: show', (t) => { + const remove = stub(); + const element = { + classList: { + remove, + }, + }; + + show(element); + + t.calledWith(remove, ['hidden'], 'should remove hidden class'); + t.end(); +}); + +test('dom: getByClassAll: without element uses document', (t) => { + const getElementsByClassName = stub(); + const prevDocument = globalThis.document; + + globalThis.document = { + getElementsByClassName, + }; + + getByClassAll('my-class'); + + globalThis.document = prevDocument; + + t.calledWith(getElementsByClassName, ['my-class'], 'should fallback to document when no element'); + t.end(); +}); diff --git a/client/dom/events.js b/client/dom/events.js deleted file mode 100644 index 706c0849..00000000 --- a/client/dom/events.js +++ /dev/null @@ -1,231 +0,0 @@ -'use strict'; - -const itype = require('itype/legacy'); - -module.exports = new EventsProto(); - -function EventsProto() { - const Events = this; - - const getEventOptions = (eventName) => { - if (eventName !== 'touchstart') - return false; - - return { - passive: true - }; - }; - - function parseArgs(eventName, element, listener, callback) { - let isFunc; - const args = [ - eventName, - element, - listener, - callback, - ]; - - const EVENT_NAME = 1; - const ELEMENT = 0; - const type = itype(eventName); - - switch(type) { - default: - if (!/element$/.test(type)) - throw Error('unknown eventName: ' + type); - - parseArgs( - args[EVENT_NAME], - args[ELEMENT], - listener, - callback - ); - break; - - case 'string': - isFunc = itype.function(element); - - if (isFunc) { - listener = element; - element = null; - } - - if (!element) - element = window; - - callback(element, [ - eventName, - listener, - getEventOptions(eventName), - ]); - break; - - case 'array': - eventName.forEach((eventName) => { - parseArgs( - eventName, - element, - listener, - callback - ); - }); - break; - - case 'object': - Object.keys(eventName).forEach((name) => { - const eventListener = eventName[name]; - - parseArgs( - name, - element, - eventListener, - callback - ); - }); - - break; - } - } - - /** - * safe add event listener - * - * @param type - * @param element {document by default} - * @param listener - */ - this.add = (type, element, listener) => { - checkType(type); - - parseArgs(type, element, listener, (element, args) => { - element.addEventListener.apply(element, args); - }); - - return Events; - }; - - /** - * safe add event listener - * - * @param type - * @param listener - * @param element {document by default} - */ - this.addOnce = (type, element, listener) => { - const once = (event) => { - Events.remove(type, element, once); - listener(event); - }; - - if (!listener) { - listener = element; - element = null; - } - - this.add(type, element, once); - - return Events; - }; - - /** - * safe remove event listener - * - * @param type - * @param listener - * @param element {document by default} - */ - this.remove = (type, element, listener) => { - checkType(type); - - parseArgs(type, element, listener, (element, args) => { - element.removeEventListener.apply(element, args); - }); - - return Events; - }; - - /** - * safe add event keydown listener - * - * @param listener - */ - this.addKey = function(...argsArr) { - const name = 'keydown'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; - - /** - * safe remove event click listener - * - * @param listener - */ - this.rmKey = function(...argsArr) { - const name = 'keydown'; - const args = [name, ...argsArr]; - - return Events.remove(...args); - }; - - /** - * safe add event click listener - * - * @param listener - */ - this.addClick = function(...argsArr) { - const name = 'click'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; - - /** - * safe remove event click listener - * - * @param listener - */ - this.rmClick = function(...argsArr) { - const name = 'click'; - const args = [name, ...argsArr]; - - return Events.remove(...args); - }; - - this.addContextMenu = function(...argsArr) { - const name = 'contextmenu'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; - - /** - * safe add event click listener - * - * @param listener - */ - this.addError = function(...argsArr) { - const name = 'error'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; - - /** - * safe add load click listener - * - * @param listener - */ - this.addLoad = function(...argsArr) { - const name = 'load'; - const args = [name, ...argsArr]; - - return Events.add(...args); - }; - - function checkType(type) { - if (!type) - throw Error('type could not be empty!'); - } -} - diff --git a/client/dom/events/event-store.js b/client/dom/events/event-store.js new file mode 100644 index 00000000..1cbfe7a4 --- /dev/null +++ b/client/dom/events/event-store.js @@ -0,0 +1,15 @@ +let list = []; + +export const add = (el, name, fn) => { + list.push([ + el, + name, + fn, + ]); +}; + +export const clear = () => { + list = []; +}; + +export const get = () => list; diff --git a/client/dom/events/event-store.spec.js b/client/dom/events/event-store.spec.js new file mode 100644 index 00000000..6a623939 --- /dev/null +++ b/client/dom/events/event-store.spec.js @@ -0,0 +1,37 @@ +import {test} from 'supertape'; +import * as eventStore from './event-store.js'; + +test('event-store: get', (t) => { + const el = {}; + const name = 'click'; + const fn = () => {}; + + eventStore.add(el, name, fn); + const result = eventStore.get(); + + const expected = [ + [ + el, + name, + fn, + ], + ]; + + t.deepEqual(result, expected); + t.end(); +}); + +test('event-store: clear', (t) => { + const el = {}; + const name = 'click'; + const fn = () => {}; + + eventStore.add(el, name, fn); + eventStore.clear(); + + const result = eventStore.get(); + const expected = []; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/client/dom/events/index.js b/client/dom/events/index.js new file mode 100644 index 00000000..c49d2333 --- /dev/null +++ b/client/dom/events/index.js @@ -0,0 +1,203 @@ +import itype from 'itype'; +import * as EventStore from './event-store.js'; + +/** + * safe add event listener + * + * @param type + * @param element - document by default + * @param listener + */ +export const add = (type, element, listener) => { + checkType(type); + + parseArgs(type, element, listener, (element, args) => { + const [name, fn, options] = args; + + element.addEventListener(name, fn, options); + EventStore.add(element, name, fn); + }); + + return Events; +}; + +/** + * safe add event listener + * + * @param type + * @param listener + * @param element - document by default + */ +export const addOnce = (type, element, listener) => { + const once = (event) => { + Events.remove(type, element, once); + listener(event); + }; + + if (!listener) { + listener = element; + element = null; + } + + add(type, element, once); + + return Events; +}; + +/** + * safe remove event listener + * + * @param type + * @param listener + * @param element - document by default + */ +export const remove = (type, element, listener) => { + checkType(type); + + parseArgs(type, element, listener, (element, args) => { + element.removeEventListener(...args); + }); + + return Events; +}; + +/** + * remove all added event listeners + */ +export const removeAll = () => { + const events = EventStore.get(); + + for (const [el, name, fn] of events) + el.removeEventListener(name, fn); + + EventStore.clear(); +}; + +/** + * safe add event keydown listener + * + * @param args + */ +export const addKey = function(...args) { + return add('keydown', ...args); +}; + +/** + * safe remove event click listener + * + * @param args + */ +export const rmKey = function(...args) { + return Events.remove('keydown', ...args); +}; + +/** + * safe add event click listener + */ +export const addClick = function(...args) { + return Events.add('click', ...args); +}; + +/** + * safe remove event click listener + */ +export const rmClick = function(...args) { + return remove('click', ...args); +}; + +export const addContextMenu = function(...args) { + return add('contextmenu', ...args); +}; + +/** + * safe add load listener + */ +export const addLoad = function(...args) { + return add('load', ...args); +}; + +function checkType(type) { + if (!type) + throw Error('type could not be empty!'); +} + +const getEventOptions = (eventName) => { + if (eventName !== 'touchstart') + return false; + + return { + passive: true, + }; +}; + +function parseArgs(eventName, element, listener, callback) { + let isFunc; + const args = [ + eventName, + element, + listener, + callback, + ]; + + const EVENT_NAME = 1; + const ELEMENT = 0; + const type = itype(eventName); + + switch(type) { + default: + if (!type.endsWith('element')) + throw Error(`unknown eventName: ${type}`); + + parseArgs(args[EVENT_NAME], args[ELEMENT], listener, callback); + break; + + case 'string': + isFunc = itype.function(element); + + if (isFunc) { + listener = element; + element = null; + } + + if (!element) + element = window; + + callback(element, [ + eventName, + listener, + getEventOptions(eventName), + ]); + break; + + case 'array': + + for (const name of eventName) { + parseArgs(name, element, listener, callback); + } + + break; + + case 'object': + + for (const name of Object.keys(eventName)) { + const eventListener = eventName[name]; + + parseArgs(name, element, eventListener, callback); + } + + break; + } +} + +const Events = { + add, + addClick, + addContextMenu, + addKey, + addLoad, + addOnce, + remove, + removeAll, + rmClick, + rmKey, +}; diff --git a/client/dom/files.js b/client/dom/files.js index 562738f0..395ac0fa 100644 --- a/client/dom/files.js +++ b/client/dom/files.js @@ -1,104 +1,82 @@ /* global CloudCmd */ - -'use strict'; - -const itype = require('itype/legacy'); -const currify = require('currify/legacy'); -const exec = require('execon'); - -const load = require('./load'); -const RESTful = require('./rest'); +import itype from 'itype'; +import {promisify} from 'es6-promisify'; +import * as load from '#dom/load'; +import * as RESTful from '#dom/rest'; const Promises = {}; const FILES_JSON = 'config|modules'; const FILES_HTML = 'file|path|link|pathLink|media'; const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload'; const DIR_HTML = '/tmpl/'; -const DIR_HTML_FS = DIR_HTML + 'fs/'; +const DIR_HTML_FS = `${DIR_HTML}fs/`; const DIR_JSON = '/json/'; const timeout = getTimeoutOnce(2000); -const get = currify(getFile); -const unaryMap = (array, fn) => array.map((a) => fn(a)); +export const get = getFile; -module.exports.get = get; - -function getFile(name, callback) { +function getFile(name) { const type = itype(name); - let array; + check(name); - check(name, callback); + if (type === 'string') + return getModule(name); - switch(type) { - case 'string': - getModule(name, callback); - break; - - case 'array': - array = unaryMap(name, get); - - exec.parallel(array, callback); - break; - } + if (type === 'array') + return Promise.all(name.map(getFile)); } -function check(name, callback) { +function check(name) { if (!name) throw Error('name could not be empty!'); - - if (typeof callback !== 'function') - throw Error('callback should be a function'); } -function getModule(name, callback) { - let path; - - const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); - const regExpJSON = new RegExp(FILES_JSON); +function getModule(name) { + const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); + const regExpJSON = RegExp(FILES_JSON); const isHTML = regExpHTML.test(name); const isJSON = regExpJSON.test(name); - if (!isHTML && !isJSON) { - showError(name); - } else if (name === 'config') { - getConfig(callback); - } else { - path = getPath(name, isHTML, isJSON); - - getSystemFile(path, callback); - } + if (!isHTML && !isJSON) + return showError(name); + if (name === 'config') + return getConfig(); + + const path = getPath(name, isHTML, isJSON); + + return getSystemFile(path); } function getPath(name, isHTML, isJSON) { let path; - const regExp = new RegExp(FILES_HTML_ROOT); + const regExp = RegExp(FILES_HTML_ROOT); const isRoot = regExp.test(name); if (isHTML) { if (isRoot) path = DIR_HTML + name.replace('-tmpl', ''); else - path = DIR_HTML_FS + name; + path = DIR_HTML_FS + name; path += '.hbs'; } else if (isJSON) { - path = DIR_JSON + name + '.json'; + path = DIR_JSON + name + '.json'; } return path; } function showError(name) { - const str = 'Wrong file name: ' + name; - const error = new Error(str); + const str = `Wrong file name: ${name}`; + const error = Error(str); - throw(error); + throw error; } -function getSystemFile(file, callback) { - const prefix = CloudCmd.PREFIX; +const getSystemFile = promisify((file, callback) => { + const {prefix} = CloudCmd; if (!Promises[file]) Promises[file] = new Promise((success, error) => { @@ -107,7 +85,7 @@ function getSystemFile(file, callback) { load.ajax({ url, success, - error + error, }); }); @@ -117,36 +95,29 @@ function getSystemFile(file, callback) { Promises[file] = null; callback(error); }); -} +}); -function getConfig(callback) { +const getConfig = async () => { let is; if (!Promises.config) - Promises.config = new Promise((resolve, reject) => { + Promises.config = () => { is = true; - RESTful.Config.read((error, data) => { - if (error) - return reject(error); - - resolve(data); - }); - }); + return RESTful.Config.read(); + }; - Promises.config.then((data) => { + const [, data] = await Promises.config(); + + if (data) is = false; - - callback(null, data); - - timeout(() => { - if (!is) - Promises.config = null; - }); - }, () => { + + timeout(() => { if (!is) Promises.config = null; }); -} + + return data; +}; function getTimeoutOnce(time) { let is; @@ -163,4 +134,3 @@ function getTimeoutOnce(time) { }, time); }; } - diff --git a/client/dom/images.js b/client/dom/images.js index 19eb32cf..75663e72 100644 --- a/client/dom/images.js +++ b/client/dom/images.js @@ -1,21 +1,13 @@ -/* global CloudCmd */ /* global DOM */ - -'use strict'; - -const Images = module.exports; +import createElement from '@cloudcmd/create-element'; const LOADING = 'loading'; const HIDDEN = 'hidden'; const ERROR = 'error'; -const LoadingImage = LOADING + getLoadingType(); +const getLoadingType = () => isSVG() ? '-svg' : '-gif'; -function getLoadingType() { - return isSVG() ? '-svg' : '-gif'; -} - -module.exports.get = getElement; +export const get = getElement; /** * check SVG SMIL animation support @@ -34,54 +26,60 @@ function isSVG() { return /SVGAnimate/.test(name); } - function getElement() { - return DOM.load({ - name : 'span', - id : 'js-status-image', - className : 'icon', - attribute : 'data-progress', - notAppend : true + return createElement('span', { + id: 'js-status-image', + className: 'icon', + dataName: 'progress', + notAppend: true, }); } /* Функция создаёт картинку загрузки */ -module.exports.loading = () => { +export const loading = () => { const element = getElement(); - const classList = element.classList; + const {classList} = element; + const loadingImage = LOADING + getLoadingType(); - classList.add(LOADING, LoadingImage); + classList.add(LOADING, loadingImage); classList.remove(ERROR, HIDDEN); return element; }; /* Функция создаёт картинку ошибки загрузки */ -module.exports.error = () => { +export const error = () => { const element = getElement(); - const classList = element.classList; + const {classList} = element; + const loadingImage = LOADING + getLoadingType(); classList.add(ERROR); - classList.remove(HIDDEN, LOADING, LoadingImage); + classList.remove(HIDDEN, LOADING, loadingImage); return element; }; -module.exports.show = show; -module.exports.show.load = show; -module.exports.show.error = error; +show.load = show; +show.error = (text) => { + const image = Images.error(); + + DOM.show(image); + image.title = text; + + return image; +}; /** * Function shows loading spinner * position = {top: true}; */ -function show(position, panel) { +export function show(position, panel) { const image = Images.loading(); const parent = image.parentElement; const refreshButton = DOM.getRefreshButton(panel); let current; - + if (position === 'top') { current = refreshButton.parentElement; } else { @@ -93,7 +91,7 @@ function show(position, panel) { current = refreshButton.parentElement; } - if (!parent || (parent && parent !== current)) + if (!parent || parent && parent !== current) current.appendChild(image); DOM.show(image); @@ -101,36 +99,25 @@ function show(position, panel) { return image; } -function error(text) { - const image = Images.error(); - - DOM.show(image); - image.title = text; - - CloudCmd.log(text); - - return image; -} - /** * hide load image */ -module.exports.hide = () => { +export function hide() { const element = Images.get(); DOM.hide(element); return Images; -}; +} -module.exports.setProgress = (value, title) => { +export const setProgress = (value, title) => { const DATA = 'data-progress'; const element = Images.get(); - + if (!element) return Images; - element.setAttribute(DATA, value + '%'); + element.setAttribute(DATA, `${value}%`); if (title) element.title = title; @@ -138,7 +125,7 @@ module.exports.setProgress = (value, title) => { return Images; }; -module.exports.clearProgress = () => { +export const clearProgress = () => { const DATA = 'data-progress'; const element = Images.get(); @@ -151,3 +138,12 @@ module.exports.clearProgress = () => { return Images; }; +const Images = { + clearProgress, + setProgress, + show, + hide, + get, + error, + loading, +}; diff --git a/client/dom/index.js b/client/dom/index.js index 22d82a3c..a82b6f72 100644 --- a/client/dom/index.js +++ b/client/dom/index.js @@ -1,984 +1,811 @@ /* global CloudCmd */ +import * as load from '#dom/load'; +import * as Files from '#dom/files'; +import * as Dialog from '#dom/dialog'; +import * as Events from '#dom/events'; +import {getExt} from '#common/util'; +import * as Storage from '#dom/storage'; +import * as RESTful from '#dom/rest'; +import * as Images from '#dom/images'; +import renameCurrent from './operations/rename-current.js'; +import * as CurrentFile from './current-file.js'; +import * as DOMTree from './dom-tree.js'; +import * as Cmd from './cmd.js'; +import * as IO from './io/index.js'; +import {uploadDirectory} from './directory.js'; +import * as Buffer from './buffer.js'; +import {loadRemote as _loadRemote} from './load-remote.js'; +import {selectByPattern} from './select-by-pattern.js'; -'use strict'; - -const itype = require('itype/legacy'); -const exec = require('execon'); -const jonny = require('jonny/legacy'); -const Util = require('../../common/util'); - -const Images = require('./images'); -const load = require('./load'); -const Files = require('./files'); -const RESTful = require('./rest'); -const Storage = require('./storage'); -const Dialog = require('./dialog'); - -const currentFile = require('./current-file'); -const DOMTree = require('./dom-tree'); +const {assign} = Object; const DOM = { - ...DOMTree, - ...currentFile, - ...new CmdProto(), + getCurrentDirName, + getNotCurrentDirPath, + getParentDirPath, + loadRemote, + loadSocket, + promptNewDir, + promptNewFile, + unselectFiles, + getActiveFiles, + getCurrentDate, + getCurrentSize, + loadCurrentSize, + loadCurrentHash, + setCurrentSize, + getCurrentMode, + getCurrentOwner, + getCurrentData, + getRefreshButton, + getAllFiles, + expandSelection, + shrinkSelection, + setHistory, + getCurrentLink, + getFilenames, + checkStorageHash, + saveDataToStorage, + getFM, + getPanelPosition, + getCSSVar, + getPanel, + getFiles, + showPanel, + hidePanel, + remove, + deleteCurrent, + deleteSelected, + renameCurrent, + scrollIntoViewIfNeeded, + scrollByPages, + changePanel, + getPackerExt, + goToDirectory, + duplicatePanel, + swapPanels, + updateCurrentInfo, }; +assign(DOM, { + ...DOMTree, + ...CurrentFile, + ...Cmd, +}); + +export const CurrentInfo = {}; + DOM.Images = Images; DOM.load = load; DOM.Files = Files; DOM.RESTful = RESTful; +DOM.IO = IO; DOM.Storage = Storage; DOM.Dialog = Dialog; +DOM.CurrentInfo = CurrentInfo; -module.exports = DOM; +export default DOM; -DOM.uploadDirectory = require('./directory'); -DOM.Buffer = require('./buffer'); -DOM.Events = require('./events'); +DOM.uploadDirectory = uploadDirectory; +DOM.Buffer = Buffer; +DOM.Events = Events; +const isString = (a) => typeof a === 'string'; -const loadRemote = require('./load-remote'); -const selectByPattern = require('./select-by-pattern'); +const TabPanel = { + 'js-left': null, + 'js-right': null, +}; -function CmdProto() { - let CurrentInfo = {}; - - const Cmd = this; - const SELECTED_FILE = 'selected-file'; - const TITLE = 'Cloud Commander'; - const TabPanel = { - 'js-left' : null, - 'js-right' : null - }; - - this.loadRemote = (name, options, callback) => { - loadRemote(name, options, callback); - return DOM; - }; - /** - * load jquery from google cdn or local copy - * @param callback - */ - this.loadJquery = function(callback) { - DOM.loadRemote('jquery', { - name : '$' - }, callback); - - return DOM; - }; - - this.loadSocket = function(callback) { - DOM.loadRemote('socket', { - name : 'io' - }, callback); - - return DOM; - }; - - /** - * create new folder - * - */ - this.promptNewDir = function() { - promptNew('directory', '?dir'); - }; - - /** - * create new file - * - * @typeName - * @type - */ - this.promptNewFile = () => { - promptNew('file'); - }; - - function promptNew(typeName, type) { - const {Dialog} = DOM; - const dir = DOM.getCurrentDirPath(); - const msg = 'New ' + typeName || 'File'; - const getName = () => { - const name = DOM.getCurrentName(); - - if (name === '..') - return ''; - - return name; - }; - - const name = getName(); - const cancel = false; - - Dialog.prompt(TITLE, msg, name, {cancel}).then((name) => { - if (!name) - return; - - const path = (type) => { - const result = dir + name; - - if (!type) - return result; - - return result + type; - }; - - RESTful.write(path(type), (error) => { - if (error) - return; - - const currentName = name; - - CloudCmd.refresh({ - currentName - }); - }); - }); - } - - /** -<<<<<<< HEAD - * get current direcotory name - */ - this.getCurrentDirName = () => { - const href = DOM.getCurrentDirPath() - .replace(/\/$/, ''); - - const substr = href.substr(href, href.lastIndexOf('/')); - const ret = href.replace(substr + '/', '') || '/'; - - return ret; - }; - - /** - * get current direcotory path - */ - this.getParentDirPath = (panel) => { - const path = DOM.getCurrentDirPath(panel); - const dirName = DOM.getCurrentDirName() + '/'; - const index = path.lastIndexOf(dirName); - - if (path !== '/') - return path.slice(0, index); - - return path; - }; - - /** - * get not current direcotory path - */ - this.getNotCurrentDirPath = () => { - const panel = DOM.getPanel({active: false}); - const path = DOM.getCurrentDirPath(panel); - - return path; - }; - - /** - * get current file by name - */ - this.getCurrentByName = (name, panel = CurrentInfo.panel) => { - const dataName = 'js-file-' + btoa(encodeURI(name)); - const element = DOM.getByDataName(dataName, panel); - - return element; - }; - - /** - * unified way to get selected files - * - * @currentFile - */ - this.getSelectedFiles = () => { - const panel = DOM.getPanel(); - const selected = DOM.getByClassAll(SELECTED_FILE, panel); - - return [...selected]; - }; - - /* - * unselect all files - */ - this.unselectFiles = (files) => { - files = files || DOM.getSelectedFiles(); - - [...files].forEach(DOM.toggleSelectedFile); - }; - - /** - * get all selected files or current when none selected - * - * @currentFile - */ - this.getActiveFiles = () => { - const current = DOM.getCurrentFile(); - const files = DOM.getSelectedFiles(); - const name = DOM.getCurrentName(current); - - if (!files.length && name !== '..') - return [current]; - - return files; - }; - - this.getCurrentDate = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const date = DOM - .getByDataName('js-date', current) - .textContent; - - return date; - }; - - /** - * get size - * @currentFile - */ - this.getCurrentSize = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - /* если это папка - возвращаем слово dir вместо размера*/ - const size = DOM.getByDataName('js-size', current) - .textContent - .replace(/^<|>$/g, ''); - - return size; - }; - - /** - * get size - * @currentFile - */ - this.loadCurrentSize = (callback, currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const query = '?size'; - const link = DOM.getCurrentPath(current); - - Images.show.load(); - - if (name === '..') - return; - - RESTful.read(link + query, (error, size) => { - if (error) - return; - - DOM.setCurrentSize(size, current); - exec(callback, current); - Images.hide(); - }); - }; - - /** - * load hash - * @callback - * @currentFile - */ - this.loadCurrentHash = (callback, currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const query = '?hash'; - const link = DOM.getCurrentPath(current); - - RESTful.read(link + query, callback); - }; - - /** - * load current modification time of file - * @callback - * @currentFile - */ - this.loadCurrentTime = (callback, currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const query = '?time'; - const link = DOM.getCurrentPath(current); - - RESTful.read(link + query, callback); - }; - - /** - * set size - * @currentFile - */ - this.setCurrentSize = (size, currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const sizeElement = DOM.getByDataName('js-size', current); - - sizeElement.textContent = size; - }; - - /** - * @currentFile - */ - this.getCurrentMode = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const mode = DOM.getByDataName('js-mode', current); - - return mode.textContent; - }; - - /** - * @currentFile - */ - this.getCurrentOwner = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const owner = DOM.getByDataName('js-owner', current); - - return owner.textContent; - }; - - /** - * unified way to get current file content - * - * @param callback - * @param currentFile - */ - this.getCurrentData = (callback, currentFile) => { - let hash; - const Dialog = DOM.Dialog; - const Info = DOM.CurrentInfo; - const current = currentFile || DOM.getCurrentFile(); - const path = DOM.getCurrentPath(current); - const isDir = DOM.isCurrentIsDir(current); - - const func = (error, data) => { - const ONE_MEGABYTE = 1024 * 1024 * 1024; - - if (!error) { - if (itype.object(data)) - data = jonny.stringify(data); - - const length = data.length; - - if (hash && length < ONE_MEGABYTE) - DOM.saveDataToStorage(path, data, hash); - } - - callback(error, data); - }; - - if (Info.name === '..') { - Dialog.alert.noFiles(TITLE); - return callback(Error('No files selected!')); - } - - if (isDir) - return RESTful.read(path, func); - - DOM.checkStorageHash(path, (error, equal, hashNew) => { - if (error) - return callback(error); - - if (equal) - return DOM.getDataFromStorage(path, callback); - - hash = hashNew; - RESTful.read(path, func); - }); - }; - - /** - * unified way to save current file content - * - * @callback - function({data, name}) {} - * @currentFile - */ - this.saveCurrentData = (url, data, callback, query = '') => { - DOM.RESTful.write(url + query, data, (error) => { - !error && DOM.saveDataToStorage(url, data); - }); - }; - - /** - * unified way to get RefreshButton - */ - this.getRefreshButton = (panel) => { - const currentPanel = panel || DOM.getPanel(); - const refresh = DOM.getByDataName('js-refresh', currentPanel); - - return refresh; - }; - - this.setCurrentByName = (name) => { - const current = DOM.getCurrentByName(name); - return DOM.setCurrentFile(current); - }; - - /** - * select current file - * @param currentFile - */ - this.selectFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - - current.classList.add(SELECTED_FILE); - - return Cmd; - }; - - this.unselectFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - - current.classList.remove(SELECTED_FILE); - - return Cmd; - }; - - this.toggleSelectedFile = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const name = DOM.getCurrentName(current); - - if (name === '..') - return Cmd; - - current.classList.toggle(SELECTED_FILE); - - return Cmd; - }; - - this.toggleAllSelectedFiles = () => { - DOM.getAllFiles().map(DOM.toggleSelectedFile); - - return Cmd; - }; - - this.selectAllFiles = () => { - DOM.getAllFiles().map(DOM.selectFile); - - return Cmd; - }; - - this.getAllFiles = () => { - const panel = DOM.getPanel(); - const files = DOM.getFiles(panel); - const name = DOM.getCurrentName(files[0]); - - const from = (a) => a === '..' ? 1 : 0; - const i = from(name); - - return [...files].slice(i); - }; - - /** - * open dialog with expand selection - */ - this.expandSelection = () => { - const msg = 'expand'; - const files = CurrentInfo.files; - - selectByPattern(msg, files); - }; - - /** - * open dialog with shrink selection - */ - this.shrinkSelection = () => { - const msg = 'shrink'; - const files = CurrentInfo.files; - - selectByPattern(msg, files); - }; - - /** - * setting history wrapper - */ - this.setHistory = (data, title, url) => { - const ret = window.history; - - url = CloudCmd.PREFIX + url; - - if (ret) - history.pushState(data, title, url); - - return ret; - }; - - /** - * selected file check - * - * @param currentFile - */ - this.isSelected = (selected) => { - if (!selected) - return false; - - return DOM.isContainClass(selected, SELECTED_FILE); - }; - - /** - * check is current file is a directory - * - * @param currentFile - */ - this.isCurrentIsDir = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const fileType = DOM.getByDataName('js-type', current); - const ret = DOM.isContainClass(fileType, 'directory'); - - return ret; - }; - - /** - * get link from current (or param) file - * - * @param currentFile - current file by default - */ - this.getCurrentLink = (currentFile) => { - const current = currentFile || DOM.getCurrentFile(); - const link = DOM.getByTag('a', current); - - return link[0]; - }; - - this.getFilenames = (files) => { - if (!files) - throw Error('AllFiles could not be empty'); - - const first = files[0] || DOM.getCurrentFile(); - const name = DOM.getCurrentName(first); - - const allFiles = [...files]; - - if (name === '..') - allFiles.shift(); - - const names = allFiles.map((current) => { - return DOM.getCurrentName(current); - }); - - return names; - }; - - /** - * check storage hash - */ - this.checkStorageHash = (name, callback) => { - const parallel = exec.parallel; - const loadHash = DOM.loadCurrentHash; - const nameHash = name + '-hash'; - const getStoreHash = exec.with(Storage.get, nameHash); - - if (typeof name !== 'string') - throw Error('name should be a string!'); - - if (typeof callback !== 'function') - throw Error('callback should be a function!'); - - parallel([loadHash, getStoreHash], (error, loadHash, storeHash) => { - let equal; - const isContain = /error/.test(loadHash); - - if (isContain) - error = loadHash; - else if (loadHash === storeHash) - equal = true; - - callback(error, equal, loadHash); - }); - }; - - /** - * save data to storage - * - * @param name - * @param data - * @param hash - * @param callback - */ - this.saveDataToStorage = function(name, data, hash, callback) { - const isDir = DOM.isCurrentIsDir(); - const nameHash = name + '-hash'; - const nameData = name + '-data'; - - if (isDir) - return exec(callback); - - exec.if(hash, () => { - Storage.set(nameHash, hash); - Storage.set(nameData, data); - - exec(callback, hash); - }, (callback) => { - DOM.loadCurrentHash((error, loadHash) => { - hash = loadHash; - callback(); - }); - }); - }; - - /** - * save data to storage - * - * @param name - * @param data - * @param callback - */ - this.getDataFromStorage = (name, callback) => { - const nameHash = name + '-hash'; - const nameData = name + '-data'; - const isDir = DOM.isCurrentIsDir(); - - if (isDir) - return exec(callback); - - exec.parallel([ - exec.with(Storage.get, nameData), - exec.with(Storage.get, nameHash), - ], callback); - }; - - this.getFM = () => { - return DOM.getPanel().parentElement; - }; - - this.getPanelPosition = (panel) => { - panel = panel || DOM.getPanel(); - - return panel.dataset.name.replace('js-', ''); - }; - - /** function getting panel active, or passive - * @param options = {active: true} - */ - this.getPanel = (options) => { - let files, panel, isLeft; - let dataName = 'js-'; - - const current = DOM.getCurrentFile(); - - if (!current) { - panel = DOM.getByDataName('js-left'); - } else { - files = current.parentElement, - panel = files.parentElement, - isLeft = panel.getAttribute('data-name') === 'js-left'; - } - - /* if {active : false} getting passive panel */ - if (options && !options.active) { - dataName += isLeft ? 'right' : 'left'; - panel = DOM.getByDataName(dataName); - } - - /* if two panels showed - * then always work with passive - * panel - */ - if (window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) - panel = DOM.getByDataName('js-left'); - - if (!panel) - throw Error('can not find Active Panel!'); - - return panel; - }; - - this.getFiles = (element) => { - const files = DOM.getByDataName('js-files', element); - return files.children || []; - }; - - /** - * shows panel right or left (or active) - */ - this.showPanel = (active) => { - const panel = DOM.getPanel({active: active}); - - if (!panel) - return false; - - DOM.show(panel); - - return true; - }; - - /** - * hides panel right or left (or active) - */ - this.hidePanel = (active) => { - const panel = DOM.getPanel({ - active - }); - - if (!panel) - return false; - - return DOM.hide(panel); - }; - - /** - * remove child of element - * @param pChild - * @param element - */ - this.remove = (child, element) => { - const parent = element || document.body; - - parent.removeChild(child); - - return DOM; - }; - - /** - * remove current file from file table - * @param current - * - */ - this.deleteCurrent = (current) => { - if (!current) - DOM.getCurrentFile(); - - const parent = current && current.parentElement; - const name = DOM.getCurrentName(current); - - if (current && name !== '..') { - const next = current.nextSibling; - const prev = current.previousSibling; - - DOM.setCurrentFile(next || prev); - parent.removeChild(current); - } - }; - - /** - * remove selected files from file table - * @Selected - */ - this.deleteSelected = (selected) => { - selected = selected || DOM.getSelectedFiles(); - - if (!selected) - return; - - selected.map(DOM.deleteCurrent); - }; - - /** - * rename current file - * - * @currentFile - */ - this.renameCurrent = (current) => { - const Dialog = DOM.Dialog; - - if (!DOM.isCurrentFile(current)) - current = DOM.getCurrentFile(); - - const from = DOM.getCurrentName(current); - - if (from === '..') - return Dialog.alert.noFiles(TITLE); - - const cancel = false; - - Dialog.prompt(TITLE, 'Rename', from, {cancel}).then((to) => { - const isExist = !!DOM.getCurrentByName(to); - const dirPath = DOM.getCurrentDirPath(); - - if (from === to) - return; - - const files = { - from : dirPath + from, - to : dirPath + to - }; - - RESTful.mv(files, (error) => { - if (error) - return; - - DOM.setCurrentName(to, current); - Storage.remove(dirPath); - - if (isExist) - CloudCmd.refresh(); - }); - }); - }; - - /** - * unified way to scrollIntoViewIfNeeded - * (native suporte by webkit only) - * @param element - * @param center - to scroll as small as possible param should be false - */ - this.scrollIntoViewIfNeeded = function(element, center = false) { - if (!element || !element.scrollIntoViewIfNeeded) - return; - - element.scrollIntoViewIfNeeded(center); - }; - - /* scroll on one page */ - this.scrollByPages = (element, pPages) => { - const ret = element && element.scrollByPages && pPages; - - if (ret) - element.scrollByPages(pPages); - - return ret; - }; - - this.changePanel = () => { - const Info = CurrentInfo; - let panel = DOM.getPanel(); - - CloudCmd.emit('passive-dir', Info.dirPath); - - const panelPassive = DOM.getPanel({ - active: false - }); - - let name = DOM.getCurrentName(); - const filesPassive = DOM.getFiles(panelPassive); - - let dataName = panel.getAttribute('data-name'); - - TabPanel[dataName] = name; - - panel = panelPassive; - dataName = panel.getAttribute('data-name'); - - name = TabPanel[dataName]; - - let files; - let current; - - if (name) { - current = DOM.getCurrentByName(name, panel); - - if (current) - files = current.parentElement; - } - - if (!files || !files.parentElement) { - current = DOM.getCurrentByName(name, panel); - - if (!current) - current = filesPassive[0]; - } - - DOM.setCurrentFile(current, { - history: true - }); - - CloudCmd.emit('active-dir', Info.dirPath); - - return DOM; - }; - - this.getPackerExt = (type) => { - if (type === 'zip') - return '.zip'; - - return '.tar.gz'; - }; - - this.goToDirectory = () => { - const msg = 'Go to directory:'; - const path = CurrentInfo.dirPath; - const Dialog = DOM.Dialog; - const cancel = false; - const setPath = (path) => ({ - path - }); - - Dialog.prompt(TITLE, msg, path, {cancel}) - .then(setPath) - .then(CloudCmd.loadDir); - }, - - this.duplicatePanel = () => { - const Info = CurrentInfo; - const isDir = Info.isDir; - const panel = Info.panelPassive; - const noCurrent = !Info.isOnePanel; - - const getPath = (isDir) => { - if (isDir) - return Info.path; - - return Info.dirPath; - }; - - const path = getPath(isDir); - - CloudCmd.loadDir({ - path, - panel, - noCurrent, - }); - }; - - this.swapPanels = () => { - const Info = CurrentInfo; - const {panel} = Info; - const {files} = Info; - const {element} = Info; - - const panelPassive = Info.panelPassive; - - const dirPath = DOM.getCurrentDirPath(); - const dirPathPassive = DOM.getNotCurrentDirPath(); - - let currentIndex = files.indexOf(element); - - CloudCmd.loadDir({ - path: dirPath, - panel: panelPassive, - noCurrent: true - }); - - CloudCmd.loadDir({ - path: dirPathPassive, - panel: panel - }, () => { - const files = Info.files; - const length = files.length - 1; - - if (currentIndex > length) - currentIndex = length; - - const el = files[currentIndex]; - - DOM.setCurrentFile(el); - }); - }; - - this.CurrentInfo = CurrentInfo, - - this.updateCurrentInfo = (currentFile) => { - const info = DOM.CurrentInfo; - const current = currentFile || DOM.getCurrentFile(); - const files = current.parentElement; - const panel = files.parentElement; - - const panelPassive = DOM.getPanel({ - active: false - }); - - const filesPassive = DOM.getFiles(panelPassive); - const name = DOM.getCurrentName(current); - - info.dir = DOM.getCurrentDirName(); - info.dirPath = DOM.getCurrentDirPath(); - info.parentDirPath = DOM.getParentDirPath(); - info.element = current; - info.ext = Util.getExt(name); - info.files = [...files.children]; - info.filesPassive = [...filesPassive]; - info.first = files.firstChild; - info.getData = DOM.getCurrentData; - info.last = files.lastChild; - info.link = DOM.getCurrentLink(current); - info.mode = DOM.getCurrentMode(current); - info.name = name; - info.path = DOM.getCurrentPath(current); - info.panel = panel; - info.panelPassive = panelPassive; - info.size = DOM.getCurrentSize(current); - info.isDir = DOM.isCurrentIsDir(); - info.isSelected = DOM.isSelected(current); - info.panelPosition = DOM.getPanel().dataset.name.replace('js-', ''); - info.isOnePanel = - info.panel.getAttribute('data-name') === - info.panelPassive.getAttribute('data-name'); - }; +export function loadRemote(name, options, callback) { + _loadRemote(name, options, callback); + return DOM; } +export function loadSocket(callback) { + DOM.loadRemote('socket', { + name: 'io', + }, callback); + + return DOM; +} + +/** + * create new folder + * + */ +export async function promptNewDir() { + await promptNew('directory'); +} + +/** + * create new file + * + * @typeName + * @type + */ +export async function promptNewFile() { + await promptNew('file'); +} + +async function promptNew(typeName) { + const {Dialog} = DOM; + const dir = DOM.getCurrentDirPath(); + const msg = `New ${typeName}` || 'File'; + + const getName = () => { + const name = DOM.getCurrentName(); + + if (name === '..') + return ''; + + return name; + }; + + const name = getName(); + const [cancel, currentName] = await Dialog.prompt(msg, name); + + if (cancel) + return; + + const path = `${dir}${currentName}`; + + if (typeName === 'directory') + await RESTful.createDirectory(path); + else + await RESTful.write(path); + + await CloudCmd.refresh({ + currentName, + }); +} + +/** + * get current directory name + */ +export function getCurrentDirName() { + const href = DOM + .getCurrentDirPath() + .replace(/\/$/, ''); + + const substr = href.substr(href, href.lastIndexOf('/')); + + return href.replace(`${substr}/`, '') || '/'; +} + +/** + * get current directory path + */ +export function getParentDirPath(panel) { + const path = DOM.getCurrentDirPath(panel); + const dirName = DOM.getCurrentDirName() + '/'; + const index = path.lastIndexOf(dirName); + + if (path !== '/') + return path.slice(0, index); + + return path; +} + +/** + * get not current directory path + */ +export function getNotCurrentDirPath() { + const panel = DOM.getPanel({ + active: false, + }); + + return DOM.getCurrentDirPath(panel); +} + +/* + * unselect all files + */ +export function unselectFiles(files) { + files = files || DOM.getSelectedFiles(); + + Array + .from(files) + .forEach(DOM.toggleSelectedFile); +} + +/** + * get all selected files or current when none selected + * + * @currentFile + */ +export function getActiveFiles() { + const current = DOM.getCurrentFile(); + const files = DOM.getSelectedFiles(); + const name = DOM.getCurrentName(current); + + if (!files.length && name !== '..') + return [current]; + + return files; +} + +export function getCurrentDate(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + return DOM.getByDataName('js-date', current).textContent; +} + +/** + * get size + * @currentFile + */ +export function getCurrentSize(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + + /* если это папка - возвращаем слово dir вместо размера*/ + const size = DOM + .getByDataName('js-size', current) + .textContent + .replace(/^<|>$/g, ''); + + return size; +} + +/** + * get size + * @currentFile + */ +export async function loadCurrentSize(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const query = '?size'; + const link = DOM.getCurrentPath(current); + + Images.show.load(); + + if (name === '..') + return; + + const [, size] = await RESTful.read(link + query); + + DOM.setCurrentSize(size, current); + Images.hide(); + + return current; +} + +/** + * load hash + * @callback + * @currentFile + */ +export async function loadCurrentHash(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const query = '?hash'; + const link = DOM.getCurrentPath(current); + + const [, data] = await RESTful.read(link + query); + + return data; +} + +/** + * set size + * @currentFile + */ +export function setCurrentSize(size, currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const sizeElement = DOM.getByDataName('js-size', current); + + sizeElement.textContent = size; +} + +/** + * @currentFile + */ +export function getCurrentMode(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const mode = DOM.getByDataName('js-mode', current); + + return mode.textContent; +} + +/** + * @currentFile + */ +export function getCurrentOwner(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const owner = DOM.getByDataName('js-owner', current); + + return owner.textContent; +} + +/** + * unified way to get current file content + * + * @param currentFile + */ +export async function getCurrentData(currentFile) { + const {Dialog} = DOM; + const Info = DOM.CurrentInfo; + const current = currentFile || DOM.getCurrentFile(); + const path = DOM.getCurrentPath(current); + const isDir = DOM.isCurrentIsDir(current); + + if (Info.name === '..') { + Dialog.alert.noFiles(); + return [ + Error('No Files'), + ]; + } + + if (isDir) + return await RESTful.read(path); + + const [hashNew, hash] = await DOM.checkStorageHash(path); + + if (!hashNew) + return [ + Error(`Can't get hash of a file`), + ]; + + if (hash === hashNew) + return [null, await Storage.get(`${path}-data`)]; + + const [e, data] = await RESTful.read(path); + + if (e) + return [ + e, + null, + ]; + + const ONE_MEGABYTE = 1024 ** 2 * 1024; + const {length} = data; + + if (hash && length < ONE_MEGABYTE) + await DOM.saveDataToStorage(path, data, hashNew); + + return [null, data]; +} + +/** + * unified way to get RefreshButton + */ +export function getRefreshButton(panel = DOM.getPanel()) { + return DOM.getByDataName('js-refresh', panel); +} + +export function getAllFiles() { + const panel = DOM.getPanel(); + const files = DOM.getFiles(panel); + const name = DOM.getCurrentName(files[0]); + + const from = (a) => a === '..' ? 1 : 0; + const i = from(name); + + return Array + .from(files) + .slice(i); +} + +/** + * open dialog with expand selection + */ +export async function expandSelection() { + const msg = 'expand'; + const {files} = CurrentInfo; + + await selectByPattern(msg, files); +} + +/** + * open dialog with shrink selection + */ +export async function shrinkSelection() { + const msg = 'shrink'; + const {files} = CurrentInfo; + + await selectByPattern(msg, files); +} + +/** + * setting history wrapper + */ +export function setHistory(data, title, url) { + const ret = globalThis.history; + const {prefix} = CloudCmd; + + url = prefix + url; + + if (ret) + history.pushState(data, title, url); + + return ret; +} + +/** + * get link from current (or param) file + * + * @param currentFile - current file by default + */ +export function getCurrentLink(currentFile) { + const current = currentFile || DOM.getCurrentFile(); + const link = DOM.getByTag('a', current); + + return link[0]; +} + +export function getFilenames(files) { + if (!files) + throw Error('AllFiles could not be empty'); + + const first = files[0] || DOM.getCurrentFile(); + const name = DOM.getCurrentName(first); + + const allFiles = Array.from(files); + + if (name === '..') + allFiles.shift(); + + return allFiles.map(DOM.getCurrentName); +} + +/** + * check storage hash + */ +export async function checkStorageHash(name) { + const nameHash = `${name}-hash`; + + if (!isString(name)) + throw Error('name should be a string!'); + + const [loadHash, storeHash] = await Promise.all([ + DOM.loadCurrentHash(), + Storage.get(nameHash), + ]); + + return [loadHash, storeHash]; +} + +/** + * save data to storage + * + * @param name + * @param data + * @param hash + */ +export async function saveDataToStorage(name, data, hash) { + const isDir = DOM.isCurrentIsDir(); + + if (isDir) + return; + + hash = hash || await DOM.loadCurrentHash(); + + const nameHash = `${name}-hash`; + const nameData = `${name}-data`; + + await Storage.set(nameHash, hash); + await Storage.set(nameData, data); + + return hash; +} + +export function getFM() { + const {parentElement} = DOM.getPanel(); + return parentElement; +} + +export function getPanelPosition(panel) { + panel = panel || DOM.getPanel(); + + return panel.dataset.name.replace('js-', ''); +} + +export function getCSSVar(name, {body = document.body} = {}) { + const bodyStyle = getComputedStyle(body); + return bodyStyle.getPropertyValue(`--${name}`); +} + +/** function getting panel active, or passive + * @param options = {active: true} + */ +export function getPanel(options) { + let files; + let panel; + let isLeft; + let dataName = 'js-'; + + const current = DOM.getCurrentFile(); + + if (!current) { + panel = DOM.getByDataName('js-left'); + } else { + files = current.parentElement; + panel = files.parentElement; + isLeft = panel.getAttribute('data-name') === 'js-left'; + } + + /* if {active : false} getting passive panel */ + if (options && !options.active) { + dataName += isLeft ? 'right' : 'left'; + panel = DOM.getByDataName(dataName); + } + + /* if two panels showed + * then always work with passive + * panel + */ + if (globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH) + panel = DOM.getByDataName('js-left'); + + if (!panel) + throw Error('can not find Active Panel!'); + + return panel; +} + +export function getFiles(element) { + const files = DOM.getByDataName('js-files', element); + return files.children || []; +} + +/** + * shows panel right or left (or active) + */ +export function showPanel(active) { + const panel = DOM.getPanel({ + active, + }); + + if (!panel) + return false; + + DOM.show(panel); + + return true; +} + +/** + * hides panel right or left (or active) + */ +export function hidePanel(active) { + const panel = DOM.getPanel({ + active, + }); + + if (!panel) + return false; + + return DOM.hide(panel); +} + +/** + * remove child of element + * @param child + * @param element + */ +export function remove(child, element) { + const parent = element || document.body; + + parent.removeChild(child); + + return DOM; +} + +/** + * remove current file from file table + * @param current + * + */ +export function deleteCurrent(current) { + if (!current) + DOM.getCurrentFile(); + + const parent = current?.parentElement; + const name = DOM.getCurrentName(current); + + if (current && name !== '..') { + const next = current.nextSibling; + const prev = current.previousSibling; + + DOM.setCurrentFile(next || prev); + parent.removeChild(current); + } +} + +/** + * remove selected files from file table + * @Selected + */ +export function deleteSelected(selected) { + selected = selected || DOM.getSelectedFiles(); + + if (!selected) + return; + + selected.map(DOM.deleteCurrent); +} + +/** + * rename current file + * + * @currentFile + */ +export function scrollIntoViewIfNeeded(element, center = false) { + if (!element || !element.scrollIntoViewIfNeeded) + return; + + element.scrollIntoViewIfNeeded(center); +} + +/* scroll on one page */ +export function scrollByPages(element, pPages) { + const ret = element?.scrollByPages && pPages; + + if (ret) + element.scrollByPages(pPages); + + return ret; +} + +export function changePanel() { + const Info = CurrentInfo; + let panel = DOM.getPanel(); + + CloudCmd.emit('passive-dir', Info.dirPath); + + const panelPassive = DOM.getPanel({ + active: false, + }); + + let name = DOM.getCurrentName(); + const filesPassive = DOM.getFiles(panelPassive); + + let dataName = panel.getAttribute('data-name'); + + TabPanel[dataName] = name; + + panel = panelPassive; + dataName = panel.getAttribute('data-name'); + + name = TabPanel[dataName]; + + let files; + let current; + + if (name) { + current = DOM.getCurrentByName(name, panel); + + if (current) + files = current.parentElement; + } + + if (!files || !files.parentElement) { + current = DOM.getCurrentByName(name, panel); + + if (!current) + [current] = filesPassive; + } + + DOM.setCurrentFile(current, { + history: true, + }); + + CloudCmd.emit('active-dir', Info.dirPath); + + return DOM; +} + +export function getPackerExt(type) { + if (type === 'zip') + return '.zip'; + + return '.tar.gz'; +} + +export async function goToDirectory(overrides = {}) { + const {Dialog} = DOM; + const { + prompt = Dialog.prompt, + changeDir = CloudCmd.changeDir, + } = overrides; + + const msg = 'Go to directory:'; + const {dirPath} = CurrentInfo; + + const [cancel, path = dirPath] = await prompt(msg, dirPath); + + if (cancel) + return; + + await changeDir(path); +} + +export async function duplicatePanel() { + const Info = CurrentInfo; + const {isDir} = Info; + const panel = Info.panelPassive; + const noCurrent = !Info.isOnePanel; + + const getPath = (isDir) => { + if (isDir) + return Info.path; + + return Info.dirPath; + }; + + const path = getPath(isDir); + + await CloudCmd.changeDir(path, { + panel, + noCurrent, + }); +} + +export async function swapPanels() { + const Info = CurrentInfo; + const { + panel, + files, + element, + panelPassive, + } = Info; + + const path = DOM.getCurrentDirPath(); + const dirPathPassive = DOM.getNotCurrentDirPath(); + + let currentIndex = files.indexOf(element); + + await CloudCmd.changeDir(path, { + panel: panelPassive, + noCurrent: true, + }); + + await CloudCmd.changeDir(dirPathPassive, { + panel, + }); + + const length = Info.files.length - 1; + + if (currentIndex > length) + currentIndex = length; + + const el = Info.files[currentIndex]; + + DOM.setCurrentFile(el); +} + +export function updateCurrentInfo(currentFile) { + const info = DOM.CurrentInfo; + const current = currentFile || DOM.getCurrentFile(); + const files = current.parentElement; + + const panelPassive = DOM.getPanel({ + active: false, + }); + + const filesPassive = DOM.getFiles(panelPassive); + const name = DOM.getCurrentName(current); + + info.dir = DOM.getCurrentDirName(); + info.dirPath = DOM.getCurrentDirPath(); + info.parentDirPath = DOM.getParentDirPath(); + info.element = current; + info.ext = getExt(name); + info.files = Array.from(files.children); + info.filesPassive = Array.from(filesPassive); + info.first = files.firstChild; + info.getData = DOM.getCurrentData; + info.last = files.lastChild; + info.link = DOM.getCurrentLink(current); + info.mode = DOM.getCurrentMode(current); + info.name = name; + info.path = DOM.getCurrentPath(current); + info.panel = files.parentElement || DOM.getPanel(); + info.panelPassive = panelPassive; + info.size = DOM.getCurrentSize(current); + info.isDir = DOM.isCurrentIsDir(); + info.isSelected = DOM.isSelected(current); + info.panelPosition = DOM + .getPanel() + .dataset + .name + .replace('js-', ''); + info.isOnePanel = info.panel.getAttribute('data-name') === info.panelPassive.getAttribute('data-name'); +} diff --git a/client/dom/index.spec.js b/client/dom/index.spec.js new file mode 100644 index 00000000..268efe5f --- /dev/null +++ b/client/dom/index.spec.js @@ -0,0 +1,52 @@ +import {test, stub} from 'supertape'; +import {getCSSVar, goToDirectory} from '#dom'; + +globalThis.CloudCmd = {}; + +test('cloudcmd: client: dom: goToDirectory', async (t) => { + const path = ''; + const changeDir = stub(); + const prompt = stub().returns([null, path]); + + await goToDirectory({ + prompt, + changeDir, + }); + + t.calledWith(changeDir, [path]); + t.end(); +}); + +test('cloudcmd: client: dom: getCSSVar', (t) => { + const body = {}; + const getPropertyValue = stub().returns(0); + + globalThis.getComputedStyle = stub().returns({ + getPropertyValue, + }); + const result = getCSSVar('hello', { + body, + }); + + delete globalThis.getComputedStyle; + + t.notOk(result); + t.end(); +}); + +test('cloudcmd: client: dom: getCSSVar: 1', (t) => { + const body = {}; + const getPropertyValue = stub().returns(1); + + globalThis.getComputedStyle = stub().returns({ + getPropertyValue, + }); + const result = getCSSVar('hello', { + body, + }); + + delete globalThis.getComputedStyle; + + t.ok(result); + t.end(); +}); diff --git a/client/dom/io/index.js b/client/dom/io/index.js new file mode 100644 index 00000000..192769e6 --- /dev/null +++ b/client/dom/io/index.js @@ -0,0 +1,166 @@ +import {FS} from '#common/cloudfunc'; +import {sendRequest as _sendRequest} from './send-request.js'; + +const {assign} = Object; + +const imgPosition = { + top: true, +}; + +export const remove = async (url, data, overrides = {}) => { + const { + sendRequest = _sendRequest, + } = overrides; + + const request = { + method: 'DELETE', + url: FS + url, + imgPosition: { + top: Boolean(data), + }, + }; + + if (data) + assign(request, { + data, + url: `${request.url}?files`, + }); + + return await sendRequest(request); +}; + +export const patch = async (url, data) => { + return await _sendRequest({ + method: 'PATCH', + url: FS + url, + data, + imgPosition, + }); +}; + +export const write = async (url, data) => { + return await _sendRequest({ + method: 'PUT', + url: FS + url, + data, + imgPosition, + }); +}; + +export const createDirectory = async (url, overrides = {}) => { + const { + sendRequest = _sendRequest, + } = overrides; + + return await sendRequest({ + method: 'PUT', + url: `${FS}${url}?dir`, + imgPosition, + }); +}; + +export const read = async (url, dataType = 'text') => { + const notLog = !url.includes('?'); + + return await _sendRequest({ + method: 'GET', + url: FS + url, + notLog, + dataType, + }); +}; + +export const copy = async (from, to, names) => { + return await _sendRequest({ + method: 'PUT', + url: '/copy', + data: { + from, + to, + names, + }, + imgPosition, + }); +}; + +export const pack = async (data) => { + return await _sendRequest({ + method: 'PUT', + url: '/pack', + data, + }); +}; + +export const extract = async (data) => { + return await _sendRequest({ + method: 'PUT', + url: '/extract', + data, + }); +}; + +export const move = async (from, to, names) => { + return await _sendRequest({ + method: 'PUT', + url: '/move', + data: { + from, + to, + names, + }, + imgPosition, + }); +}; + +export const rename = async (from, to) => { + return await _sendRequest({ + method: 'PUT', + url: '/rename', + data: { + from, + to, + }, + imgPosition, + }); +}; + +export const Config = { + read: async () => { + return await _sendRequest({ + method: 'GET', + url: '/config', + imgPosition, + notLog: true, + }); + }, + + write: async (data) => { + return await _sendRequest({ + method: 'PATCH', + url: '/config', + data, + imgPosition, + }); + }, +}; + +export const Markdown = { + read: async (url) => { + return await _sendRequest({ + method: 'GET', + url: `/markdown${url}`, + imgPosition, + notLog: true, + }); + }, + + render: async (data) => { + return await _sendRequest({ + method: 'PUT', + url: '/markdown', + data, + imgPosition, + notLog: true, + }); + }, +}; diff --git a/client/dom/io/index.spec.js b/client/dom/io/index.spec.js new file mode 100644 index 00000000..8e9e2d08 --- /dev/null +++ b/client/dom/io/index.spec.js @@ -0,0 +1,61 @@ +import {test, stub} from 'supertape'; +import * as io from './index.js'; + +test('client: dom: io', (t) => { + const sendRequest = stub(); + + io.createDirectory('/hello', { + sendRequest, + }); + + const expected = { + imgPosition: { + top: true, + }, + method: 'PUT', + url: '/fs/hello?dir', + }; + + t.calledWith(sendRequest, [expected]); + t.end(); +}); + +test('client: dom: io: remove: no files', async (t) => { + const sendRequest = stub(); + + await io.remove('/hello', null, { + sendRequest, + }); + + const expected = { + imgPosition: { + top: false, + }, + method: 'DELETE', + url: '/fs/hello', + }; + + t.calledWith(sendRequest, [expected]); + t.end(); +}); + +test('client: dom: io: remove: files', async (t) => { + const sendRequest = stub(); + const files = ['world']; + + await io.remove('/hello', files, { + sendRequest, + }); + + const expected = { + imgPosition: { + top: true, + }, + data: ['world'], + method: 'DELETE', + url: '/fs/hello?files', + }; + + t.calledWith(sendRequest, [expected]); + t.end(); +}); diff --git a/client/dom/io/send-request.js b/client/dom/io/send-request.js new file mode 100644 index 00000000..177d34b5 --- /dev/null +++ b/client/dom/io/send-request.js @@ -0,0 +1,48 @@ +/* global CloudCmd */ +import {promisify} from 'es6-promisify'; +import * as Images from '#dom/images'; +import * as load from '#dom/load'; + +export const sendRequest = promisify((params, callback) => { + const p = params; + const {prefixURL} = CloudCmd; + + p.url = prefixURL + p.url; + p.url = encodeURI(p.url); + + p.url = replaceHash(p.url); + + load.ajax({ + method: p.method, + url: p.url, + data: p.data, + dataType: p.dataType, + error: (jqXHR) => { + const response = jqXHR.responseText; + + const {statusText, status} = jqXHR; + + const text = status === 404 ? response : statusText; + + callback(Error(text)); + }, + success: (data) => { + Images.hide(); + + if (!p.notLog) + CloudCmd.log(data); + + callback(null, data); + }, + }); +}); + +export const _replaceHash = replaceHash; + +function replaceHash(url) { + /* + * if we send ajax request - + * no need in hash so we escape # + */ + return url.replace(/#/g, '%23'); +} diff --git a/client/dom/io/send-request.spec.js b/client/dom/io/send-request.spec.js new file mode 100644 index 00000000..f8a960d8 --- /dev/null +++ b/client/dom/io/send-request.spec.js @@ -0,0 +1,11 @@ +import {test} from 'supertape'; +import {_replaceHash} from './send-request.js'; + +test('cloudcmd: client: io: replaceHash', (t) => { + const url = '/hello/####world'; + const result = _replaceHash(url); + const expected = '/hello/%23%23%23%23world'; + + t.equal(result, expected); + t.end(); +}); diff --git a/client/dom/load-remote.js b/client/dom/load-remote.js index 525fe63b..5be8e327 100644 --- a/client/dom/load-remote.js +++ b/client/dom/load-remote.js @@ -1,69 +1,58 @@ -'use strict'; - /* global CloudCmd */ +import {callbackify} from 'node:util'; +import {rendy} from 'rendy'; +import itype from 'itype'; +import * as load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {findObjByNameInArr} from '#common/util'; +import * as Files from '#dom/files'; -const exec = require('execon'); -const rendy = require('rendy/legacy'); -const itype = require('itype/legacy'); -const wraptile = require('wraptile/legacy'); - -const {findObjByNameInArr} = require('../../common/util'); - -const load = require('./load'); -const Files = require('./files'); - -const parallel = wraptile(load.parallel); - -module.exports = (name, options, callback = options) => { - const {PREFIX, config} = CloudCmd; +export const loadRemote = callbackify(async (name, options) => { + const {prefix, config} = CloudCmd; const o = options; if (o.name && window[o.name]) - return callback(); + return; - Files.get('modules', (error, modules) => { - const online = config('online') && navigator.onLine; - const module = findObjByNameInArr(modules.remote, name); + const modules = await Files.get('modules'); + + const online = config('online') && navigator.onLine; + const module = findObjByNameInArr(modules.remote, name); + + const isArray = itype.array(module.local); + const {version} = module; + + let remoteTmpls; + let local; + + if (isArray) { + remoteTmpls = module.remote; + ({local} = module); + } else { + remoteTmpls = [module.remote]; + local = [module.local]; + } + + const localURL = []; + + for (const url of local) { + localURL.push(prefix + url); + } + + const remoteURL = []; + + for (const tmpl of remoteTmpls) { + remoteURL.push(rendy(tmpl, { + version, + })); + } + + if (online) { + const [e] = await tryToCatch(load.parallel, remoteURL); - const isArray = itype.array(module.local); - const version = module.version; - - let remoteTmpls, local; - if (isArray) { - remoteTmpls = module.remote; - local = module.local; - } else { - remoteTmpls = [module.remote]; - local = [module.local]; - } - - const localURL = local.map((url) => { - return PREFIX + url; - }); - - const remoteURL = remoteTmpls.map((tmpl) => { - return rendy(tmpl, { - version: version - }); - }); - - const on = funcON(localURL, remoteURL, callback); - const off = funcOFF(localURL, callback); - - exec.if(online, on, off); - }); -}; - -function funcOFF(local, callback) { - return parallel(local, callback); -} - -function funcON (local, remote,callback) { - return parallel(remote, (error) => { - if (error) - return funcOFF(); - - callback(); - }); -} - + if (!e) + return; + } + + await load.parallel(localURL); +}); diff --git a/client/dom/load.js b/client/dom/load.js index f429e594..8c7011c1 100644 --- a/client/dom/load.js +++ b/client/dom/load.js @@ -1,130 +1,8 @@ -'use strict'; - -const itype = require('itype/legacy'); -const jonny = require('jonny/legacy'); -const Emitify = require('emitify/legacy'); -const exec = require('execon'); -const Images = require('./images'); -const Events = require('./events'); - -const {getExt} = require('../../common/util'); - -module.exports = load; -module.exports.getIdBySrc = getIdBySrc; -module.exports.ext = ext; - -/** - * Функция создаёт элемент и загружает файл с src. - * - * @param params = { - * name, - название тэга - * src', - путь к файлу - * func, - обьект, содержаий одну из функций - * или сразу две onload и onerror - * {onload: function() {}, onerror: function();} - * style, - * id, - * element, - * async, - true by default - * inner: 'id{color:red, }, - * class, - * notAppend - false by default - * - */ -function load(params) { - const { - src, - id = getIdBySrc(params.src), - func, - name, - async, - inner, - style, - parent = document.body, - className, - attribute, - notAppend, - } = params; - - let element = document.getElementById(id); - - if (element) { - exec(func); - return element; - } - - element = document.createElement(name); - - const funcError = () => { - const msg = `file ${src} could not be loaded`; - const error = new Error(msg); - - parent.removeChild(element); - - Images.show.error(msg); - - const callback = func && func.onerror || func.onload || func; - - exec(callback, error); - }; - - const funcLoad = () => { - const callback = func && func.onload || func; - - Events.remove('error', element, funcError); - - exec(callback); - }; - - if (/^(script|link)$/.test(name)) - Events.addOnce('load', element, funcLoad) - .addError(element, funcError); - - if (id) - element.id = id; - - if (className) - element.className = className; - - if (src) { - if (name !== 'link') { - element.src = src; - } else { - element.href = src; - element.rel = 'stylesheet'; - } - } - - if (attribute) { - const type = itype(attribute); - - switch(type) { - case 'string': - element.setAttribute(attribute, ''); - break; - - case 'object': - Object.keys(attribute).forEach((name) => { - element.setAttribute(name, attribute[name]); - }); - break; - } - } - - if (style) - element.style.cssText = style; - - if (async && name === 'script' || async === undefined) - element.async = true; - - if (!notAppend) - parent.appendChild(element); - - if (inner) - element.innerHTML = inner; - - return element; -} +import itype from 'itype'; +import jonny from 'jonny'; +import Emitify from 'emitify'; +import exec from 'execon'; +import * as Images from '#dom/images'; /** * Function gets id by src @@ -132,17 +10,18 @@ function load(params) { * * Example: http://domain.com/1.js -> 1_js */ -function getIdBySrc(src) { +export function getIdBySrc(src) { const isStr = itype.string(src); if (!isStr) return; - if (~src.indexOf(':')) + if (src.includes(':')) src += '-join'; const num = src.lastIndexOf('/') + 1; const sub = src.substr(src, num); + const id = src .replace(sub, '') .replace(/\./g, '-'); @@ -155,34 +34,37 @@ function getIdBySrc(src) { * * @param params */ -module.exports.ajax = (params) => { +export const ajax = (params) => { const p = params; const isObject = itype.object(p.data); const isArray = itype.array(p.data); const isArrayBuf = itype(p.data) === 'arraybuffer'; const type = p.type || p.method || 'GET'; - const headers = p.headers || {}; + + const {headers = {}} = p; + const xhr = new XMLHttpRequest(); xhr.open(type, p.url, true); - Object.keys(headers).forEach((name) => { + for (const name of Object.keys(headers)) { const value = headers[name]; xhr.setRequestHeader(name, value); - }); + } if (p.responseType) xhr.responseType = p.responseType; let data; + if (!isArrayBuf && isObject || isArray) data = jonny.stringify(p.data); else - data = p.data; + ({data} = p); xhr.onreadystatechange = (event) => { const xhr = event.target; - const OK = 200; + const OK = 200; if (xhr.readyState !== xhr.DONE) return; @@ -190,30 +72,30 @@ module.exports.ajax = (params) => { Images.clearProgress(); const TYPE_JSON = 'application/json'; - const type = xhr.getResponseHeader('content-type'); + const type = xhr.getResponseHeader('content-type'); if (xhr.status !== OK) return exec(p.error, xhr); const notText = p.dataType !== 'text'; - const isContain = ~type.indexOf(TYPE_JSON); + const isContain = type.includes(TYPE_JSON); let data = xhr.response; + if (type && isContain && notText) data = jonny.parse(xhr.response) || xhr.response; - + exec(p.success, data, xhr.statusText, xhr); }; xhr.send(data); }; -module.exports.put = (url, body) => { +export const put = (url, body) => { const emitter = Emitify(); const xhr = new XMLHttpRequest(); - url = encodeURI(url) - .replace('#', '%23'); + url = encodeURI(url).replace(/#/g, '#'); xhr.open('put', url, true); @@ -221,8 +103,8 @@ module.exports.put = (url, body) => { if (!event.lengthComputable) return; - const percent = (event.loaded / event.total) * 100; - const count = Math.round(percent); + const percent = event.loaded / event.total * 100; + const count = Math.round(percent); emitter.emit('progress', count); }; @@ -234,8 +116,10 @@ module.exports.put = (url, body) => { if (!over) return; - if (xhr.status === OK) + if (xhr.status === OK) { + emitter.emit('progress', 100); return emitter.emit('end'); + } const error = Error(xhr.responseText); emitter.emit('error', error); @@ -245,117 +129,3 @@ module.exports.put = (url, body) => { return emitter; }; - -function ext(src, func) { - switch (getExt(src)) { - case '.js': - return load.js(src, func); - - case '.css': - return load.css(src, func); - - default: - return load({ - src, - func, - }); - } -} - -/** - * create elements and load them to DOM-tree - * one-by-one - * - * @param params - * @param callback - */ -load.series = (params, callback) => { - if (!params) - return load; - - const funcs = params - .map((url) => ext.bind(null, url)) - .concat(callback); - - exec.series(funcs); - - return load; -}; - -/** - * improve callback of funcs so - * we pop number of function and - * if it's last we call pCallBack - * - * @param params - * @param callback - onload function - */ -load.parallel = (params, callback) => { - if (!params) - return load; - - const funcs = params.map((url) => { - return ext.bind(null, url); - }); - - exec.parallel(funcs, callback); - - return load; -}; - -/** - * Функция загружает js-файл - * - * @param src - * @param func - */ -load.js = (src, func) => { - const name = 'script'; - - return load({ - name, - src, - func, - }); -}, - -load.css = (src, func) => { - const name = 'link'; - const {head:parent} = document; - - return load({ - name, - src, - parent, - func - }); -}; - -/** - * Функция создаёт елемент style и записывает туда стили - * @param params - структура параметров, заполняеться таким - * образом: {src: ' ',func: '', id: '', element: '', inner: ''} - * все параметры опциональны - */ -load.style = (params) => { - const { - id, - src, - name = 'style', - func, - inner, - parent = document.head, - element, - } = params; - - return load({ - id, - src, - func, - name, - inner, - parent, - element, - }); -}; - diff --git a/client/dom/operations/rename-current.js b/client/dom/operations/rename-current.js new file mode 100644 index 00000000..8b6536d3 --- /dev/null +++ b/client/dom/operations/rename-current.js @@ -0,0 +1,66 @@ +/* global CloudCmd */ +import capitalize from 'just-capitalize'; +import * as _Dialog from '#dom/dialog'; +import * as Storage from '#dom/storage'; +import * as RESTful from '#dom/rest'; +import * as _currentFile from '../current-file.js'; + +export default async (current, overrides = {}) => { + const { + refresh = CloudCmd.refresh, + Dialog = _Dialog, + currentFile = _currentFile, + } = overrides; + + const { + isCurrentFile, + getCurrentName, + getCurrentFile, + getCurrentByName, + getCurrentType, + getCurrentDirPath, + setCurrentName, + } = currentFile; + + if (!isCurrentFile(current)) + current = getCurrentFile(); + + const from = getCurrentName(current); + + if (from === '..') + return Dialog.alert.noFiles(); + + const [cancel, to] = await Dialog.prompt('Rename', from); + + if (cancel) + return; + + const nextFile = getCurrentByName(to); + + if (nextFile) { + const type = getCurrentType(nextFile); + const msg = `${capitalize(type)} "${to}" already exists. Proceed?`; + const [cancel] = await Dialog.confirm(msg); + + if (cancel) + return; + } + + if (from === to) + return; + + const dirPath = getCurrentDirPath(); + + const fromFull = `${dirPath}${from}`; + const toFull = `${dirPath}${to}`; + + const [e] = await RESTful.rename(fromFull, toFull); + + if (e) + return; + + setCurrentName(to, current); + + Storage.remove(dirPath); + refresh(); +}; diff --git a/client/dom/operations/rename-current.spec.js b/client/dom/operations/rename-current.spec.js new file mode 100644 index 00000000..a5ebb415 --- /dev/null +++ b/client/dom/operations/rename-current.spec.js @@ -0,0 +1,86 @@ +import {test, stub} from 'supertape'; +import renameCurrent from './rename-current.js'; + +test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { + const current = {}; + const isCurrentFile = stub(); + + const currentFile = stubCurrentFile({ + isCurrentFile, + }); + + await renameCurrent(current, { + Dialog: stubDialog(), + currentFile, + }); + + t.calledWith(isCurrentFile, [current], 'should call isCurrentFile'); + t.end(); +}); + +test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { + const current = {}; + const name = 'hello'; + + const prompt = stub().returns([null, name]); + const confirm = stub().returns([true]); + + const getCurrentByName = stub().returns(current); + const getCurrentType = stub().returns('directory'); + + const Dialog = stubDialog({ + confirm, + prompt, + }); + + const currentFile = stubCurrentFile({ + getCurrentByName, + getCurrentType, + }); + + await renameCurrent(null, { + Dialog, + currentFile, + }); + + const expected = 'Directory "hello" already exists. Proceed?'; + + t.calledWith(confirm, [expected], 'should call confirm'); + t.end(); +}); + +const stubDialog = (fns = {}) => { + const { + alert = stub().returns([]), + confirm = stub().returns([]), + prompt = stub().returns([]), + } = fns; + + return { + alert, + confirm, + prompt, + }; +}; + +const stubCurrentFile = (fns = {}) => { + const { + isCurrentFile = stub(), + getCurrentName = stub(), + getCurrentFile = stub(), + getCurrentByName = stub(), + getCurrentType = stub(), + getCurrentDirPath = stub(), + setCurrentName = stub(), + } = fns; + + return { + isCurrentFile, + getCurrentName, + getCurrentFile, + getCurrentByName, + getCurrentType, + getCurrentDirPath, + setCurrentName, + }; +}; diff --git a/client/dom/rest.js b/client/dom/rest.js index 6e0faa01..3cf2fb66 100644 --- a/client/dom/rest.js +++ b/client/dom/rest.js @@ -1,220 +1,40 @@ -'use strict'; +import {tryToCatch} from 'try-to-catch'; +import * as Dialog from '#dom/dialog'; +import * as Images from '#dom/images'; +import {encode} from '#common/entity'; +import * as IO from './io/index.js'; -/* global CloudCmd, DOM */ +const handleError = (promise) => async (...args) => { + const [e, data] = await tryToCatch(promise, ...args); + + if (!e) + return [e, data]; + + const encoded = encode(e.message); + + Images.show.error(encoded); + Dialog.alert(encoded); + + return [e, data]; +}; -const itype = require('itype/legacy'); +export const remove = handleError(IO.remove); +export const patch = handleError(IO.patch); +export const write = handleError(IO.write); +export const createDirectory = handleError(IO.createDirectory); +export const read = handleError(IO.read); +export const copy = handleError(IO.copy); +export const pack = handleError(IO.pack); +export const extract = handleError(IO.extract); +export const move = handleError(IO.move); +export const rename = handleError(IO.rename); -const {FS} = require('../../common/cloudfunc'); -const { - encode, -} = require('../../common/entity'); +export const Config = { + read: handleError(IO.Config.read), + write: handleError(IO.Config.write), +}; -module.exports = new RESTful(); - -const Images = require('./images'); -const load = require('./load'); - -function RESTful() { - this.delete = (url, data, callback) => { - const isFunc = itype.function(data); - - if (!callback && isFunc) { - callback = data; - data = null; - } - - sendRequest({ - method : 'DELETE', - url : FS + url, - data, - callback, - imgPosition : { top: !!data } - }); - }; - - this.patch = (url, data, callback) => { - const isFunc = itype.function(data); - - if (!callback && isFunc) { - callback = data; - data = null; - } - - const imgPosition = { - top: true - }; - - sendRequest({ - method: 'PATCH', - url: FS + url, - data, - callback, - imgPosition, - }); - }; - - this.write = (url, data, callback) => { - const isFunc = itype.function(data); - - if (!callback && isFunc) { - callback = data; - data = null; - } - - sendRequest({ - method: 'PUT', - url: FS + url, - data, - callback, - imgPosition : { top: true } - }); - }; - - this.read = (url, dataType, callback) => { - const isQuery = /\?/.test(url); - const isBeautify = /\?beautify$/.test(url); - const isMinify = /\?minify$/.test(url); - const notLog = !isQuery || isBeautify || isMinify; - const isFunc = itype.function(dataType); - - if (!callback && isFunc) { - callback = dataType; - dataType = 'text'; - } - - sendRequest({ - method: 'GET', - url: FS + url, - callback, - notLog, - dataType, - }); - }; - - this.cp = (data, callback) => { - sendRequest({ - method: 'PUT', - url: '/cp', - data, - callback, - imgPosition : { top: true } - }); - }; - - this.pack = (data, callback) => { - sendRequest({ - method : 'PUT', - url : '/pack', - data, - callback, - }); - }; - - this.extract = function(data, callback) { - sendRequest({ - method : 'PUT', - url : '/extract', - data : data, - callback : callback - }); - }; - - this.mv = function(data, callback) { - sendRequest({ - method : 'PUT', - url : '/mv', - data : data, - callback : callback, - imgPosition : { top: true } - }); - }; - - this.Config = { - read: function(callback) { - sendRequest({ - method : 'GET', - url : '/config', - callback : callback, - imgPosition : { top: true }, - notLog : true - }); - }, - - write: function(data, callback) { - sendRequest({ - method : 'PATCH', - url : '/config', - data : data, - callback : callback, - imgPosition : { top: true } - }); - } - }; - - this.Markdown = { - read : function(url, callback) { - sendRequest({ - method : 'GET', - url : '/markdown' + url, - callback : callback, - imgPosition : { top: true }, - notLog : true - }); - }, - - render : function(data, callback) { - sendRequest({ - method : 'PUT', - url : '/markdown', - data : data, - callback : callback, - imgPosition : { top: true }, - notLog : true - }); - } - }; - - function sendRequest(params) { - const p = params; - const prefixUrl = CloudCmd.PREFIX_URL; - - p.url = prefixUrl + p.url; - p.url = encodeURI(p.url); - - /* - * if we send ajax request - - * no need in hash so we escape # - */ - p.url = p.url.replace('#', '%23'); - - load.ajax({ - method : p.method, - url : p.url, - data : p.data, - dataType : p.dataType, - error : (jqXHR) => { - const response = jqXHR.responseText; - const statusText = jqXHR.statusText; - const status = jqXHR.status; - const text = status === 404 ? response : statusText; - const encoded = encode(text); - - Images.show.error(encoded); - - setTimeout(() => { - DOM.Dialog.alert(CloudCmd.TITLE, encoded); - }, 100); - - p.callback(Error(text)); - }, - success: (data) => { - Images.hide(); - - if (!p.notLog) - CloudCmd.log(data); - - p.callback(null, data); - } - }); - } -} +export const Markdown = { + read: handleError(IO.Markdown.read), + render: handleError(IO.Markdown.render), +}; diff --git a/client/dom/select-by-pattern.js b/client/dom/select-by-pattern.js index 870d722f..0bada6ef 100644 --- a/client/dom/select-by-pattern.js +++ b/client/dom/select-by-pattern.js @@ -1,52 +1,46 @@ -'use strict'; - -/* global DOM */ +import {alert, prompt} from '#dom/dialog'; +import {getRegExp} from '#common/util'; +import {getCurrentName} from './current-file.js'; +import { + isSelected, + toggleSelectedFile, +} from './cmd.js'; let SelectType = '*.*'; -const TITLE = 'Cloud Commander'; -const {getRegExp} = require('../../common/util'); - -module.exports = (msg, files) => { - const allMsg = `Specify file type for ${msg} selection`; - const cancel = false; - const {Dialog} = DOM; +export const selectByPattern = async (msg, files) => { + if (!files) + return; - Dialog.prompt(TITLE, allMsg, SelectType, {cancel}).then((type) => { - SelectType = type; + const allMsg = `Specify file type for ${msg} selection`; + const [cancel, type] = await prompt(allMsg, SelectType); + + if (cancel) + return; + + SelectType = type; + + const regExp = getRegExp(type); + let matches = 0; + + for (const current of files) { + const name = getCurrentName(current); - const regExp = getRegExp(type); + if (name === '..' || !regExp.test(name)) + continue; - if (!files) - return; + ++matches; - let matches = 0; + let selected = isSelected(current); + const shouldSel = msg === 'expand'; - files.forEach((current) => { - const name = DOM.getCurrentName(current); - - if (name === '..') - return; - - const isMatch = regExp.test(name); - - if (!isMatch) - return; - - ++matches; - - let isSelected = DOM.isSelected(current); - const shouldSel = msg === 'expand'; - - if (shouldSel) - isSelected = !isSelected; - - if (isSelected) - DOM.toggleSelectedFile(current); - }); + if (shouldSel) + selected = !selected; - if (!matches) - Dialog.alert('Select Files', 'No matches found!'); - }); + if (selected) + toggleSelectedFile(current); + } + + if (!matches) + alert('No matches found!'); }; - diff --git a/client/dom/storage.js b/client/dom/storage.js index 495c654e..d5cf231f 100644 --- a/client/dom/storage.js +++ b/client/dom/storage.js @@ -1,66 +1,26 @@ -'use strict'; +const {parse, stringify} = JSON; -const itype = require('itype/legacy'); -const jonny = require('jonny/legacy'); -const exec = require('execon'); -const tryCatch = require('try-catch'); -const setItem = localStorage.setItem.bind(localStorage); - -/** remove element */ -module.exports.remove = (item, callback) => { - localStorage.removeItem(item); - exec(callback, null); - - return module.exports; +export const set = (name, data) => { + localStorage.setItem(name, data); }; -module.exports.removeMatch = (string, callback) => { - const reg = RegExp('^' + string + '.*$'); - const test = (a) => reg.test(a); - const remove = (a) => localStorage.removeItem(a); - - Object.keys(localStorage) - .filter(test) - .forEach(remove); - - exec(callback); - - return module.exports; +export const setJson = (name, data) => { + localStorage.setItem(name, stringify(data)); }; -/** если доступен localStorage и - * в нём есть нужная нам директория - - * записываем данные в него - */ -module.exports.set = (name, data, callback) => { - let str, error; - - if (itype.object(data)) - str = jonny.stringify(data); - - if (name) - [error] = tryCatch(setItem, name, str || data); - - exec(callback, error); - - return module.exports; -}, +export const get = (name) => { + return localStorage.getItem(name); +}; -/** Если доступен Storage принимаем из него данные*/ -module.exports.get = (name, callback) => { - const ret = localStorage.getItem(name); - - exec(callback, null, ret); - - return module.exports; -}, +export const getJson = (name) => { + const data = localStorage.getItem(name); + return parse(data); +}; -/** функция чистит весь кэш для всех каталогов*/ -module.exports.clear = (callback) => { +export const clear = () => { localStorage.clear(); - - exec(callback); - - return module.exports; }; +export const remove = (item) => { + localStorage.removeItem(item); +}; diff --git a/client/dom/storage.spec.js b/client/dom/storage.spec.js new file mode 100644 index 00000000..c538952d --- /dev/null +++ b/client/dom/storage.spec.js @@ -0,0 +1,105 @@ +import {test, stub} from 'supertape'; +import * as storage from '#dom/storage'; + +const {stringify} = JSON; + +test('cloudcmd: client: storage: set', async (t) => { + const {localStorage} = globalThis; + const setItem = stub(); + + globalThis.localStorage = { + setItem, + }; + + await storage.set('hello', 'world'); + globalThis.localStorage = localStorage; + + t.calledWith(setItem, ['hello', 'world'], 'should call setItem'); + t.end(); +}); + +test('cloudcmd: client: storage: get', async (t) => { + const {localStorage} = globalThis; + const getItem = stub().returns('world'); + + globalThis.localStorage = { + getItem, + }; + + const result = await storage.get('hello'); + + globalThis.localStorage = localStorage; + + t.equal(result, 'world'); + t.end(); +}); + +test('cloudcmd: client: storage: getJson', async (t) => { + const {localStorage} = globalThis; + const expected = { + hello: 'world', + }; + + const getItem = stub().returns(stringify(expected)); + + globalThis.localStorage = { + getItem, + }; + + const result = await storage.getJson('hello'); + + globalThis.localStorage = localStorage; + + t.deepEqual(result, expected); + t.end(); +}); + +test('cloudcmd: client: storage: setJson', async (t) => { + const {localStorage} = globalThis; + const data = { + hello: 'world', + }; + + const expected = stringify(data); + const setItem = stub(); + + globalThis.localStorage = { + setItem, + }; + + await storage.setJson('hello', data); + globalThis.localStorage = localStorage; + + t.calledWith(setItem, ['hello', expected]); + t.end(); +}); + +test('cloudcmd: client: storage: remove', async (t) => { + const {localStorage} = globalThis; + const removeItem = stub(); + + globalThis.localStorage = { + removeItem, + }; + + await storage.remove('hello'); + globalThis.localStorage = localStorage; + + t.calledWith(removeItem, ['hello'], 'should call removeItem'); + t.end(); +}); + +test('cloudcmd: client: storage: clear', async (t) => { + const {localStorage} = globalThis; + const clear = stub(); + + globalThis.localStorage = { + clear, + }; + + await storage.clear(); + globalThis.localStorage = localStorage; + + t.calledWithNoArgs(clear, 'should call clear'); + t.end(); +}); diff --git a/client/dom/upload-files.js b/client/dom/upload-files.js index accbb203..1c37fe1b 100644 --- a/client/dom/upload-files.js +++ b/client/dom/upload-files.js @@ -1,30 +1,19 @@ -'use strict'; - /* global CloudCmd */ +import {eachSeries} from 'execon'; +import wraptile from 'wraptile'; +import * as load from '#dom/load'; +import {alert} from '#dom/dialog'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {getCurrentDirPath} from './current-file.js'; -const {eachSeries} = require('execon'); -const wraptile = require('wraptile/legacy'); - -const DOM = require('.'); -const load = require('./load'); -const Images = require('./images'); -const {alert} = require('./dialog'); - -const {FS} = require('../../common/cloudfunc'); - -const {TITLE} = CloudCmd; - -const onEnd = wraptile(_onEnd); const loadFile = wraptile(_loadFile); +const onEnd = wraptile(_onEnd); -const { - getCurrentDirPath: getPathWhenRootEmpty -} = DOM; - -module.exports = (dir, files) => { +export const uploadFiles = (dir, files) => { if (!files) { files = dir; - dir = getPathWhenRootEmpty(); + dir = getCurrentDirPath(); } const n = files.length; @@ -32,7 +21,7 @@ module.exports = (dir, files) => { if (!n) return; - const array = [...files]; + const array = Array.from(files); const {name} = files[0]; eachSeries(array, loadFile(dir, n), onEnd(name)); @@ -40,17 +29,17 @@ module.exports = (dir, files) => { function _onEnd(currentName) { CloudCmd.refresh({ - currentName + currentName, }); } function _loadFile(dir, n, file, callback) { let i = 0; - const name = file.name; + const {name} = file; const path = dir + name; - const {PREFIX_URL} = CloudCmd; - const api = PREFIX_URL + FS; + const {prefixURL} = CloudCmd; + const api = prefixURL + FS; const percent = (i, n, per = 100) => { return Math.round(i * per / n); @@ -60,7 +49,8 @@ function _loadFile(dir, n, file, callback) { ++i; - load.put(api + path, file) + load + .put(api + path, file) .on('error', showError) .on('end', callback) .on('progress', (count) => { @@ -73,6 +63,5 @@ function _loadFile(dir, n, file, callback) { } function showError({message}) { - alert(TITLE, message); + alert(message); } - diff --git a/client/get-json-from-file-table.js b/client/get-json-from-file-table.js new file mode 100644 index 00000000..ea90f366 --- /dev/null +++ b/client/get-json-from-file-table.js @@ -0,0 +1,44 @@ +/* global DOM */ +/** + * Функция генерирует JSON из html-таблицы файлов и + * используеться при первом заходе в корень + */ +export const getJsonFromFileTable = () => { + const Info = DOM.CurrentInfo; + const path = DOM.getCurrentDirPath(); + const infoFiles = Info.files || []; + + const files = infoFiles + .filter(notParent) + .map(parse); + + const fileTable = { + path, + files, + }; + + return fileTable; +}; + +const notParent = (current) => { + const name = DOM.getCurrentName(current); + return name !== '..'; +}; + +const parse = (current) => { + const name = DOM.getCurrentName(current); + const size = DOM.getCurrentSize(current); + const owner = DOM.getCurrentOwner(current); + const mode = DOM.getCurrentMode(current); + const date = DOM.getCurrentDate(current); + const type = DOM.getCurrentType(current); + + return { + name, + size, + mode, + owner, + date, + type, + }; +}; diff --git a/client/input.js b/client/input.js deleted file mode 100644 index 2c8c072e..00000000 --- a/client/input.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -const currify = require('currify/legacy'); - -const isType = currify((type, object, name) => { - return typeof object[name] === type; -}); - -const isBool = isType('boolean'); - -module.exports.getElementByName = getElementByName; - -function getElementByName(selector, element) { - const str = `[data-name="js-${selector}"]`; - - return element - .querySelector(str); -} - -module.exports.getName = (element) => { - const name = element - .getAttribute('data-name') - .replace(/^js-/, ''); - - return name; -}; - -module.exports.convert = (config) => { - const result = Object.assign({}, config); - const array = Object.keys(result); - - array - .filter(isBool(result)) - .forEach((name) => { - const item = result[name]; - - result[name] = setState(item); - }); - - return result; -}; - -function setState(state) { - if (state) - return ' checked'; - - return ''; -} - -module.exports.getValue = (name, element) => { - const el = getElementByName(name, element); - const type = el.type; - - switch(type) { - case 'checkbox': - return el.checked; - - case 'number': - return Number(el.value); - - default: - return el.value; - } -}; - -module.exports.setValue = (name, value, element) => { - const el = getElementByName(name, element); - const type = el.type; - - switch(type) { - case 'checkbox': - el.checked = value; - break; - - default: - el.value = value; - break; - } -}; - diff --git a/client/key/binder.js b/client/key/binder.js new file mode 100644 index 00000000..6aec97ea --- /dev/null +++ b/client/key/binder.js @@ -0,0 +1,15 @@ +export const createBinder = () => { + let binded = false; + + return { + isBind() { + return binded; + }, + setBind() { + binded = true; + }, + unsetBind() { + binded = false; + }, + }; +}; diff --git a/client/key/binder.spec.js b/client/key/binder.spec.js new file mode 100644 index 00000000..f9d54fdd --- /dev/null +++ b/client/key/binder.spec.js @@ -0,0 +1,28 @@ +import {test} from 'supertape'; +import {createBinder} from './binder.js'; + +test('client: key: binder: isBind: default', (t) => { + const binder = createBinder(); + + t.notOk(binder.isBind(), 'should not be bind by default'); + t.end(); +}); + +test('client: key: binder: setBind', (t) => { + const binder = createBinder(); + + binder.setBind(); + + t.ok(binder.isBind(), 'should be bind'); + t.end(); +}); + +test('client: key: binder: unsetBind', (t) => { + const binder = createBinder(); + + binder.setBind(); + binder.unsetBind(); + + t.notOk(binder.isBind(), 'should not be bind'); + t.end(); +}); diff --git a/client/key/index.js b/client/key/index.js index 83d75806..283a7f11 100644 --- a/client/key/index.js +++ b/client/key/index.js @@ -1,480 +1,544 @@ /* global CloudCmd, DOM */ +import clipboard from '@cloudcmd/clipboard'; +import {fullstore} from 'fullstore'; +import * as Events from '#dom/events'; +import * as Buffer from '../dom/buffer.js'; +import * as KEY from './key.js'; +import _vim from './vim/index.js'; +import setCurrentByChar from './set-current-by-char.js'; +import {createBinder} from './binder.js'; -'use strict'; - -const Info = DOM.CurrentInfo; - -const exec = require('execon'); -const clipboard = require('@cloudcmd/clipboard'); -const wraptile = require('wraptile/legacy'); - -const Events = require('../dom/events'); -const Buffer = require('../dom/buffer'); -const KEY = require('./key'); -const vim = require('./vim'); -const setCurrentByChar = require('./set-current-by-char'); -const fullstore = require('fullstore/legacy'); const Chars = fullstore(); +const toggleVim = (keyCode, overrides = {}) => { + const {_config, config} = overrides; + + if (!config('vim') && keyCode === KEY.ESC) + _config('vim', true); +}; + +const isUndefined = (a) => typeof a === 'undefined'; + Chars([]); -KeyProto.prototype = KEY; -CloudCmd.Key = KeyProto; +const {assign} = Object; +const binder = createBinder(); -function KeyProto() { - let Binded; +const bind = () => { + Events.addKey(listener, true); + binder.setBind(); +}; + +export const Key = assign(binder, KEY, { + bind, +}); + +export const _listener = listener; + +function getChar(event) { + /* + * event.keyIdentifier deprecated in chrome v51 + * but event.key is absent in chrome <= v51 + */ + const { + key, + shift, + keyCode, + keyIdentifier, + } = event; - const Key = this; + const char = key || fromCharCode(keyIdentifier); + const symbol = getSymbol(shift, keyCode); - this.isBind = () => { - return Binded; - }; - - this.setBind = () => { - Binded = true; - }; - - this.unsetBind = () => { - Binded = false; - }; - - this.bind = () => { - Events.addKey(listener); - Binded = true; - }; - - function getChar(event) { - /* - * event.keyIdentifier deprecated in chrome v51 - * but event.key is absent in chrome <= v51 - */ - - if (event.key) - return event.key; - - return fromCharCode(event.keyIdentifier); - } - - function listener(event) { - const keyCode = event.keyCode; - const alt = event.altKey; - const ctrl = event.ctrlKey; - const shift = event.shiftKey; - const meta = event.metaKey; - const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z; - const isNumpad = /Numpad/.test(event.code); - - let char = getChar(event); - let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char); - - if (!isSymbol) { - isSymbol = getSymbol(shift, keyCode); - - if (isSymbol) - char = isSymbol; - } - - if (!Key.isBind()) - return; - - const isVim = CloudCmd.config('vim'); - - if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol)) - return setCurrentByChar(char, Chars); - - Chars([]); - switchKey(event); - - if (isVim) - vim(char, event); - } - - function getSymbol(shift, keyCode) { - switch (keyCode) { - case KEY.DOT: - return '.'; - - case KEY.HYPHEN: - return shift ? '_' : '-'; - - case KEY.EQUAL: - return shift ? '+' : '='; - } - } - - function fromCharCode(keyIdentifier) { - const code = keyIdentifier.substring(2); - const hex = parseInt(code, 16); - const char = String.fromCharCode(hex); - - return char; - } - - function switchKey(event) { - let i, isSelected, prev, next; - let current = Info.element; - let dataName; - - const name = Info.name; - - const {Operation} = CloudCmd; - const panel = Info.panel; - const path = Info.path; - const isDir = Info.isDir; - - const keyCode = event.keyCode; - const alt = event.altKey; - const shift = event.shiftKey; - const ctrl = event.ctrlKey; - const meta = event.metaKey; - const ctrlMeta = ctrl || meta; - - if (current) { - prev = current.previousSibling; - next = current.nextSibling; - } - - switch (keyCode) { - case Key.TAB: - DOM.changePanel(); - event.preventDefault(); - break; - - case Key.INSERT: - DOM .toggleSelectedFile(current) - .setCurrentFile(next); - break; - - case Key.INSERT_MAC: - DOM .toggleSelectedFile(current) - .setCurrentFile(next); - break; - - case Key.DELETE: - if (shift) - Operation.show('delete:silent'); - else - Operation.show('delete'); - break; - - case Key.ASTERISK: - DOM.toggleAllSelectedFiles(current); - break; - - case Key.PLUS: - DOM.expandSelection(); - event.preventDefault(); - break; - - case Key.MINUS: - DOM.shrinkSelection(); - event.preventDefault(); - break; - - case Key.F1: - CloudCmd.Help.show(); - event.preventDefault(); - break; - - case Key.F2: - DOM.renameCurrent(current); - break; - - case Key.F3: - if (shift) - CloudCmd.Markdown.show(path); - else if (ctrlMeta) - CloudCmd.sortPanel('name'); - else - CloudCmd.View.show(); - - event.preventDefault(); - break; - - case Key.F4: - if (shift) - CloudCmd.EditFileVim.show(); - else - CloudCmd.EditFile.show(); - - event.preventDefault(); - break; - - case Key.F5: - if (ctrlMeta) - CloudCmd.sortPanel('date'); - else if (alt) - Operation.show('pack'); - else - Operation.show('copy'); - - event.preventDefault(); - break; - - case Key.F6: - if (ctrlMeta) - CloudCmd.sortPanel('size'); - else - Operation.show('move'); - - event.preventDefault(); - break; - - case Key.F7: - if (shift) - DOM.promptNewFile(); - else - DOM.promptNewDir(); - - event.preventDefault(); - break; - - case Key.F8: - Operation.show('delete'); - event.preventDefault(); - break; - - case Key.F9: - if (alt) - Operation.show('extract'); - else - CloudCmd.Menu.show(); - event.preventDefault(); - break; - - case Key.F10: - CloudCmd.Config.show(); - event.preventDefault(); - break; - - case Key.TRA: - event.preventDefault(); - - if (shift) - return CloudCmd.Terminal.show(); - - CloudCmd.Konsole.show(); - break; - - case KEY.BRACKET_CLOSE: - CloudCmd.Konsole.show(); - event.preventDefault(); - break; - - case Key.SPACE: - if (!isDir || name === '..') - isSelected = true; - else - isSelected = DOM.isSelected(current); - - exec.if(isSelected, () => { - DOM.toggleSelectedFile(current); - }, (callback) => { - DOM.loadCurrentSize(callback, current); - }); - - event.preventDefault(); - break; - - case Key.U: - if (ctrlMeta) { - DOM.swapPanels(); - event.preventDefault(); - } - break; - - /* navigation on file table: * - * in case of pressing button 'up', * - * select previous row */ - case Key.UP: - if (shift) - DOM.toggleSelectedFile(current); - - DOM.setCurrentFile(prev); - event.preventDefault(); - break; - - /* in case of pressing button 'down', * - * select next row */ - case Key.DOWN: - if (shift) - DOM.toggleSelectedFile(current); - - DOM.setCurrentFile(next); - event.preventDefault(); - break; - - case Key.LEFT: - if (!alt) - return; - - event.preventDefault(); - - dataName = Info.panel.getAttribute('data-name'); - - if (dataName === 'js-right') - DOM.duplicatePanel(); - - break; - - case Key.RIGHT: - if (!alt) - return; - - event.preventDefault(); - - dataName = Info.panel.getAttribute('data-name'); - - if (dataName === 'js-left') - DOM.duplicatePanel(); - - break; - - /* in case of pressing button 'Home', * - * go to top element */ - case Key.HOME: - DOM.setCurrentFile(Info.first); - event.preventDefault(); - break; - - /* in case of pressing button 'End', select last element */ - case Key.END: - DOM.setCurrentFile(Info.last); - event.preventDefault(); - break; - - /* если нажали клавишу page down проматываем экран */ - case Key.PAGE_DOWN: - DOM.scrollByPages(panel, 1); - - for (i = 0; i < 30; i++) { - if (!current.nextSibling) - break; - - current = current.nextSibling; - } - - DOM.setCurrentFile(current); - event.preventDefault(); - break; - - /* если нажали клавишу page up проматываем экран */ - case Key.PAGE_UP: - DOM.scrollByPages(panel, -1); - - for (i = 0; i < 30; i++) { - if (!current.previousSibling) - break; - - current = current.previousSibling; - } - - DOM.setCurrentFile(current); - event.preventDefault(); - break; - - /* open directory */ - case Key.ENTER: - if (Info.isDir) - CloudCmd.loadDir({ - path: path === '/' ? '/' : path + '/' - }); - break; - - case Key.BACKSPACE: - CloudCmd.goToParentDir(); - event.preventDefault(); - break; - - case Key.BACKSLASH: - if (ctrlMeta) - CloudCmd.loadDir({ - path: '/' - }); - break; - - case Key.A: - if (ctrlMeta) { - DOM.selectAllFiles(); - event.preventDefault(); - } - - break; - - case Key.G: - if (alt) { - DOM.goToDirectory(); - event.preventDefault(); - } - - break; - - case Key.M: - if (ctrlMeta) { - if (shift) - CloudCmd.EditNamesVim.show(); - else - CloudCmd.EditNames.show(); - - event.preventDefault(); - } - - break; - - case Key.P: - if (!ctrlMeta) - return; - - event.preventDefault(); - clipboard - .writeText(Info.dirPath) - .catch(CloudCmd.log); - - break; - /** - * обновляем страницу, - * загружаем содержимое каталога - * при этом данные берём всегда с - * сервера, а не из кэша - * (обновляем кэш) - */ - case Key.R: - if (ctrlMeta) { - CloudCmd.log('reloading page...\n'); - CloudCmd.refresh(); - event.preventDefault(); - } - break; - - case Key.C: - if (ctrlMeta) - Buffer.copy(); - break; - - case Key.X: - if (ctrlMeta) - Buffer.cut(); - break; - - case Key.V: - if (ctrlMeta) - Buffer.paste(); - break; - - case Key.Z: - if (ctrlMeta) - Buffer.clear(); - break; - - /* чистим хранилище */ - case Key.D: - if (ctrlMeta) { - CloudCmd.log('clearing storage...'); - DOM.Storage.clear(wraptile(CloudCmd.log, 'storage cleared')); - event.preventDefault(); - } - break; - } - } + return [symbol, char]; } +async function listener(event, overrides = {}) { + const { + config = CloudCmd.config, + _config = CloudCmd._config, + switchKey = _switchKey, + vim = _vim, + } = overrides; + + const {keyCode} = event; + + // strange chrome bug calls listener twice + // in second time event misses a lot fields + if (isUndefined(event.altKey)) + return; + + const alt = event.altKey; + const ctrl = event.ctrlKey; + const meta = event.metaKey; + const isBetween = keyCode >= KEY.ZERO && keyCode <= KEY.Z; + const isNumpad = /Numpad/.test(event.code); + + const [symbol, char] = getChar(event); + + if (!binder.isBind()) + return; + + toggleVim(keyCode, { + config, + _config, + }); + + const isVim = config('vim'); + + if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || symbol)) + return setCurrentByChar(char, Chars); + + Chars([]); + await switchKey(event); + + if (keyCode >= KEY.F1 && keyCode <= KEY.F10) + return; + + if (isVim) + vim(char, event); +} + +function getSymbol(shift, keyCode) { + switch(keyCode) { + case KEY.DOT: + return '.'; + + case KEY.HYPHEN: + return shift ? '_' : '-'; + + case KEY.EQUAL: + return shift ? '+' : '='; + } + + return ''; +} + +function fromCharCode(keyIdentifier) { + const code = keyIdentifier.substring(2); + const hex = parseInt(code, 16); + + return String.fromCharCode(hex); +} + +async function _switchKey(event) { + const Info = DOM.CurrentInfo; + let i; + let isSelected; + let prev; + let next; + let current = Info.element; + let dataName; + + const { + name, + panel, + path, + isDir, + } = Info; + + const { + Operation, + changeDir, + config, + } = CloudCmd; + + const {keyCode} = event; + + const alt = event.altKey; + const shift = event.shiftKey; + const ctrl = event.ctrlKey; + const meta = event.metaKey; + const ctrlMeta = ctrl || meta; + + if (current) { + prev = current.previousSibling; + next = current.nextSibling; + } + + switch(keyCode) { + case KEY.TAB: + DOM.changePanel(); + event.preventDefault(); + break; + + case KEY.INSERT: + DOM + .toggleSelectedFile(current) + .setCurrentFile(next); + break; + + case KEY.INSERT_MAC: + DOM + .toggleSelectedFile(current) + .setCurrentFile(next); + break; + + case KEY.DELETE: + if (shift) + Operation.show('delete:silent'); + else + Operation.show('delete'); + + break; + + case KEY.ASTERISK: + DOM.toggleAllSelectedFiles(current); + break; + + case KEY.PLUS: + DOM.expandSelection(); + event.preventDefault(); + break; + + case KEY.MINUS: + DOM.shrinkSelection(); + event.preventDefault(); + break; + + case KEY.F1: + CloudCmd.Help.show(); + event.preventDefault(); + break; + + case KEY.F2: + CloudCmd.UserMenu.show(); + break; + + case KEY.F3: + event.preventDefault(); + + if (Info.isDir) + await changeDir(path); + else if (shift) + CloudCmd.View.show(null, { + raw: true, + }); + else if (ctrlMeta) + CloudCmd.sortPanel('name'); + else + CloudCmd.View.show(); + + break; + + case KEY.F4: + if (config('vim')) + CloudCmd.EditFileVim.show(); + else + CloudCmd.EditFile.show(); + + event.preventDefault(); + break; + + case KEY.F5: + if (ctrlMeta) + CloudCmd.sortPanel('date'); + else if (alt) + Operation.show('pack'); + else + Operation.show('copy'); + + event.preventDefault(); + break; + + case KEY.F6: + if (ctrlMeta) + CloudCmd.sortPanel('size'); + else if (shift) + DOM.renameCurrent(current); + else + Operation.show('move'); + + event.preventDefault(); + break; + + case KEY.F7: + if (shift) + DOM.promptNewFile(); + else + DOM.promptNewDir(); + + event.preventDefault(); + break; + + case KEY.F8: + Operation.show('delete'); + event.preventDefault(); + break; + + case KEY.F9: + if (alt) + Operation.show('extract'); + else + CloudCmd.Menu.show(); + + event.preventDefault(); + break; + + case KEY.F10: + CloudCmd.Config.show(); + event.preventDefault(); + break; + + case KEY.TRA: + event.preventDefault(); + + if (shift) + return CloudCmd.Terminal.show(); + + CloudCmd.Konsole.show(); + break; + + case KEY.BRACKET_CLOSE: + CloudCmd.Konsole.show(); + event.preventDefault(); + break; + + case KEY.SPACE: + event.preventDefault(); + + if (!isDir || name === '..') + isSelected = true; + else + isSelected = DOM.isSelected(current); + + if (!isSelected) + await DOM.loadCurrentSize(current); + + DOM.toggleSelectedFile(current); + + break; + + case KEY.U: + if (ctrlMeta) { + DOM.swapPanels(); + event.preventDefault(); + } + + break; + + /* navigation on file table: * + * in case of pressing button 'up', * + * select previous row */ + case KEY.UP: + if (shift) + DOM.toggleSelectedFile(current); + + DOM.setCurrentFile(prev); + event.preventDefault(); + break; + + /* in case of pressing button 'down', * + * select next row */ + case KEY.DOWN: + if (shift) + DOM.toggleSelectedFile(current); + + DOM.setCurrentFile(next); + event.preventDefault(); + break; + + case KEY.LEFT: + if (!alt) + return; + + event.preventDefault(); + + dataName = Info.panel.getAttribute('data-name'); + + if (dataName === 'js-right') + DOM.duplicatePanel(); + + break; + + case KEY.RIGHT: + if (!alt) + return; + + event.preventDefault(); + + dataName = Info.panel.getAttribute('data-name'); + + if (dataName === 'js-left') + DOM.duplicatePanel(); + + break; + + /* in case of pressing button 'Home', * + * go to top element */ + case KEY.HOME: + DOM.setCurrentFile(Info.first); + event.preventDefault(); + break; + + /* in case of pressing button 'End', select last element */ + case KEY.END: + DOM.setCurrentFile(Info.last); + event.preventDefault(); + break; + + /* если нажали клавишу page down проматываем экран */ + case KEY.PAGE_DOWN: + DOM.scrollByPages(panel, 1); + + for (i = 0; i < 30; i++) { + if (!current.nextSibling) + break; + + current = current.nextSibling; + } + + DOM.setCurrentFile(current); + event.preventDefault(); + break; + + /* если нажали клавишу page up проматываем экран */ + case KEY.PAGE_UP: + DOM.scrollByPages(panel, -1); + + for (i = 0; i < 30; i++) { + if (!current.previousSibling) + break; + + current = current.previousSibling; + } + + DOM.setCurrentFile(current); + event.preventDefault(); + break; + + case KEY.ENTER: + if (Info.isDir) + await changeDir(path); + else + CloudCmd.View.show(); + + break; + + case KEY.BACKSPACE: + CloudCmd.goToParentDir(); + event.preventDefault(); + break; + + case KEY.BACKSLASH: + if (ctrlMeta) + await changeDir('/'); + + break; + + case KEY.A: + if (ctrlMeta) { + DOM.selectAllFiles(); + event.preventDefault(); + } + + break; + + case KEY.G: + if (alt) { + DOM.goToDirectory(); + event.preventDefault(); + } + + break; + + case KEY.L: + if (ctrlMeta && shift) { + CloudCmd.logOut(); + event.preventDefault(); + } + + break; + + case KEY.M: + if (ctrlMeta) { + if (config('vim')) + CloudCmd.EditNamesVim.show(); + else + CloudCmd.EditNames.show(); + + event.preventDefault(); + } + + break; + + case KEY.P: + if (!ctrlMeta) + return; + + event.preventDefault(); + clipboard + .writeText(Info.dirPath) + .catch(CloudCmd.log); + + break; + + /** + * обновляем страницу, + * загружаем содержимое каталога + * при этом данные берём всегда с + * сервера, а не из кэша + * (обновляем кэш) + */ + case KEY.R: + if (ctrlMeta) { + CloudCmd.log('reloading page...\n'); + CloudCmd.refresh(); + event.preventDefault(); + } + + break; + + case KEY.C: + if (ctrlMeta) + Buffer.copy(); + + break; + + case KEY.X: + if (ctrlMeta) + Buffer.cut(); + + break; + + case KEY.V: + if (ctrlMeta) + Buffer.paste(); + + break; + + case KEY.Z: + if (ctrlMeta) + Buffer.clear(); + + break; + + case KEY.COLON: + CloudCmd.CommandLine.show(); + event.preventDefault(); + break; + + /* чистим хранилище */ + case KEY.D: + if (ctrlMeta) { + CloudCmd.log('clearing storage...'); + await DOM.Storage.clear(); + CloudCmd.log('storage cleared'); + event.preventDefault(); + } + + break; + + case KEY.DOT: + if (meta && shift) { + const showDotFiles = !CloudCmd.config('showDotFiles'); + CloudCmd._config('showDotFiles', showDotFiles); + CloudCmd.refresh(); + await DOM.RESTful.Config.write({ + showDotFiles, + }); + } + + break; + } +} diff --git a/client/key/index.spec.js b/client/key/index.spec.js new file mode 100644 index 00000000..bec92b31 --- /dev/null +++ b/client/key/index.spec.js @@ -0,0 +1,56 @@ +import autoGlobals from 'auto-globals'; +import supertape from 'supertape'; +import {ESC} from './key.js'; +import {Key, _listener} from './index.js'; +import {getDOM, getCloudCmd} from './vim/globals.fixture.js'; + +const test = autoGlobals(supertape); +const {stub} = supertape; + +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); + +test('cloudcmd: client: key: enable vim', async (t) => { + const vim = stub(); + const config = stub().returns(true); + const _config = stub(); + + const event = { + keyCode: ESC, + key: 'Escape', + altKey: false, + }; + + Key.setBind(); + + await _listener(event, { + vim, + config, + _config, + switchKey: stub(), + }); + + t.calledWith(vim, ['Escape', event]); + t.end(); +}); + +test('cloudcmd: client: key: disable vim', async (t) => { + const _config = stub(); + const config = stub(); + + const event = { + keyCode: ESC, + key: 'Escape', + altKey: false, + }; + + Key.setBind(); + await _listener(event, { + config, + _config, + switchKey: stub(), + }); + + t.calledWith(_config, ['vim', true]); + t.end(); +}); diff --git a/client/key/key.js b/client/key/key.js index 197e521e..6772e34c 100644 --- a/client/key/key.js +++ b/client/key/key.js @@ -1,81 +1,58 @@ -'use strict'; - -module.exports = { - BACKSPACE : 8, - TAB : 9, - ENTER : 13, - ESC : 27, - - SPACE : 32, - PAGE_UP : 33, - PAGE_DOWN : 34, - END : 35, - HOME : 36, - - LEFT : 37, - UP : 38, - RIGHT : 39, - DOWN : 40, - - INSERT : 45, - DELETE : 46, - - ZERO : 48, - - SEMICOLON : 52, - - COLON : 54, - - A : 65, - - C : 67, - D : 68, - - G : 71, - - J : 74, - K : 75, - - M : 77, - - O : 79, - P : 80, - Q : 81, - R : 82, - S : 83, - T : 84, - U : 85, - - V : 86, - - X : 88, - - Z : 90, - - INSERT_MAC : 96, - - ASTERISK : 106, - PLUS : 107, - MINUS : 109, - - F1 : 112, - F2 : 113, - F3 : 114, - F4 : 115, - F5 : 116, - F6 : 117, - F7 : 118, - F8 : 119, - F9 : 120, - F10 : 121, - - EQUAL : 187, - HYPHEN : 189, - DOT : 190, - SLASH : 191, - TRA : 192, /* Typewritten Reverse Apostrophe (`) */ - BACKSLASH : 220, - - BRACKET_CLOSE: 221 -}; - +export const BACKSPACE = 8; +export const TAB = 9; +export const ENTER = 13; +export const CAPSLOCK = 20; +export const ESC = 27; +export const SPACE = 32; +export const PAGE_UP = 33; +export const PAGE_DOWN = 34; +export const END = 35; +export const HOME = 36; +export const LEFT = 37; +export const UP = 38; +export const RIGHT = 39; +export const DOWN = 40; +export const INSERT = 45; +export const DELETE = 46; +export const ZERO = 48; +export const SEMICOLON = 52; +export const A = 65; +export const C = 67; +export const D = 68; +export const G = 71; +export const J = 74; +export const K = 75; +export const L = 76; +export const M = 77; +export const O = 79; +export const P = 80; +export const Q = 81; +export const R = 82; +export const S = 83; +export const T = 84; +export const U = 85; +export const V = 86; +export const X = 88; +export const Z = 90; +export const INSERT_MAC = 96; +export const ASTERISK = 106; +export const PLUS = 107; +export const MINUS = 109; +export const F1 = 112; +export const F2 = 113; +export const F3 = 114; +export const F4 = 115; +export const F5 = 116; +export const F6 = 117; +export const F7 = 118; +export const F8 = 119; +export const F9 = 120; +export const F10 = 121; +export const COLON = 186; +export const EQUAL = 187; +export const HYPHEN = 189; +export const DOT = 190; +export const SLASH = 191; +export const TRA = 192; +export const BACKSLASH = 220; +export const BRACKET_CLOSE = 221; diff --git a/client/key/set-current-by-char.js b/client/key/set-current-by-char.js index d010fa86..92764f11 100644 --- a/client/key/set-current-by-char.js +++ b/client/key/set-current-by-char.js @@ -1,23 +1,20 @@ /* global DOM */ +import {escapeRegExp} from '#common/util'; -'use strict'; - -const Info = DOM.CurrentInfo; -const {escapeRegExp} = require('../../common/util'); - -module.exports = function setCurrentByChar(char, charStore) { +export default function setCurrentByChar(char, charStore) { + const Info = DOM.CurrentInfo; let firstByName; let skipCount = 0; - let setted = false; + let set = false; let i = 0; const escapeChar = escapeRegExp(char); - const regExp = new RegExp('^' + escapeChar + '.*$', 'i'); + const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); const {files} = Info; const chars = charStore(); const n = chars.length; - while(i < n && char === chars[i]) + while (i < n && char === chars[i]) i++; if (!i) @@ -31,19 +28,21 @@ module.exports = function setCurrentByChar(char, charStore) { const isTest = (a) => regExp.test(a); const isRoot = (a) => a === '..'; const not = (f) => (a) => !f(a); + const setCurrent = (name) => { const byName = DOM.getCurrentByName(name); if (!skipCount) { - setted = true; + set = true; DOM.setCurrentFile(byName); - return true; - } else { - if (skipN === skipCount) - firstByName = byName; - --skipCount; + return true; } + + if (skipN === skipCount) + firstByName = byName; + + --skipCount; }; names @@ -51,9 +50,8 @@ module.exports = function setCurrentByChar(char, charStore) { .filter(not(isRoot)) .some(setCurrent); - if (!setted) { + if (!set) { DOM.setCurrentFile(firstByName); charStore([char]); } -}; - +} diff --git a/client/key/vim/find.js b/client/key/vim/find.js index bbf1d380..e8db825a 100644 --- a/client/key/vim/find.js +++ b/client/key/vim/find.js @@ -1,41 +1,36 @@ -'use strict'; -/* global DOM */ - -const fullstore = require('fullstore/legacy'); -const limier = require('limier/legacy'); -const Info = DOM.CurrentInfo; +import {fullstore} from 'fullstore'; +import limier from 'limier'; const searchStore = fullstore([]); const searchIndex = fullstore(0); -module.exports.find = (value) => { - const names = Info.files.map(DOM.getCurrentName); +export const find = (value, names) => { const result = limier(value, names); searchStore(result); searchIndex(0); - DOM.setCurrentByName(result[0]); + return result; }; -module.exports.findNext = () => { +export const findNext = () => { const names = searchStore(); const index = next(searchIndex(), names.length); searchIndex(index); - DOM.setCurrentByName(names[searchIndex()]); + return names[searchIndex()]; }; -module.exports.findPrevious = () => { +export const findPrevious = () => { const names = searchStore(); const index = previous(searchIndex(), names.length); searchIndex(index); - DOM.setCurrentByName(names[index]); + return names[index]; }; -module.exports._next = next; -module.exports._previous = previous; +export const _next = next; +export const _previous = previous; function next(index, length) { if (index === length - 1) @@ -50,4 +45,3 @@ function previous(index, length) { return --index; } - diff --git a/client/key/vim/find.spec.js b/client/key/vim/find.spec.js new file mode 100644 index 00000000..191a3652 --- /dev/null +++ b/client/key/vim/find.spec.js @@ -0,0 +1,55 @@ +import test from 'supertape'; +import {getDOM} from './globals.fixture.js'; +import { + _next, + _previous, + find, + findNext, + findPrevious, +} from './find.js'; + +globalThis.DOM = getDOM(); + +test('cloudcmd: client: vim: _next', (t) => { + const result = _next(1, 2); + + t.notOk(result, 'should return 0'); + t.end(); +}); + +test('cloudcmd: client: vim: _next: increment', (t) => { + const result = _next(0, 2); + + t.equal(result, 1, 'should return 1'); + t.end(); +}); + +test('cloudcmd: client: vim: _previous', (t) => { + const result = _previous(0, 2); + + t.equal(result, 1, 'should return 1'); + t.end(); +}); + +test('cloudcmd: client: vim: _previous: decrement', (t) => { + const result = _previous(1, 2); + + t.equal(result, 0, 'should return 0'); + t.end(); +}); + +test('cloudcmd: client: vim: findNext: after find', (t) => { + find('a', ['alpha', 'beta', 'apple']); + const result = findNext(); + + t.equal(result, 'beta', 'should return next found name'); + t.end(); +}); + +test('cloudcmd: client: vim: findPrevious: after find', (t) => { + find('a', ['alpha', 'beta', 'apple']); + const result = findPrevious(); + + t.equal(result, 'apple', 'should return previous found name'); + t.end(); +}); diff --git a/test/client/key/vim/globals.js b/client/key/vim/globals.fixture.js similarity index 62% rename from test/client/key/vim/globals.js rename to client/key/vim/globals.fixture.js index 81994ecf..c49894d7 100644 --- a/test/client/key/vim/globals.js +++ b/client/key/vim/globals.fixture.js @@ -1,20 +1,19 @@ -'use strict'; +const noop = () => {}; -module.exports.getDOM = () => { - const resolve = Promise.resolve.bind(Promise); +export const getDOM = () => { + const prompt = Promise.resolve.bind(Promise); const CurrentInfo = { element: {}, files: [], }; - const noop = () => {}; const Buffer = { copy: noop, paste: noop, }; const Dialog = { - prompt: resolve + prompt, }; return { @@ -28,16 +27,20 @@ module.exports.getDOM = () => { getCurrentName: noop, setCurrentByName: noop, toggleSelectedFile: noop, + prompNewDirectory: noop, + promptNewFile: noop, }; }; -module.exports.getCloudCmd = () => { +export const getCloudCmd = () => { const show = () => {}; return { - Operation: { - show - } + Operation: { + show, + }, + + config: noop, + _config: noop, }; }; - diff --git a/client/key/vim/index.js b/client/key/vim/index.js index 1c14d764..981f6f19 100644 --- a/client/key/vim/index.js +++ b/client/key/vim/index.js @@ -1,199 +1,167 @@ -'use strict'; -/* global CloudCmd, DOM */ +import vim from './vim.js'; +import * as finder from './find.js'; +import { + setCurrent, + selectFileNotParent, +} from './set-current.js'; -const KEY = require('../key'); -const Info = DOM.CurrentInfo; -const Dialog = DOM.Dialog; - -const fullstore = require('fullstore/legacy'); -const store = fullstore(''); -const visual = fullstore(false); -const { - find, - findNext, - findPrevious, -} = require('./find'); - -const TITLE = 'Cloud Commander'; - -const stopVisual = () => { - visual(false); +export default (key, event, overrides = {}) => { + const defaults = { + ...globalThis.DOM, + ...globalThis.CloudCmd, + }; + + const deps = { + ...defaults, + ...overrides, + }; + + const operations = getOperations(event, deps); + + vim(key, operations, deps); }; -const end = () => { - store(''); +const getOperations = (event, deps) => { + const { + Info = globalThis.DOM.CurrentInfo, + CloudCmd = globalThis.CloudCmd, + Operation, + unselectFiles, + setCurrentFile, + setCurrentByName, + getCurrentName, + prompt = globalThis.DOM.Dialog.prompt, + preventDefault = event?.preventDefault?.bind(event), + stopImmediatePropagation = event?.preventDefault?.bind(event), + promptNewFile = globalThis.DOM.promptNewFile, + toggleSelectedFile, + Buffer = {}, + createFindNext = _createFindNext, + createFindPrevious = _createFindPrevious, + createMakeFile = _createMakeFile, + renameCurrent, + } = deps; + + return { + makeFile: createMakeFile({ + promptNewFile, + preventDefault, + stopImmediatePropagation, + }), + findNext: createFindNext({ + setCurrentByName, + }), + findPrevious: createFindPrevious({ + setCurrentByName, + }), + escape: unselectFiles, + rename: () => { + event.preventDefault(); + renameCurrent(); + }, + remove: () => { + Operation.show('delete'); + }, + operationCopy: () => { + event.preventDefault(); + Operation.show('copy'); + }, + operationMove: () => { + event.preventDefault(); + Operation.show('move'); + }, + + makeDirectory: () => { + event.stopImmediatePropagation(); + event.preventDefault(); + globalThis.DOM.promptNewDir(); + }, + + terminal: () => { + CloudCmd.Terminal.show(); + }, + + edit: () => { + CloudCmd.EditFileVim.show(); + }, + + copy: () => { + Buffer.copy(); + unselectFiles(); + }, + + select: () => { + const current = Info.element; + toggleSelectedFile(current); + }, + + paste: Buffer.paste, + + moveNext: ({count, isVisual, isDelete}) => { + setCurrent('next', { + count, + isVisual, + isDelete, + }, { + Info, + setCurrentFile, + unselectFiles, + Operation, + }); + }, + + movePrevious: ({count, isVisual, isDelete}) => { + setCurrent('previous', { + count, + isVisual, + isDelete, + }, { + Info, + setCurrentFile, + unselectFiles, + Operation, + }); + }, + + find: async () => { + preventDefault(); + const [, value] = await prompt('Find', ''); + + if (!value) + return; + + const names = Info.files.map(getCurrentName); + const [result] = finder.find(value, names); + + setCurrentByName(result); + }, + }; }; -const rmFirst = (a) => { - return a - .split('') - .slice(1) - .join(''); +export const selectFile = selectFileNotParent; + +const _createFindPrevious = (overrides = {}) => () => { + const {setCurrentByName} = overrides; + const name = finder.findPrevious(); + + setCurrentByName(name); }; -module.exports = (key, event) => { - const current = Info.element; - const keyCode = event.keyCode; - const prevStore = store(); +const _createFindNext = (overrides = {}) => () => { + const {setCurrentByName} = overrides; + const name = finder.findNext(); - const value = store(prevStore.concat(key)); - - if (keyCode === KEY.ENTER) - return end(); - - if (keyCode === KEY.ESC) { - DOM.unselectFiles(); - visual(false); - return end(); - } - - if (key === 'j') { - move('next', { - prevStore, - current, - }); - - return end(); - } - - if (key === 'k') { - move('previous', { - prevStore, - current, - }); - - return end(); - } - - if (/gg/.test(value)) { - move('previous', { - current, - prevStore, - max: Infinity, - }); - - return end(); - } - - if (key === 'd' && (visual() || prevStore === 'd')) { - CloudCmd.Operation.show('delete'); - stopVisual(); - return end(); - } - - if (key === 'G') { - move('next', { - current, - prevStore, - max: Infinity, - }); - - return end(); - } - - if (key === 'y') { - if (!visual()) - return end(); - - DOM.Buffer.copy(); - stopVisual(); - DOM.unselectFiles(); - return end(); - } - - if (/^p$/i.test(key)) { - DOM.Buffer.paste(); - return end(); - } - - if (/^v$/i.test(key)) { - DOM.toggleSelectedFile(current); - visual(!visual()); - - return end(); - } - - if (key === '/') { - event.preventDefault(); - - Dialog.prompt(TITLE, 'Find', '', {cancel: false}) - .then(find); - - return end(); - } - - if (key === 'n') { - findNext(); - return end(); - } - - if (key === 'N') { - findPrevious(); - return end(); - } + setCurrentByName(name); }; -module.exports.selectFile = selectFile; - -function move(sibling, {max, current, prevStore}) { - const isDelete = prevStore[0] === 'd'; +const _createMakeFile = (overrides = {}) => () => { + const { + promptNewFile, + stopImmediatePropagation, + preventDefault, + } = overrides; - if (isDelete) { - visual(true); - prevStore = rmFirst(prevStore); - } - - const n = max || getNumber(prevStore); - - if (isNaN(n)) - return; - - setCurrent({ - n, - current, - sibling, - visual: visual(), - }); - - if (isDelete) - CloudCmd.Operation.show('delete'); -} - -function getNumber(value) { - if (!value) - return 1; - - if (value === 'g') - return 1; - - return parseInt(value); -} - -function selectFile(current) { - const name = DOM.getCurrentName(current); - - if (name === '..') - return; - - DOM.selectFile(current); -} - -function setCurrent({n, current, visual, sibling}) { - const select = visual ? selectFile : DOM.unselectFile; - - select(current); - - const position = `${sibling}Sibling`; - for (let i = 0; i < n; i++) { - const next = current[position]; - - if (!next) - break; - - current = next; - select(current); - } - - DOM.setCurrentFile(current); -} - + stopImmediatePropagation(); + preventDefault(); + promptNewFile(); +}; diff --git a/client/key/vim/index.spec.js b/client/key/vim/index.spec.js new file mode 100644 index 00000000..231c6577 --- /dev/null +++ b/client/key/vim/index.spec.js @@ -0,0 +1,756 @@ +import {test, stub} from 'supertape'; +import {getDOM, getCloudCmd} from './globals.fixture.js'; +import vim, {selectFile as vimSelectFile} from './index.js'; +import * as finder from './find.js'; + +globalThis.DOM = getDOM(); +globalThis.CloudCmd = getCloudCmd(); + +const {assign} = Object; +const {DOM} = globalThis; +const {Buffer} = DOM; + +test('cloudcmd: client: key: set next file: no', (t) => { + const element = {}; + const setCurrentFile = stub(); + const unselectFiles = stub(); + + const Info = { + element, + }; + + vim('j', {}, { + Info, + setCurrentFile, + unselectFiles, + }); + + t.calledWith(setCurrentFile, [element], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: set next file current: j', async (t) => { + const nextSibling = 'hello'; + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + const Info = { + element, + }; + + await vim('j', {}, { + Info, + setCurrentFile, + unselectFiles: stub(), + }); + + t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: set next file current: mjj', (t) => { + const nextSibling = 'hello'; + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + const Info = { + element, + }; + + const deps = { + Info, + setCurrentFile, + unselectFiles: stub(), + }; + + vim('m', {}, deps); + vim('j', {}, deps); + vim('j', {}, deps); + + t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: set next file current: g', (t) => { + const nextSibling = 'hello'; + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + const Info = { + element, + }; + + const deps = { + Info, + setCurrentFile, + unselectFiles: stub(), + }; + + vim('g', {}, deps); + vim('j', {}, deps); + + t.calledWith(setCurrentFile, [nextSibling], 'should ignore g'); + t.end(); +}); + +test('cloudcmd: client: key: set +2 file current', (t) => { + const last = {}; + const setCurrentFile = stub(); + const element = {}; + + const Info = { + element, + }; + + const deps = { + setCurrentFile, + Info, + unselectFiles: stub(), + }; + + const event = {}; + + vim('2', event, deps); + vim('j', event, deps); + + t.calledWith(setCurrentFile, [last], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: select +2 files from current before delete', (t) => { + const last = {}; + const nextSibling = { + nextSibling: last, + }; + + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + const Info = { + element, + }; + + const Operation = { + show: stub(), + }; + + const selectFile = stub(); + const getCurrentName = stub().returns('x'); + + const event = {}; + + const deps = { + Info, + setCurrentFile, + selectFile, + getCurrentName, + Operation, + }; + + vim('d', event, deps); + vim('2', event, deps); + vim('j', event, deps); + + t.calledWith(setCurrentFile, [last], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: delete +2 files from current', (t) => { + const last = {}; + const nextSibling = { + nextSibling: last, + }; + + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + const show = stub(); + + const deps = { + Info: { + element, + }, + Operation: { + show, + }, + setCurrentFile, + selectFile: stub(), + getCurrentName: stub().returns('x'), + unselectFiles: stub(), + }; + + const event = {}; + + vim('d', event, deps); + vim('2', event, deps); + vim('j', event, deps); + + t.calledWith(show, ['delete'], 'should call delete'); + t.end(); +}); + +test('cloudcmd: client: key: set previous file current', (t) => { + const previousSibling = 'hello'; + const element = { + previousSibling, + }; + + const setCurrentFile = stub(); + const unselectFiles = stub(); + + const Info = { + element, + }; + + const deps = { + Info, + setCurrentFile, + unselectFiles, + }; + + vim('k', {}, deps); + + t.calledWith(setCurrentFile, [previousSibling], 'should set previous file'); + t.end(); +}); + +test('cloudcmd: client: key: copy: no', (t) => { + const copy = stub(); + + vim('y', {}, { + unselectFiles: stub(), + Buffer: { + copy, + }, + }); + + t.notCalled(copy, 'should not copy files'); + t.end(); +}); + +test('cloudcmd: client: key: copy', (t) => { + const copy = stub(); + const Info = { + element: {}, + }; + + const toggleSelectedFile = stub(); + const unselectFiles = stub(); + + const deps = { + Info, + unselectFiles, + toggleSelectedFile, + Buffer: { + copy, + }, + }; + + vim('v', {}, deps); + vim('y', {}, deps); + + t.calledWithNoArgs(copy, 'should copy files'); + t.end(); +}); + +test('cloudcmd: client: key: copy: unselectFiles', (t) => { + const unselectFiles = stub(); + const Info = { + element: {}, + }; + + const toggleSelectedFile = stub(); + + const deps = { + Info, + unselectFiles, + toggleSelectedFile, + Buffer: { + copy: stub(), + }, + }; + + vim('v', {}, deps); + vim('y', {}, deps); + + t.calledWithNoArgs(unselectFiles, 'should unselect files'); + t.end(); +}); + +test('cloudcmd: client: key: paste', (t) => { + const paste = stub(); + + Buffer.paste = paste; + + vim('p', {}, { + Buffer, + }); + + t.calledWithNoArgs(paste, 'should paste files'); + t.end(); +}); + +test('cloudcmd: client: key: selectFile: ..', (t) => { + const getCurrentName = stub().returns('..'); + const selectFile = stub(); + const current = {}; + + vimSelectFile(current, { + selectFile, + getCurrentName, + }); + + t.notCalled(selectFile, 'should not call selectFile'); + t.end(); +}); + +test('cloudcmd: client: key: selectFile', (t) => { + const selectFile = stub(); + const getCurrentName = stub().returns('x'); + const current = {}; + + vimSelectFile(current, { + selectFile, + getCurrentName, + }); + + t.calledWith(selectFile, [current], 'should call selectFile'); + t.end(); +}); + +test('cloudcmd: client: key: set last file current: shift + g', async (t) => { + const last = 'last'; + const nextSibling = { + nextSibling: last, + }; + + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + await vim('G', {}, { + Info: { + element, + }, + setCurrentFile, + unselectFiles: stub(), + }); + + t.calledWith(setCurrentFile, [last], 'should set last file'); + t.end(); +}); + +test('cloudcmd: client: key: set last file current: $', (t) => { + const last = 'last'; + const nextSibling = { + nextSibling: last, + }; + + const element = { + nextSibling, + }; + + const setCurrentFile = stub(); + + vim('$', {}, { + Info: { + element, + }, + setCurrentFile, + unselectFiles: stub(), + }); + + t.calledWith(setCurrentFile, [last], 'should set last file'); + t.end(); +}); + +test('cloudcmd: client: key: set first file current: gg', (t) => { + const first = 'first'; + const previousSibling = { + previousSibling: first, + }; + + const element = { + previousSibling, + }; + + const Operation = { + show: stub(), + }; + + const unselectFiles = stub(); + const setCurrentFile = stub(); + + const deps = { + Operation, + unselectFiles, + setCurrentFile, + Info: { + element, + }, + }; + + vim('g', {}, deps); + vim('g', {}, deps); + + t.calledWith(setCurrentFile, [first], 'should set first file'); + t.end(); +}); + +test('cloudcmd: client: key: set first file current: ^', async (t) => { + const first = 'first'; + const previousSibling = { + previousSibling: first, + }; + + const element = { + previousSibling, + }; + + const Operation = { + show: stub(), + }; + + const unselectFiles = stub(); + const setCurrentFile = stub(); + + const deps = { + setCurrentFile, + Info: { + element, + }, + unselectFiles, + Operation, + }; + + await vim('^', {}, deps); + + t.calledWith(setCurrentFile, [first], 'should set first file'); + t.end(); +}); + +test('cloudcmd: client: key: visual', (t) => { + const element = {}; + const toggleSelectedFile = stub(); + + const Info = { + element, + }; + + vim('v', {}, { + Info, + toggleSelectedFile, + }); + + t.calledWith(toggleSelectedFile, [element], 'should toggle selection'); + t.end(); +}); + +test('cloudcmd: client: key: ESC', (t) => { + const element = {}; + const unselectFiles = stub(); + + const Info = { + element, + }; + + vim('Escape', null, { + Info, + unselectFiles, + }); + + t.calledWithNoArgs(unselectFiles, 'should toggle selection'); + t.end(); +}); + +test('cloudcmd: client: key: Enter', async (t) => { + const nextSibling = 'hello'; + const element = { + nextSibling, + }; + + const unselectFiles = stub(); + const setCurrentFile = stub(); + + const Info = { + element, + }; + + await vim('Enter', null, { + Info, + setCurrentFile, + unselectFiles, + }); + + await vim('j', null, { + Info, + setCurrentFile, + unselectFiles, + }); + + t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); + t.end(); +}); + +test('cloudcmd: client: key: /', (t) => { + const preventDefault = stub(); + const element = {}; + + const Info = { + element, + files: [], + }; + + const getCurrentName = stub().returns(''); + + const event = { + preventDefault, + }; + + const prompt = stub().returns([]); + + vim('/', event, { + getCurrentName, + Info, + prompt, + }); + + t.calledWithNoArgs(preventDefault); + t.end(); +}); + +test('cloudcmd: client: find', (t) => { + assign(DOM.Dialog, { + prompt: stub().returns([]), + }); + + const setCurrentByName = stub(); + + assign(DOM, { + setCurrentByName, + }); + + const event = { + preventDefault: stub(), + }; + + vim('/', event); + + t.notCalled(setCurrentByName); + t.end(); +}); + +test('cloudcmd: client: key: n', (t) => { + const findNext = stub(); + const createFindNext = stub().returns(findNext); + + const event = {}; + + vim('n', event, { + createFindNext, + }); + + t.calledWithNoArgs(findNext, 'should call findNext'); + t.end(); +}); + +test('cloudcmd: client: key: N', (t) => { + const findPrevious = stub(); + const createFindPrevious = stub().returns(findPrevious); + const event = {}; + + vim('N', event, { + createFindPrevious, + }); + + t.calledWithNoArgs(findPrevious); + t.end(); +}); + +test('cloudcmd: client: key: make directory', async (t) => { + const {DOM} = globalThis; + + assign(DOM, { + promptNewDir: stub(), + }); + + const event = { + stopImmediatePropagation: stub(), + preventDefault: stub(), + }; + + await vim('m', event); + await vim('d', event); + + t.calledWithNoArgs(DOM.promptNewDir); + t.end(); +}); + +test('cloudcmd: client: key: make file', (t) => { + const promptNewFile = stub(); + + const event = { + stopImmediatePropagation: stub(), + preventDefault: stub(), + }; + + vim('m', event); + vim('f', event, { + promptNewFile, + }); + + t.calledWithNoArgs(promptNewFile); + t.end(); +}); + +test('cloudcmd: client: vim: terminal', (t) => { + const CloudCmd = { + Terminal: { + show: stub(), + }, + }; + + const event = {}; + + vim('t', event, { + CloudCmd, + }); + vim('t', event, { + CloudCmd, + }); + + t.calledWithNoArgs(CloudCmd.Terminal.show); + t.end(); +}); + +test('cloudcmd: client: vim: edit', async (t) => { + globalThis.DOM = getDOM(); + globalThis.CloudCmd = getCloudCmd(); + + const {CloudCmd} = globalThis; + + assign(CloudCmd, { + EditFileVim: { + show: stub(), + }, + }); + + const event = {}; + + await vim('e', event); + + t.calledWithNoArgs(CloudCmd.EditFileVim.show); + t.end(); +}); + +test('cloudcmd: client: vim: rename', async (t) => { + const DOM = getDOM(); + const renameCurrent = stub(); + + assign(DOM, { + renameCurrent, + }); + + const event = { + preventDefault: stub(), + }; + + await vim('rr', event, DOM); + + t.calledWithNoArgs(renameCurrent); + t.end(); +}); + +test('cloudcmd: client: key: cc: operationCopy', (t) => { + const show = stub(); + const preventDefault = stub(); + + const Operation = { + show, + }; + + const event = { + preventDefault, + }; + + vim('c', event, { + Operation, + }); + + vim('c', event, { + Operation, + }); + + t.calledWith(show, ['copy'], 'should show copy operation'); + t.end(); +}); + +test('cloudcmd: client: key: mm: operationMove', (t) => { + const show = stub(); + const preventDefault = stub(); + + const Operation = { + show, + }; + + const event = { + preventDefault, + }; + + vim('m', event, { + Operation, + }); + + vim('m', event, { + Operation, + }); + + t.calledWith(show, ['move'], 'should show move operation'); + t.end(); +}); + +test('cloudcmd: client: key: n: findNext: real', (t) => { + const setCurrentByName = stub(); + + finder.find('a', ['alpha', 'beta', 'apple']); + + const event = {}; + + vim('n', event, { + setCurrentByName, + }); + + t.calledWith(setCurrentByName, ['beta'], 'should set current by next found name'); + t.end(); +}); + +test('cloudcmd: client: key: N: findPrevious: real', (t) => { + const setCurrentByName = stub(); + + finder.find('a', ['alpha', 'beta', 'apple']); + + const event = {}; + + vim('N', event, { + setCurrentByName, + }); + + t.calledWith(setCurrentByName, ['apple'], 'should set current by previous found name'); + t.end(); +}); diff --git a/client/key/vim/set-current.js b/client/key/vim/set-current.js new file mode 100644 index 00000000..a2fc8646 --- /dev/null +++ b/client/key/vim/set-current.js @@ -0,0 +1,33 @@ +/* global DOM */ +export function selectFileNotParent(current, {getCurrentName, selectFile} = DOM) { + const name = getCurrentName(current); + + if (name === '..') + return; + + selectFile(current); +} + +export const setCurrent = (sibling, {count, isVisual, isDelete}, {Info, setCurrentFile, unselectFiles, Operation}) => { + let current = Info.element; + const select = isVisual ? selectFileNotParent : unselectFiles; + + select(current); + + const position = `${sibling}Sibling`; + + for (let i = 0; i < count; i++) { + const next = current[position]; + + if (!next) + break; + + current = next; + select(current); + } + + setCurrentFile(current); + + if (isDelete) + Operation.show('delete'); +}; diff --git a/client/key/vim/vim.js b/client/key/vim/vim.js new file mode 100644 index 00000000..95a27caf --- /dev/null +++ b/client/key/vim/vim.js @@ -0,0 +1,220 @@ +import {fullstore} from 'fullstore'; + +const store = fullstore(''); +const visual = fullstore(false); + +const stopVisual = () => { + visual(false); +}; + +const end = () => { + store(''); +}; + +const rmFirst = (a) => { + return a + .split('') + .slice(1) + .join(''); +}; + +const noop = () => {}; + +export default (key, operations = {}) => { + const prevStore = store(); + const isVisual = visual(); + const value = store(prevStore.concat(key)); + + const { + escape = noop, + moveNext = noop, + movePrevious = noop, + remove = noop, + copy = noop, + paste = noop, + select = noop, + find = noop, + findNext = noop, + findPrevious = noop, + makeFile = noop, + makeDirectory = noop, + terminal = noop, + edit = noop, + operationCopy = noop, + operationMove = noop, + rename = noop, + } = operations; + + if (key === 'Enter') + return end(); + + if (key === 'Escape') { + visual(false); + escape(); + + return end(); + } + + if (key === 'j' || key === 'w') { + const { + count, + isDelete, + isVisual, + } = handleDelete(prevStore); + + !isNaN(count) && moveNext({ + count, + isVisual, + isDelete, + }); + + return end(); + } + + if (key === 'k' || key === 'b') { + const { + count, + isDelete, + isVisual, + } = handleDelete(prevStore); + + !isNaN(count) && movePrevious({ + count, + isVisual, + isDelete, + }); + + return end(); + } + + if (value === 'gg' || key === '^') { + const {isDelete, isVisual} = handleDelete(prevStore); + + movePrevious({ + count: Infinity, + isVisual, + isDelete, + }); + + return end(); + } + + if (value === 'md') { + makeDirectory(); + return end(); + } + + if (value === 'tt') { + terminal(); + return end(); + } + + if (value === 'e') { + edit(); + return end(); + } + + if (value === 'cc') { + operationCopy(); + return end(); + } + + if (value === 'mm') { + operationMove(); + return end(); + } + + if (value === 'mf') { + makeFile(); + return end(); + } + + if (value === 'rr') { + rename(); + return end(); + } + + if (key === 'd' && (visual() || prevStore === 'd')) { + stopVisual(); + remove(); + + return end(); + } + + if (key === 'G' || key === '$') { + moveNext({ + count: Infinity, + isVisual, + }); + + return end(); + } + + if (key === 'y') { + if (!visual()) + return end(); + + stopVisual(); + copy(); + + return end(); + } + + if (/^p$/i.test(key)) { + paste(); + return end(); + } + + if (/^v$/i.test(key)) { + visual(!visual()); + select(); + + return end(); + } + + if (key === '/') { + find(); + return end(); + } + + if (key === 'n') { + findNext(); + return end(); + } + + if (key === 'N') { + findPrevious(); + return end(); + } + + if (key === ' ') + return end(); +}; + +function handleDelete(prevStore) { + const isDelete = prevStore[0] === 'd'; + + if (isDelete) { + visual(true); + prevStore = rmFirst(prevStore); + } + + const count = getNumber(prevStore); + const isVisual = visual(); + + return { + count, + isDelete, + isVisual, + }; +} + +function getNumber(value) { + if (!value) + return 1; + + if (value === 'g') + return 1; + + return parseInt(value); +} diff --git a/client/key/vim/vim.spec.js b/client/key/vim/vim.spec.js new file mode 100644 index 00000000..02fd52d4 --- /dev/null +++ b/client/key/vim/vim.spec.js @@ -0,0 +1,100 @@ +import {test, stub} from 'supertape'; +import vim from './vim.js'; + +test('vim: no operations', (t) => { + const result = vim('hello', {}); + + t.notOk(result); + t.end(); +}); + +test('vim: space', (t) => { + const moveNext = stub(); + + vim(' '); + vim('j', { + moveNext, + }); + + const args = [{ + count: 1, + isDelete: false, + isVisual: false, + }]; + + t.calledWith(moveNext, args); + t.end(); +}); + +test('vim: ^', (t) => { + const movePrevious = stub(); + + vim('^', { + movePrevious, + }); + + const expected = { + count: Infinity, + isVisual: false, + isDelete: false, + }; + + t.calledWith(movePrevious, [expected], 'should call movePrevious'); + t.end(); +}); + +test('vim: cc', (t) => { + const operationCopy = stub(); + + vim('cc', { + operationCopy, + }); + + t.calledWithNoArgs(operationCopy); + t.end(); +}); + +test('vim: mm', (t) => { + const operationMove = stub(); + + vim('mm', { + operationMove, + }); + + t.calledWithNoArgs(operationMove); + t.end(); +}); + +test('vim: w', (t) => { + const moveNext = stub(); + + vim('w', { + moveNext, + }); + + const expected = { + count: 1, + isVisual: false, + isDelete: false, + }; + + t.calledWith(moveNext, [expected], 'should call moveNext'); + t.end(); +}); + +test('vim: b', (t) => { + const movePrevious = stub(); + + vim('b', { + movePrevious, + }); + + const expected = { + count: 1, + isVisual: false, + isDelete: false, + }; + + t.calledWith(movePrevious, [expected], 'should call movePrevious'); + t.end(); +}); diff --git a/client/listeners/get-index.js b/client/listeners/get-index.js index d7870608..45ce85e8 100644 --- a/client/listeners/get-index.js +++ b/client/listeners/get-index.js @@ -1,11 +1,10 @@ -'use strict'; +import currify from 'currify'; -module.exports = (array, item) => { +export const getIndex = currify((array, item) => { const index = array.indexOf(item); if (!~index) return 0; return index; -}; - +}); diff --git a/test/client/listeners/get-index.js b/client/listeners/get-index.spec.js similarity index 65% rename from test/client/listeners/get-index.js rename to client/listeners/get-index.spec.js index 16dd6999..0380cdc1 100644 --- a/test/client/listeners/get-index.js +++ b/client/listeners/get-index.spec.js @@ -1,9 +1,5 @@ -'use strict'; - -const test = require('tape'); - -const dir = '../../../client/listeners'; -const getIndex = require(`${dir}/get-index`); +import test from 'supertape'; +import {getIndex} from './get-index.js'; test('cloudcmd: client: listeners: getIndex: not found', (t) => { const array = ['hello']; @@ -13,9 +9,11 @@ test('cloudcmd: client: listeners: getIndex: not found', (t) => { }); test('cloudcmd: client: listeners: getIndex: found', (t) => { - const array = ['hello', 'world']; + const array = [ + 'hello', + 'world', + ]; t.equal(getIndex(array, 'world'), 1, 'should return index'); t.end(); }); - diff --git a/client/listeners/get-range.js b/client/listeners/get-range.js index ef1ea0f8..839c2287 100644 --- a/client/listeners/get-range.js +++ b/client/listeners/get-range.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = (indexFrom, indexTo, files) => { +export const getRange = (indexFrom, indexTo, files) => { if (indexFrom < indexTo) return files.slice(indexFrom, indexTo + 1); @@ -9,4 +7,3 @@ module.exports = (indexFrom, indexTo, files) => { return [files[indexFrom]]; }; - diff --git a/client/listeners/get-range.spec.js b/client/listeners/get-range.spec.js new file mode 100644 index 00000000..77c950f4 --- /dev/null +++ b/client/listeners/get-range.spec.js @@ -0,0 +1,52 @@ +import test from 'supertape'; +import {getRange} from './get-range.js'; + +test('cloudcmd: client: listeners: getRange: direct', (t) => { + const expected = [ + 'hello', + 'world', + ]; + + const files = [ + ...expected, + 'how', + 'come', + ]; + + const result = getRange(0, 1, files); + + t.deepEqual(result, expected, 'should return range'); + t.end(); +}); + +test('cloudcmd: client: listeners: getRange: reverse', (t) => { + const expected = [ + 'hello', + 'world', + ]; + + const files = [ + ...expected, + 'how', + 'come', + ]; + + const result = getRange(1, 0, files); + + t.deepEqual(result, expected, 'should return range'); + t.end(); +}); + +test('cloudcmd: client: listeners: getRange: one', (t) => { + const expected = ['hello']; + const files = [ + ...expected, + 'how', + 'come', + ]; + + const result = getRange(0, 0, files); + + t.deepEqual(result, expected, 'should return range'); + t.end(); +}); diff --git a/client/listeners/index.js b/client/listeners/index.js index d07f24b4..babc1e0d 100644 --- a/client/listeners/index.js +++ b/client/listeners/index.js @@ -1,38 +1,30 @@ /* global DOM, CloudCmd */ - -'use strict'; - -const exec = require('execon'); -const itype = require('itype/legacy'); -const currify = require('currify/legacy'); -const clipboard = require('@cloudcmd/clipboard'); - -const getRange = require('./get-range'); -const getIndex = currify(require('./get-index')); -const uploadFiles = require('../dom/upload-files'); - -const { - FS, - apiURL -} = require('../../common/cloudfunc'); +import exec from 'execon'; +import itype from 'itype'; +import currify from 'currify'; +import {tryToCatch} from 'try-to-catch'; +import clipboard from '@cloudcmd/clipboard'; +import * as Events from '#dom/events'; +import {uploadFiles} from '#dom/upload-files'; +import {FS} from '#common/cloudfunc'; +import {getRange} from './get-range.js'; +import {getIndex} from './get-index.js'; const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const SPACE = ' '; -module.exports.init = () => { +export async function init() { contextMenu(); dragndrop(); unload(); pop(); resize(); - config(); header(); -}; - -CloudCmd.Listeners = module.exports; + await config(); +} const unselect = (event) => { - const isMac = /Mac/.test(window.navigator.platform); + const isMac = /Mac/.test(globalThis.navigator.platform); const { shiftKey, metaKey, @@ -46,19 +38,13 @@ const unselect = (event) => { }; const execAll = currify((funcs, event) => { - funcs.forEach((fn) => { + for (const fn of funcs) fn(event); - }); }); -const Info = DOM.CurrentInfo; -const Events = DOM.Events; const EventsFiles = { mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), - click: execAll([ - onClick, - unselect, - ]), + click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), dragstart: exec.with(execIfNotUL, onDragStart), dblclick: exec.with(execIfNotUL, onDblClick), touchstart: exec.with(execIfNotUL, onTouch), @@ -69,6 +55,7 @@ let EXT; function header() { const fm = DOM.getFM(); const isDataset = (el) => el.dataset; + const isPanel = (el) => { return /^js-(left|right)$/.test(el.dataset.name); }; @@ -80,8 +67,7 @@ function header() { if (parent.dataset.name !== 'js-fm-header') return; - const name = (el.dataset.name || '') - .replace('js-', ''); + const name = (el.dataset.name || '').replace('js-', ''); if (!/^(name|size|date)$/.test(name)) return; @@ -102,44 +88,48 @@ function getPath(el, path = []) { return getPath(el.parentElement, path.concat(el)); } -function config() { - DOM.Files.get('config', (e, config) => { - const type = config && config.packer; - EXT = DOM.getPackerExt(type); - }); +async function config() { + const [, config] = await tryToCatch(DOM.Files.get, 'config'); + const type = config?.packer; + + EXT = DOM.getPackerExt(type); } -module.exports.initKeysPanel = () => { +export const initKeysPanel = () => { const keysElement = DOM.getById('js-keyspanel'); if (!keysElement) return; - Events.addClick(keysElement, ({target}) => { - const id = target.id; + Events.addClick(keysElement, (event) => { + const {target} = event; + const {id} = target; + const operation = (name) => { - const Operation = CloudCmd.Operation; - const fn = Operation.show.bind(null, name); + const {Operation} = CloudCmd; - return fn; + return Operation.show.bind(null, name); }; const clickFuncs = { - 'f1' : CloudCmd.Help.show, - 'f2' : DOM.renameCurrent, - 'f3' : CloudCmd.View.show, - 'f4' : CloudCmd.EditFile.show, - 'f5' : operation('copy'), - 'f6' : operation('move'), - 'f7' : DOM.promptNewDir, - 'f8' : operation('delete'), - 'f9' : CloudCmd.Menu.show, - 'f10' : CloudCmd.Config.show, - '~' : CloudCmd.Konsole.show, - 'shift~' : CloudCmd.Terminal.show, - 'contact' : CloudCmd.Contact.show, + 'f1': CloudCmd.Help.show, + 'f2': CloudCmd.UserMenu.show, + 'f3': CloudCmd.View.show, + 'f4': CloudCmd.EditFile.show, + 'f5': operation('copy'), + 'f6': operation('move'), + 'f7': DOM.promptNewDir, + 'f8': operation('delete'), + 'f9': () => { + event.stopPropagation(); + CloudCmd.Menu.show(); + }, + 'f10': CloudCmd.Config.show, + '~': CloudCmd.Konsole.show, + 'shift~': CloudCmd.Terminal.show, + 'contact': CloudCmd.Contact.show, }; - + exec(clickFuncs[id]); }); }; @@ -148,10 +138,10 @@ const getPanel = (side) => { if (!itype.string(side)) return side; - return DOM.getByDataName('js-' + side); + return DOM.getByDataName(`js-${side}`); }; -module.exports.setOnPanel = (side) => { +export const setOnPanel = (side) => { const panel = getPanel(side); const filesElement = DOM.getByDataName('js-files', panel); @@ -167,27 +157,32 @@ function getPathListener(panel) { } function isNoCurrent(panel) { + const Info = DOM.CurrentInfo; const infoPanel = Info.panel; + + if (!infoPanel) + return true; + const namePanel = panel.getAttribute('data-name'); const nameInfoPanel = infoPanel.getAttribute('data-name'); return namePanel !== nameInfoPanel; } -function decodePath(path){ +function decodePath(path) { const url = CloudCmd.HOST; - const prefix = CloudCmd.PREFIX; + const {prefix} = CloudCmd; const prefixReg = RegExp('^' + prefix + FS); return decodeURI(path) .replace(url, '') - .replace(prefixReg, '') - // browser doesn't replace % -> %25% do it for him + .replace(prefixReg, '') // browser doesn't replace % -> %25% do it for him .replace('%%', '%25%') .replace(NBSP_REG, SPACE) || '/'; } -function onPathElementClick(panel, event) { +async function onPathElementClick(panel, event) { + const Info = DOM.CurrentInfo; event.preventDefault(); const element = event.target; @@ -200,7 +195,7 @@ function onPathElementClick(panel, event) { if (attr === 'js-refresh') return CloudCmd.refresh({ panel, - noCurrent + noCurrent, }); if (attr !== 'js-path-link') @@ -209,19 +204,26 @@ function onPathElementClick(panel, event) { const {href} = element; const path = decodePath(href); - CloudCmd.loadDir({ - path, + await CloudCmd.changeDir(path, { isRefresh: false, - panel: noCurrent ? panel : Info.panel + panel: noCurrent ? panel : Info.panel, }); } function copyPath(el) { - clipboard.writeText(el.parentElement.title) + clipboard + .writeText(el.parentElement.title) .then(CloudCmd.log) .catch(CloudCmd.log); } +function execIfNotMobile(callback, event) { + const isMobile = DOM.getCSSVar('is-mobile'); + + if (!isMobile) + callback(event); +} + function execIfNotUL(callback, event) { const {target} = event; const {tagName} = target; @@ -231,30 +233,27 @@ function execIfNotUL(callback, event) { } function onClick(event) { - const ctrl = event.ctrlKey; - - if (!ctrl) - event.preventDefault(); - + event.preventDefault(); changePanel(event.target); } function toggleSelect(key, files) { - const isMac = /Mac/.test(window.navigator.platform); + const isMac = /Mac/.test(globalThis.navigator.platform); if (!key) throw Error('key should not be undefined!'); const [file] = files; - if (isMac && key.meta || key.ctrl) + if (isMac && key.meta) return DOM.toggleSelectedFile(file); if (key.shift) - return files.forEach(DOM.selectFile); + return files.map(DOM.selectFile); } function changePanel(element) { + const Info = DOM.CurrentInfo; const {panel} = Info; const files = DOM.getByDataName('js-files', panel); const ul = getULElement(element); @@ -263,21 +262,20 @@ function changePanel(element) { DOM.changePanel(); } -function onDblClick(event) { +async function onDblClick(event) { + event.preventDefault(); + const current = getLIElement(event.target); const isDir = DOM.isCurrentIsDir(current); const path = DOM.getCurrentPath(current); - if (isDir) { - CloudCmd.loadDir({ - path: path === '/' ? '/' : path + '/' - }); - - event.preventDefault(); - } + if (!isDir) + return CloudCmd.View.show(); + + await CloudCmd.changeDir(path); } -function onTouch(event) { +async function onTouch(event) { const current = getLIElement(event.target); const isDir = DOM.isCurrentIsDir(current); @@ -289,9 +287,7 @@ function onTouch(event) { if (!isCurrent) return; - CloudCmd.loadDir({ - path: DOM.getCurrentPath(current) - }); + await CloudCmd.changeDir(DOM.getCurrentPath(current)); } /* @@ -299,8 +295,10 @@ function onTouch(event) { * in Chrome (HTML5) */ function onDragStart(event) { + const Info = DOM.CurrentInfo; + const {prefixURL} = CloudCmd; const element = getLIElement(event.target); - const isDir = Info.isDir; + const {isDir} = Info; let link = DOM.getCurrentLink(element); let name = DOM.getCurrentName(element); @@ -309,13 +307,15 @@ function onDragStart(event) { name += EXT; link = document.createElement('a'); link.textContent = name; - link.href = apiURL + '/pack' + Info.path + EXT; + link.href = prefixURL + '/pack' + Info.path + EXT; } - event.dataTransfer.setData('DownloadURL', - 'application/octet-stream' + ':' + - name + ':' + - link); + event.dataTransfer.setData( + 'DownloadURL', + 'application/octet-stream' + ':' + name + + ':' + + link, + ); } function getLIElement(element) { @@ -336,13 +336,14 @@ function getULElement(element) { } function setCurrentFileByEvent(event) { + const Info = DOM.CurrentInfo; const BUTTON_LEFT = 0; const key = { alt: event.altKey, ctrl: event.ctrlKey, meta: event.metaKey, - shift: event.shiftKey + shift: event.shiftKey, }; const element = getLIElement(event.target); @@ -379,7 +380,7 @@ function contextMenu() { Events.addOnce('contextmenu', fm, (event) => { CloudCmd.Menu.show({ x: event.clientX, - y: event.clientY + y: event.clientY, }); }); @@ -390,21 +391,17 @@ function contextMenu() { function dragndrop() { const panels = DOM.getByClassAll('panel'); - const select = () => { - [...panels].forEach((panel) => { - panel.classList.add('selected-panel'); - }); + const select = ({target}) => { + target.classList.add('selected-panel'); }; - const unselect = () => { - [...panels].forEach((panel) => { - panel.classList.remove('selected-panel'); - }); + const unselect = ({target}) => { + target.classList.remove('selected-panel'); }; const onDrop = (event) => { - const files = event.dataTransfer.files; - const items = event.dataTransfer.items; + const {files, items} = event.dataTransfer; + const {length: filesCount} = files; event.preventDefault(); @@ -413,7 +410,10 @@ function dragndrop() { return uploadFiles(files); const isFile = (item) => item.kind === 'file'; - const dirFiles = [...items].filter(isFile); + + const dirFiles = Array + .from(items) + .filter(isFile); if (dirFiles.length) return DOM.uploadDirectory(dirFiles); @@ -425,13 +425,13 @@ function dragndrop() { }; /** - * In Mac OS Chrome dropEffect = 'none' + * In macOS Chrome dropEffect = 'none' * so drop do not firing up when try * to upload file from download bar */ const onDragOver = (event) => { - const dataTransfer = event.dataTransfer; - const effectAllowed = dataTransfer.effectAllowed; + const {dataTransfer} = event; + const {effectAllowed} = dataTransfer; if (/move|linkMove/.test(effectAllowed)) dataTransfer.dropEffect = 'move'; @@ -441,19 +441,18 @@ function dragndrop() { event.preventDefault(); }; - Events.add('dragenter', select); - Events.add(['dragleave', 'drop'], unselect); - - [...panels].forEach((panel) => { - Events.add('dragover', panel, onDragOver) - .add('drop', panel, onDrop); - }); + for (const panel of panels) + Events + .add('dragover', panel, onDragOver) + .add('drop', panel, onDrop) + .add('dragenter', select) + .add(['dragleave', 'drop'], unselect); } function unload() { - DOM.Events.add(['unload', 'beforeunload'], (event) => { - const Key = CloudCmd.Key; - const isBind = Key && Key.isBind(); + Events.add(['unload', 'beforeunload'], (event) => { + const {Key} = CloudCmd; + const isBind = Key?.isBind(); if (isBind) return; @@ -464,15 +463,15 @@ function unload() { } function pop() { - Events.add('popstate', ({state}) => { + Events.add('popstate', async ({state}) => { const path = (state || '').replace(FS, ''); if (!path) return CloudCmd.route(location.hash); const history = false; - CloudCmd.loadDir({ - path, + + await CloudCmd.changeDir(path, { history, }); }); @@ -480,7 +479,8 @@ function pop() { function resize() { Events.add('resize', () => { - const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; + const Info = DOM.CurrentInfo; + const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH; if (!is) return; @@ -500,4 +500,3 @@ function resize() { DOM.changePanel(); }); } - diff --git a/client/load-module.js b/client/load-module.js new file mode 100644 index 00000000..7fc2c328 --- /dev/null +++ b/client/load-module.js @@ -0,0 +1,56 @@ +/* global CloudCmd */ +import exec from 'execon'; +import {tryToCatch} from 'try-to-catch'; +import {js as loadJS} from 'load.js'; +import pascalCase from 'just-pascal-case'; + +const noJS = (a) => a.replace(/.js$/, ''); + +/** + * function load modules + * @params = {name, path, func, dobefore, arg} + */ +export const loadModule = (params) => { + if (!params) + return; + + const {path} = params; + + const name = path && noJS(pascalCase(path)); + const doBefore = params.dobefore; + + if (CloudCmd[name]) + return; + + CloudCmd[name] = async () => { + exec(doBefore); + + const {DIR_MODULES} = CloudCmd; + const pathFull = `${DIR_MODULES}/${path}.js`; + + await loadJS(pathFull); + const newModule = async (f) => f && f(); + const module = CloudCmd[name]; + + Object.assign(newModule, module); + + CloudCmd[name] = newModule; + CloudCmd.log('init', name); + + await module.init(); + + return newModule; + }; + + CloudCmd[name].show = async (...args) => { + CloudCmd.log('show', name, args); + const m = CloudCmd[name]; + + const [e, a] = await tryToCatch(m); + + if (e) + return; + + return await a.show(...args); + }; +}; diff --git a/client/modules/cloud.js b/client/modules/cloud.js index 466ba987..cfe0d708 100644 --- a/client/modules/cloud.js +++ b/client/modules/cloud.js @@ -1,27 +1,32 @@ /* global CloudCmd, filepicker */ - -'use strict'; - -CloudCmd.Cloud = CloudProto; - -const exec = require('execon'); -const currify = require('currify/legacy'); +import exec from 'execon'; +import currify from 'currify'; +import load from 'load.js'; +import {ajax} from '#dom/load'; +import * as Files from '#dom/files'; +import * as Images from '#dom/images'; const {log} = CloudCmd; -const load = require('../dom/load'); -const Files = require('../dom/files'); -const Images = require('../dom/images'); - const upload = currify(_upload); -function CloudProto(callback) { - loadFiles(callback); +const Name = 'Cloud'; + +CloudCmd[Name] = { + init, + uploadFile, + saveFile, +}; + +export async function init() { + const [modules] = await loadFiles(); + const {key} = modules.data.FilePicker; - return module.exports; + filepicker.setKey(key); + Images.hide(); } -module.exports.uploadFile = (filename, data) => { +export function uploadFile(filename, data) { const mimetype = ''; filepicker.store(data, { @@ -30,40 +35,30 @@ module.exports.uploadFile = (filename, data) => { }, (fpFile) => { filepicker.exportFile(fpFile, log, log); }); -}; +} -module.exports.saveFile = (callback) => { +export function saveFile(callback) { filepicker.pick(upload(callback)); -}; +} function _upload(callback, file) { - const { - url, - filename, - } = file; + const {url, filename} = file; const responseType = 'arraybuffer'; const success = exec.with(callback, filename); - load.ajax({ + ajax({ url, responseType, success, }); } -function loadFiles(callback) { +function loadFiles() { const js = '//api.filepicker.io/v2/filepicker.js'; - load.js(js, () => { - Files.get('modules', (error, modules) => { - const {key} = modules.data.FilePicker; - - filepicker.setKey(key); - - Images.hide(); - exec(callback); - }); - }); + return Promise.all([ + Files.get('modules'), + load.js(js), + ]); } - diff --git a/client/modules/command-line.js b/client/modules/command-line.js new file mode 100644 index 00000000..7a1c3993 --- /dev/null +++ b/client/modules/command-line.js @@ -0,0 +1,31 @@ +/* global CloudCmd */ +import * as Dialog from '#dom/dialog'; + +export function init() {} +CloudCmd.CommandLine = { + init, + show, + hide, +}; + +export async function show() { + const [, cmd] = await Dialog.prompt('Command Line', ''); + const TERMINAL = '^(t|terminal)'; + + if (RegExp(`${TERMINAL}$`).test(cmd)) + return await CloudCmd.Terminal.show(); + + if (RegExp(TERMINAL).test(cmd)) { + const command = cmd.replace(RegExp(`${TERMINAL} `), ''); + const exitCode = await CloudCmd.TerminalRun.show({ + command: `bash -c '${command}'`, + }); + + if (exitCode === -1) + await Dialog.alert(`☝️ Looks like Terminal is disabled, start Cloud Coammnder with '--terminal' flag.`); + + return; + } +} + +export function hide() {} diff --git a/client/modules/config.js b/client/modules/config.js deleted file mode 100644 index 459ed8bb..00000000 --- a/client/modules/config.js +++ /dev/null @@ -1,260 +0,0 @@ -'use strict'; - -/* global CloudCmd, DOM, io */ - -require('../../css/config.css'); - -const rendy = require('rendy/legacy'); -const exec = require('execon'); -const currify = require('currify/legacy'); -const squad = require('squad/legacy'); -const input = require('../input'); - -const Images = require('../dom/images'); -const Events = require('../dom/events'); -const Files = require('../dom/files'); - -const {getTitle} = require('../../common/cloudfunc'); - -const {Dialog, setTitle} = DOM; - -const TITLE = 'Config'; -const alert = currify(Dialog.alert, TITLE); - -const Config = module.exports; - -const showLoad = () => { - Images.show.load('top'); -}; - -const addKey = currify((fn, input) => { - Events.addKey(input, fn); - return input; -}); - -const addChange = currify((fn, input) => { - Events.add('change', input, fn); - return input; -}); - -CloudCmd.Config = ConfigProto; - -let Loading = true; - -function ConfigProto() { - const noop = () => {}; - - if (!CloudCmd.config('configDialog')) - return { - show: noop - }; - - Loading = true; - - showLoad(); - exec.series([ - CloudCmd.View, - (callback) => { - Loading = false; - exec(callback); - DOM.loadSocket(initSocket); - }, - show - ]); - - return module.exports; -} - -const config = CloudCmd.config; - -const {Key} = CloudCmd; - -let Element; -let Template; - -function getHost() { - const {host, origin, protocol} = location; - const href = origin || `${protocol}//${host}`; - - return href; -} - -function initSocket() { - const href = getHost(); - const prefix = CloudCmd.PREFIX; - const FIVE_SECONDS = 5000; - - const socket = io.connect(href + prefix + '/config', { - 'max reconnection attempts' : Math.pow(2, 32), - 'reconnection limit' : FIVE_SECONDS, - path: prefix + '/socket.io' - }); - - const save = (data) => { - onSave(data); - socket.send(data); - }; - - authCheck(socket); - - socket.on('connect', () => { - Config.save = save; - }); - - socket.on('message', onSave); - socket.on('log', CloudCmd.log); - - socket.on('disconnect', () => { - Config.save = saveHttp; - }); - - socket.on('err', alert); -} - -function authCheck(socket) { - socket.emit('auth', config('username'), config('password')); - - socket.on('reject', () => { - alert('Wrong credentials!'); - }); -} - -Config.save = saveHttp; - -module.exports.show = show; - -function show() { - const prefix = CloudCmd.PREFIX; - const funcs = [ - exec.with(Files.get, 'config-tmpl'), - exec.with(DOM.load.parallel, [ - prefix + '/dist/config.css' - ]) - ]; - - if (Loading) - return; - - showLoad(); - exec.parallel(funcs, fillTemplate); -} - -function fillTemplate(error, template) { - if (!Template) - Template = template; - - Files.get('config', (error, config) => { - if (error) - return alert('Could not load config!'); - - const obj = input.convert(config); - - obj[obj.editor + '-selected'] = 'selected'; - delete obj.editor; - - obj[obj.packer + '-selected'] = 'selected'; - delete obj.packer; - - obj[obj.columns + '-selected'] = 'selected'; - delete obj.columns; - - const inner = rendy(Template, obj); - - Element = DOM.load({ - name : 'form', - className : 'config', - inner, - attribute : { - 'data-name': 'js-config' - } - }); - - const inputs = document.querySelectorAll('input, select', Element); - const [inputFirst] = inputs; - - let afterShow; - if (inputFirst) { - onAuthChange(inputFirst.checked); - afterShow = inputFirst.focus.bind(inputFirst); - } - - const getTarget = ({target}) => target; - const handleChange = squad(onChange, getTarget); - - [...inputs] - .map(addKey(onKey)) - .map(addChange(handleChange)); - - const autoSize = true; - CloudCmd.View.show(Element, { - autoSize, - afterShow, - }); - }); -} - -module.exports.hide = () => { - CloudCmd.View.hide(); -}; - -function onChange(el) { - const obj = {}; - const name = input.getName(el); - const data = input.getValue(name, Element); - - if (name === 'name') - onNameChange(data); - else if (name === 'auth') - onAuthChange(data); - - obj[name] = data; - - Config.save(obj); -} - -function onSave(obj) { - Object.keys(obj).forEach((name) => { - const data = obj[name]; - - CloudCmd._config(name, data); - input.setValue(name, data, Element); - }); -} - -function saveHttp(obj) { - const {RESTful} = DOM; - - RESTful.Config.write(obj, (error) => { - if (error) - return; - - onSave(obj); - }); -} - -function onAuthChange(checked) { - const elUsername = input.getElementByName('username', Element); - const elPassword = input.getElementByName('password', Element); - - elUsername.disabled = - elPassword.disabled = !checked; -} - -function onNameChange(name) { - setTitle(getTitle({ - name - })); -} - -function onKey({keyCode, target}) { - switch (keyCode) { - case Key.ESC: - Config.hide(); - break; - - case Key.ENTER: - onChange(target); - break; - } -} - diff --git a/client/modules/config/index.js b/client/modules/config/index.js new file mode 100644 index 00000000..dfa4c105 --- /dev/null +++ b/client/modules/config/index.js @@ -0,0 +1,247 @@ +import '../../../css/config.css'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import squad from 'squad'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import load from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import * as Events from '#dom/events'; +import * as Files from '#dom/files'; +import {getTitle} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import * as input from './input.js'; + +const {CloudCmd, DOM} = globalThis; + +const {Dialog, setTitle} = DOM; + +const Name = 'Config'; + +const loadSocket = promisify(DOM.loadSocket); + +const showLoad = () => { + Images.show.load('top'); +}; + +const addKey = currify((fn, input) => { + Events.addKey(input, fn); + return input; +}); + +const addChange = currify((fn, input) => { + Events.add('change', input, fn); + return input; +}); + +const Config = {}; + +let Template; + +const loadCSS = load.css; + +export async function init() { + if (!CloudCmd.config('configDialog')) + return; + + showLoad(); + + const {DIR_DIST} = CloudCmd; + + [Template] = await Promise.all([ + Files.get('config-tmpl'), + loadSocket(), + loadCSS(`${DIR_DIST}/config.css`), + CloudCmd.View(), + ]); + + initSocket(); +} + +const {config, Key} = CloudCmd; + +let Element; + +function getHost() { + const { + host, + origin, + protocol, + } = location; + + return origin || `${protocol}//${host}`; +} + +function initSocket() { + const href = getHost(); + const {prefixSocket, prefix} = CloudCmd; + + const ONE_MINUTE = 60 * 1000; + + const socket = globalThis.io.connect(href + prefixSocket + '/config', { + reconnectionAttempts: Infinity, + reconnectionDelay: ONE_MINUTE, + path: `${prefix}/socket.io`, + }); + + const save = (data) => { + onSave(data); + socket.send(data); + }; + + authCheck(socket); + + socket.on('connect', () => { + Config.save = save; + }); + + socket.on('message', onSave); + socket.on('log', CloudCmd.log); + + socket.on('disconnect', () => { + Config.save = saveHttp; + }); + + socket.on('err', Dialog.alert); +} + +function authCheck(socket) { + socket.emit('auth', config('username'), config('password')); + socket.on('reject', wraptile(Dialog.alert, 'Wrong credentials!')); +} + +Config.save = saveHttp; + +export async function show() { + if (!CloudCmd.config('configDialog')) + return; + + await fillTemplate(); +} + +async function fillTemplate() { + const [error, config] = await tryToCatch(Files.get, 'config'); + + if (error) + return Dialog.alert('Could not load config!'); + + const { + editor, + menu, + packer, + columns, + theme, + configAuth, + configPort, + ...obj + } = input.convert(config); + + obj[`${menu}-selected`] = 'selected'; + obj[`${editor}-selected`] = 'selected'; + obj[`${packer}-selected`] = 'selected'; + obj[`${columns}-selected`] = 'selected'; + obj[`${theme}-selected`] = 'selected'; + obj.configAuth = configAuth ? '' : 'hidden'; + obj.configPort = configPort ? '' : 'hidden'; + + const innerHTML = rendy(Template, obj); + + Element = createElement('form', { + className: 'config', + innerHTML, + }); + + const inputs = document.querySelectorAll('input, select', Element); + const [inputFirst] = inputs; + + let afterShow; + + if (inputFirst) { + onAuthChange(inputFirst.checked); + afterShow = inputFirst.focus.bind(inputFirst); + } + + const getTarget = ({target}) => target; + const handleChange = squad(onChange, getTarget); + + Array + .from(inputs) + .map(addKey(onKey)) + .map(addChange(handleChange)); + + const autoSize = true; + + CloudCmd.View.show(Element, { + autoSize, + afterShow, + }); +} + +export function hide() { + CloudCmd.View.hide(); +} + +async function onChange(el) { + const obj = {}; + const name = input.getName(el); + const data = input.getValue(name, Element); + + if (name === 'name') + onNameChange(data); + else if (name === 'auth') + onAuthChange(data); + + obj[name] = data; + + await Config.save(obj); +} + +function onSave(obj) { + for (const name of Object.keys(obj)) { + const data = obj[name]; + + CloudCmd._config(name, data); + input.setValue(name, data, Element); + } +} + +async function saveHttp(obj) { + const {RESTful} = DOM; + const [e] = await RESTful.Config.write(obj); + + if (e) + return; + + onSave(obj); +} + +function onAuthChange(checked) { + const elUsername = input.getElementByName('username', Element); + const elPassword = input.getElementByName('password', Element); + + elUsername.disabled = !checked; + elPassword.disabled = !checked; +} + +function onNameChange(name) { + setTitle(getTitle({ + name, + })); +} + +async function onKey({keyCode, target}) { + switch(keyCode) { + case Key.ESC: + return hide(); + + case Key.ENTER: + return await onChange(target); + } +} + +CloudCmd[Name] = { + init, + show, + hide, +}; diff --git a/client/modules/config/input.js b/client/modules/config/input.js new file mode 100644 index 00000000..34a4be69 --- /dev/null +++ b/client/modules/config/input.js @@ -0,0 +1,78 @@ +import {encode} from '#common/entity'; + +const isBool = (a) => typeof a === 'boolean'; +const isString = (a) => typeof a === 'string'; + +const {keys} = Object; + +export function getElementByName(selector, element) { + const str = `[data-name="js-${selector}"]`; + + return element.querySelector(str); +} + +export const getName = (element) => { + const name = element + .getAttribute('data-name') + .replace(/^js-/, ''); + + return name; +}; + +export const convert = (config) => { + const result = config; + + for (const name of keys(config)) { + const item = config[name]; + + if (isBool(item)) { + result[name] = setState(item); + continue; + } + + if (isString(item)) { + result[name] = encode(item); + continue; + } + } + + return result; +}; + +function setState(state) { + if (state) + return ' checked'; + + return ''; +} + +export const getValue = (name, element) => { + const el = getElementByName(name, element); + const {type} = el; + + switch(type) { + case 'checkbox': + return el.checked; + + case 'number': + return Number(el.value); + + default: + return el.value; + } +}; + +export const setValue = (name, value, element) => { + const el = getElementByName(name, element); + const {type} = el; + + switch(type) { + case 'checkbox': + el.checked = value; + break; + + default: + el.value = value; + break; + } +}; diff --git a/client/modules/config/input.spec.js b/client/modules/config/input.spec.js new file mode 100644 index 00000000..9d0866cd --- /dev/null +++ b/client/modules/config/input.spec.js @@ -0,0 +1,140 @@ +import {test, stub} from 'supertape'; +import { + convert, + getName, + getValue, + setValue, +} from './input.js'; + +test('cloudcmd: client: config: input: convert', (t) => { + const result = convert({ + name: 'hello ', + }); + + const expected = { + name: 'hello <world>', + }; + + t.deepEqual(result, expected); + t.end(); +}); + +test('cloudcmd: client: config: input: convert: bool', (t) => { + const result = convert({ + auth: true, + }); + + const expected = { + auth: ' checked', + }; + + t.deepEqual(result, expected); + t.end(); +}); + +test('cloudcmd: client: config: input: convert: bool false', (t) => { + const result = convert({ + auth: false, + }); + + const expected = { + auth: '', + }; + + t.deepEqual(result, expected); + t.end(); +}); + +test('cloudcmd: client: config: input: getName', (t) => { + const getAttribute = stub().returns('js-hello'); + const element = { + getAttribute, + }; + + const result = getName(element); + + t.equal(result, 'hello', 'should strip js- prefix'); + t.end(); +}); + +test('cloudcmd: client: config: input: getValue: checkbox', (t) => { + const querySelector = stub().returns({ + type: 'checkbox', + checked: true, + }); + + const element = { + querySelector, + }; + + const result = getValue('auth', element); + + t.ok(result, 'should return checked value'); + t.end(); +}); + +test('cloudcmd: client: config: input: getValue: number', (t) => { + const querySelector = stub().returns({ + type: 'number', + value: '42', + }); + + const element = { + querySelector, + }; + + const result = getValue('port', element); + + t.equal(result, 42, 'should return number'); + t.end(); +}); + +test('cloudcmd: client: config: input: getValue: default', (t) => { + const querySelector = stub().returns({ + type: 'text', + value: 'hello', + }); + + const element = { + querySelector, + }; + + const result = getValue('name', element); + + t.equal(result, 'hello', 'should return value as is'); + t.end(); +}); + +test('cloudcmd: client: config: input: setValue: checkbox', (t) => { + const el = { + type: 'checkbox', + checked: false, + }; + + const querySelector = stub().returns(el); + const element = { + querySelector, + }; + + setValue('auth', true, element); + + t.ok(el.checked, 'should set checked'); + t.end(); +}); + +test('cloudcmd: client: config: input: setValue: default', (t) => { + const el = { + type: 'text', + value: 'old', + }; + + const querySelector = stub().returns(el); + const element = { + querySelector, + }; + + setValue('name', 'new', element); + + t.equal(el.value, 'new', 'should set value'); + t.end(); +}); diff --git a/client/modules/contact.js b/client/modules/contact.js index 4e7bb89d..48c7bd19 100644 --- a/client/modules/contact.js +++ b/client/modules/contact.js @@ -1,66 +1,34 @@ /* global CloudCmd */ /* global DOM */ -/* global olark */ +import olark from '@cloudcmd/olark'; +import * as Images from '#dom/images'; -'use strict'; +CloudCmd.Contact = { + init, + show, + hide, +}; -CloudCmd.Contact = ContactProto; +const {Events} = DOM; +const {Key} = CloudCmd; -const exec = require('execon'); -const Images = require('../dom/images'); - -function ContactProto(callback) { - init(callback); - - return exports; -} - -const Events = DOM.Events; -const Key = CloudCmd.Key; - -module.exports.show = show; -module.exports.hide = hide; - -let Inited = false; - -function init(callback) { - if (Inited) - return; - - load(() => { - Inited = true; - - olark.identify('6216-545-10-4223'); - olark('api.box.onExpand', show); - olark('api.box.onShow', show); - olark('api.box.onShrink', hide); - - exec(callback); - }); - +export function init() { Events.addKey(onKey); + + olark.identify('6216-545-10-4223'); + olark('api.box.onExpand', show); + olark('api.box.onShow', show); + olark('api.box.onShrink', hide); } -function load(callback) { - const {PREFIX} = CloudCmd; - const path = `${PREFIX}/modules/olark/olark.min.js`; - - Images.show.load('top'); - - DOM.load.js(path, callback); -} - -function show() { +export function show() { Key.unsetBind(); Images.hide(); - if (Inited) - return olark('api.box.expand'); - - init(show); + olark('api.box.expand'); } -function hide() { +export function hide() { Key.setBind(); olark('api.box.hide'); } @@ -69,4 +37,3 @@ function onKey({keyCode}) { if (keyCode === Key.ESC) hide(); } - diff --git a/client/modules/edit-file-vim.js b/client/modules/edit-file-vim.js index e153ccb4..16d557ae 100644 --- a/client/modules/edit-file-vim.js +++ b/client/modules/edit-file-vim.js @@ -1,55 +1,46 @@ -'use strict'; +import * as Events from '#dom/events'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -const exec = require('execon'); -const Events = require('../dom/events'); +CloudCmd.EditFileVim = { + init, + show, + hide, +}; const {Key} = CloudCmd; -CloudCmd.EditFileVim = function EditFileVimProto(callback) { - const EditFileVim = this; - - const ConfigView = { - bindKeys: false, - beforeClose: () => { - Events.rmKey(listener); - CloudCmd.EditFile.isChanged(); - } - }; - - function init(callback) { - exec.series([ - CloudCmd.EditFile, - callback || EditFileVim.show, - ]); - } - - this.show = () => { - Events.addKey(listener); - - CloudCmd.EditFile - .show(ConfigView) - .getEditor() - .setKeyMap('vim'); - }; - - this.hide = () => { - CloudCmd.Edit.hide(); - }; - - function listener(event) { - const { - keyCode, - shiftKey, - } = event; - - if (shiftKey && keyCode === Key.ESC) { - event.preventDefault(); - EditFileVim.hide(); - } - } - - init(callback); +const ConfigView = { + bindKeys: false, + beforeClose: () => { + Events.rmKey(listener); + CloudCmd.EditFile.isChanged(); + }, }; +export async function init() { + await CloudCmd.EditFile(); +} + +export async function show() { + Events.addKey(listener); + + const editFile = await CloudCmd.EditFile.show(ConfigView); + + editFile + .getEditor() + .setKeyMap('vim'); +} + +export function hide() { + CloudCmd.Edit.hide(); +} + +function listener(event) { + const {keyCode, shiftKey} = event; + + if (shiftKey && keyCode === Key.ESC) { + event.preventDefault(); + hide(); + } +} diff --git a/client/modules/edit-file.js b/client/modules/edit-file.js index 1c52e9db..83a07e36 100644 --- a/client/modules/edit-file.js +++ b/client/modules/edit-file.js @@ -1,206 +1,193 @@ -'use strict'; - /* global CloudCmd, DOM*/ +import Format from 'format-io'; +import {fullstore} from 'fullstore'; +import exec from 'execon'; +import {supermenu} from 'supermenu'; -const Format = require('format-io/legacy'); -const currify = require('currify/legacy'); -const store = require('fullstore/legacy'); -const squad = require('squad/legacy'); -const exec = require('execon'); -const supermenu = require('supermenu'); - -const call = currify((fn, callback) => { - fn(); - callback(); -}); - -CloudCmd.EditFile = function EditFileProto(callback) { - const Info = DOM.CurrentInfo; - const Dialog = DOM.Dialog; - const EditFile = exec.bind(); - const config = CloudCmd.config; - - let Menu; - - const TITLE = 'Edit'; - const Images = DOM.Images; - - let MSG_CHANGED; - const ConfigView = { - beforeClose: () => { - exec.ifExist(Menu, 'hide'); - EditFile.isChanged(); - } - }; - - function init(callback) { - const editor = store(); - - const getMainEditor = () => CloudCmd.Edit.getEditor(); - const getEditor = squad(editor, getMainEditor); - const auth = squad(authCheck, editor); - const listeners = squad(setListeners, editor); - - const show = callback ? exec : EditFile.show; - - exec.series([ - CloudCmd.Edit, - call(getEditor), - call(auth), - call(listeners), - show, - ], callback); - } - - function getName() { - const {name, isDir} = Info; - - if (isDir) - return `${name}.json`; - - return name; - } - - EditFile.show = (options) => { - const config = { - ...ConfigView, - ...options, - }; - - Images.show.load(); - - CloudCmd.Edit - .getEditor() - .setOption('keyMap', 'default'); - - Info.getData((error, data) => { - const path = Info.path; - const name = getName(); - - if (error) - return Images.hide(); - - setMsgChanged(name); - - CloudCmd.Edit - .getEditor() - .setValueFirst(path, data) - .setModeForPath(name) - .enableKey(); - - CloudCmd.Edit.show(config); - }); - - return CloudCmd.Edit; - }; - - EditFile.hide = () => { - CloudCmd.Edit.hide(); - }; - - function setListeners(editor) { - const element = CloudCmd.Edit.getElement(); - - DOM.Events.addOnce('contextmenu', element, setMenu); - - editor.on('save', (value) => { - DOM.setCurrentSize(Format.size(value)); - }); - } - - function authCheck(spawn) { - spawn.emit('auth', config('username'), config('password')); - spawn.on('reject', () => { - Dialog.alert(TITLE, 'Wrong credentials!'); - }); - } - - function setMenu(event) { - const position = { - x: event.clientX, - y: event.clientY - }; - - event.preventDefault(); - - if (Menu) - return; - - const options = { - beforeShow: (params) => { - params.x -= 18; - params.y -= 27; - }, - - afterClick: () => { - const editor = CloudCmd.Edit.getEditor(); - editor.focus(); - } - }; - - const element = CloudCmd.Edit.getElement(); - - Menu = supermenu(element, options, getMenuData()); - Menu.show(position.x, position.y); - } - - function getMenuData() { - const editor = CloudCmd.Edit.getEditor(); - - return { - 'Save Ctrl+S' : () => { - editor.save(); - }, - 'Go To Line Ctrl+G' : () => { - editor.goToLine(); - }, - 'Cut Ctrl+X' : () => { - editor.cutToClipboard(); - }, - 'Copy Ctrl+C' : () => { - editor.copyToClipboard(); - }, - 'Paste Ctrl+V' : () => { - editor.pasteFromClipboard(); - }, - 'Delete Del' : () => { - editor.remove('right'); - }, - 'Select All Ctrl+A' : () => { - editor.selectAll(); - }, - 'Beautify Ctrl+B' : () => { - editor.beautify(); - }, - 'Minify Ctrl+M' : () => { - editor.minify(); - }, - 'Close Esc' : () => { - EditFile.hide(); - } - }; - } - - function setMsgChanged(name) { - MSG_CHANGED = 'Do you want to save changes to ' + name + '?'; - } - - EditFile.isChanged = () => { - const editor = CloudCmd.Edit.getEditor(); - const is = editor.isChanged(); - - if (!is) - return; - - const cancel = false; - Dialog.confirm(TITLE, MSG_CHANGED, {cancel}) - .then(() => { - editor.save(); - }); - }; - - init(callback); - - return EditFile; +CloudCmd.EditFile = { + init, + show, + hide, + isChanged, }; +const Info = DOM.CurrentInfo; + +const {Dialog, Images} = DOM; + +const {config} = CloudCmd; + +let Menu; +let MSG_CHANGED; + +const isLoading = fullstore(); + +const ConfigView = { + beforeClose: async () => { + exec.ifExist(Menu, 'hide'); + await isChanged(); + }, +}; + +export async function init() { + isLoading(true); + + await CloudCmd.Edit(); + + const editor = CloudCmd.Edit.getEditor(); + authCheck(editor); + setListeners(editor); + + isLoading(false); +} + +function getName() { + const {name, isDir} = Info; + + if (isDir) + return `${name}.json`; + + return name; +} + +export async function show(options) { + if (isLoading()) + return; + + const optionsEdit = { + ...ConfigView, + ...options, + }; + + if (CloudCmd.config('showFileName')) + optionsEdit.title = Info.name; + + Images.show.load(); + + CloudCmd.Edit + .getEditor() + .setOption('keyMap', 'default'); + + const [error, data] = await Info.getData(); + + if (error) { + Images.hide(); + return CloudCmd.Edit; + } + + const {path} = Info; + const name = getName(); + + setMsgChanged(name); + + CloudCmd.Edit + .getEditor() + .setValueFirst(path, data) + .setModeForPath(name) + .enableKey(); + + CloudCmd.Edit.show(optionsEdit); + + return CloudCmd.Edit; +} + +export function hide() { + CloudCmd.Edit.hide(); +} + +function setListeners(editor) { + const element = CloudCmd.Edit.getElement(); + + DOM.Events.addOnce('contextmenu', element, setMenu); + + editor.on('save', (value) => { + DOM.setCurrentSize(Format.size(value)); + }); +} + +function authCheck(spawn) { + spawn.emit('auth', config('username'), config('password')); + spawn.on('reject', () => { + Dialog.alert('Wrong credentials!'); + }); +} + +function setMenu(event) { + const position = { + x: event.clientX, + y: event.clientY, + }; + + event.preventDefault(); + + if (Menu) + return; + + const options = { + beforeShow: (params) => { + params.x -= 18; + params.y -= 27; + }, + + afterClick: () => { + CloudCmd.Edit + .getEditor() + .focus(); + }, + }; + + const element = CloudCmd.Edit.getElement(); + + Menu = supermenu(element, options, getMenuData()); + + Menu.addContextMenuListener(); + Menu.show(position.x, position.y); +} + +function getMenuData() { + const editor = CloudCmd.Edit.getEditor(); + + return { + 'Save Ctrl+S': () => { + editor.save(); + }, + 'Go To Line Ctrl+G': () => { + editor.goToLine(); + }, + 'Cut Ctrl+X': () => { + editor.cutToClipboard(); + }, + 'Copy Ctrl+C': () => { + editor.copyToClipboard(); + }, + 'Paste Ctrl+V': () => { + editor.pasteFromClipboard(); + }, + 'Delete Del': () => { + editor.remove('right'); + }, + 'Select All Ctrl+A': () => { + editor.selectAll(); + }, + 'Close Esc': hide, + }; +} + +function setMsgChanged(name) { + MSG_CHANGED = `Do you want to save changes to ${name}?`; +} + +export async function isChanged() { + const editor = CloudCmd.Edit.getEditor(); + const is = editor.isChanged(); + + if (!is) + return; + + const [cancel] = await Dialog.confirm(MSG_CHANGED); + + if (cancel) + return; + + editor.save(); +} diff --git a/client/modules/edit-names-vim.js b/client/modules/edit-names-vim.js index 3b360a21..8b3b2f89 100644 --- a/client/modules/edit-names-vim.js +++ b/client/modules/edit-names-vim.js @@ -1,55 +1,44 @@ -'use strict'; +import * as Events from '#dom/events'; -/* global CloudCmd */ - -const exec = require('execon'); -const Events = require('../dom/events'); +const {CloudCmd} = globalThis; +CloudCmd.EditNamesVim = { + init, + show, + hide, +}; const {Key} = CloudCmd; -CloudCmd.EditNamesVim = function EditNamesVimProto(callback) { - const EditNamesVim = this; - - const ConfigView = { - bindKeys: false, - beforeClose: () => { - Events.rmKey(listener); - CloudCmd.EditNames.isChanged(); - } - }; - - function init(callback) { - exec.series([ - CloudCmd.EditNames, - callback || EditNamesVim.show, - ]); - } - - this.show = () => { - Events.addKey(listener); - - CloudCmd.EditNames - .show(ConfigView) - .getEditor() - .setKeyMap('vim'); - }; - - this.hide = () => { - CloudCmd.Edit.hide(); - }; - - function listener(event) { - const { - keyCode, - shiftKey, - } = event; - - if (shiftKey && keyCode === Key.ESC) { - event.preventDefault(); - EditNamesVim.hide(); - } - } - - init(callback); +const ConfigView = { + bindKeys: false, + beforeClose: () => { + Events.rmKey(listener); + CloudCmd.EditNames.isChanged(); + }, }; +export async function init() { + await CloudCmd.EditNames(); +} + +export function show() { + Events.addKey(listener); + + CloudCmd.EditNames + .show(ConfigView) + .getEditor() + .setKeyMap('vim'); +} + +export function hide() { + CloudCmd.Edit.hide(); +} + +function listener(event) { + const {keyCode, shiftKey} = event; + + if (shiftKey && keyCode === Key.ESC) { + event.preventDefault(); + hide(); + } +} diff --git a/client/modules/edit-names.js b/client/modules/edit-names.js index ecefce52..7bc29054 100644 --- a/client/modules/edit-names.js +++ b/client/modules/edit-names.js @@ -1,228 +1,218 @@ -'use strict'; +import {tryToCatch} from 'try-to-catch'; +import exec from 'execon'; +import {supermenu} from 'supermenu'; +import {multiRename} from 'multi-rename'; -/* global CloudCmd, DOM */ +const {CloudCmd, DOM} = globalThis; -const currify = require('currify/legacy'); -const store = require('fullstore/legacy'); -const squad = require('squad/legacy'); -const wraptile = require('wraptile/legacy'); -const exec = require('execon'); -const supermenu = require('supermenu'); - -const reject = Promise.reject.bind(Promise); - -const call = currify((fn, callback) => { - fn(); - callback(); -}); - -CloudCmd.EditNames = function EditNamesProto(callback) { - const Info = DOM.CurrentInfo; - const Dialog = DOM.Dialog; - - const TITLE = 'Edit Names'; - const alert = currify(Dialog.alert, TITLE); - const refresh = currify(_refresh); - const rename = currify(_rename); - - let Menu; - - const EditNames = exec.bind(); - const ConfigView = { - beforeClose: () => { - exec.ifExist(Menu, 'hide'); - DOM.Events.remove('keydown', keyListener); - EditNames.isChanged(); - } - }; - - function init(callback) { - const editor = store(); - - const getMainEditor = () => CloudCmd.Edit.getEditor(); - const getEditor = squad(editor, getMainEditor); - const listeners = squad(setListeners, editor); - - const show = callback ? exec : EditNames.show; - - exec.series([ - CloudCmd.Edit, - call(getEditor), - call(listeners), - show, - ], callback); - } - - EditNames.show = (options) => { - const names = getActiveNames().join('\n'); - const config = { - ...ConfigView, - ...options, - }; - - if (Info.name === '..' && names.length === 1) - return Dialog.alert.noFiles(TITLE); - - DOM.Events.addKey(keyListener); - - CloudCmd.Edit - .getEditor() - .setValueFirst('edit-names', names) - .setMode() - .setOption('keyMap', 'default') - .disableKey(); - - CloudCmd.Edit.show(config); - - return CloudCmd.Edit; - }; - - function keyListener(event) { - const ctrl = event.ctrlKey; - const meta = event.metaKey; - const ctrlMeta = ctrl || meta; - const Key = CloudCmd.Key; - - if (!ctrlMeta || event.keyCode !== Key.S) - return; - - EditNames.hide(); - } - - function getActiveNames() { - return DOM.getFilenames(DOM.getActiveFiles()); - } - - EditNames.hide = () => { - CloudCmd.Edit.hide(); - }; - - function setListeners() { - const element = CloudCmd.Edit.getElement(); - - DOM.Events.addOnce('contextmenu', element, setMenu); - } - - function applyNames() { - const dir = Info.dirPath; - const from = getActiveNames(); - const nameIndex = from.indexOf(Info.name); - - const editor = CloudCmd.Edit.getEditor(); - const to = editor - .getValue() - .split('\n'); - - const root = CloudCmd.config('root'); - - Promise.resolve(root) - .then(rename(dir, from, to)) - .then(refresh(to, nameIndex)) - .catch(alert); - } - - function _refresh(to, nameIndex, res) { - if (res.status === 404) - return res.text().then(reject); - - const currentName = to[nameIndex]; - - CloudCmd.refresh({ - currentName - }); - } - - function getDir(root, dir) { - if (root === '/') - return dir; - - return root + dir; - } - - function _rename(path, from, to, root) { - const dir = getDir(root, path); - - return fetch(CloudCmd.PREFIX + '/rename', { - method: 'put', - credentials: 'include', - body: JSON.stringify({ - from, - to, - dir, - }) - }); - } - - function setMenu(event) { - const position = { - x: event.clientX, - y: event.clientY - }; - - event.preventDefault(); - - if (Menu) - return; - - const editor = CloudCmd.Edit.getEditor(); - const options = { - beforeShow: (params) => { - params.x -= 18; - params.y -= 27; - }, - - afterClick: () => { - editor.focus(); - } - }; - - const menuData = { - 'Save Ctrl+S' : () => { - editor.save(); - EditNames.hide(); - }, - 'Go To Line Ctrl+G' : () => { - editor.goToLine(); - }, - 'Cut Ctrl+X' : () => { - editor.cutToClipboard(); - }, - 'Copy Ctrl+C' : () => { - editor.copyToClipboard(); - }, - 'Paste Ctrl+V' : () => { - editor.pasteFromClipboard(); - }, - 'Delete Del' : () => { - editor.remove('right'); - }, - 'Select All Ctrl+A' : () => { - editor.selectAll(); - }, - 'Close Esc' : () => { - EditNames.hide(); - } - }; - - const element = CloudCmd.Edit.getElement(); - - Menu = supermenu(element, options, menuData); - Menu.show(position.x, position.y); - } - - EditNames.isChanged = () => { - const editor = CloudCmd.Edit.getEditor(); - const msg = 'Apply new names?'; - - if (!editor.isChanged()) - return; - - Dialog.confirm(TITLE, msg, {cancel: false}) - .then(applyNames); - }; - - setTimeout(wraptile(init, callback)); - - return EditNames; +CloudCmd.EditNames = { + init, + show, + hide, + isChanged, }; +const Info = DOM.CurrentInfo; +const {Dialog} = DOM; + +let Menu; + +const ConfigView = { + beforeClose: async () => { + exec.ifExist(Menu, 'hide'); + DOM.Events.remove('keydown', keyListener); + await isChanged(); + }, +}; + +export async function init() { + await CloudCmd.Edit(); + + setListeners(); +} + +export function show(options) { + const names = getActiveNames().join('\n'); + const config = { + ...ConfigView, + ...options, + }; + + if (Info.name === '..' && names.length === 1) + return Dialog.alert.noFiles(); + + DOM.Events.addKey(keyListener); + + CloudCmd.Edit + .getEditor() + .setValueFirst('edit-names', names) + .setMode() + .setOption('keyMap', 'default') + .disableKey(); + + CloudCmd.Edit.show(config); + + return CloudCmd.Edit; +} + +async function keyListener(event) { + const ctrl = event.ctrlKey; + const meta = event.metaKey; + const ctrlMeta = ctrl || meta; + const {Key} = CloudCmd; + + if (ctrlMeta && event.keyCode === Key.S) { + hide(); + return; + } + + if (ctrlMeta && event.keyCode === Key.P) { + const [, pattern] = await Dialog.prompt('Apply pattern:', '[n][e]'); + pattern && applyPattern(pattern); + } +} + +function applyPattern(pattern) { + const newNames = multiRename(pattern, getActiveNames()); + const editor = CloudCmd.Edit.getEditor(); + + editor.setValue(newNames.join('\n')); +} + +function getActiveNames() { + return DOM.getFilenames(DOM.getActiveFiles()); +} + +export function hide() { + CloudCmd.Edit.hide(); +} + +function setListeners() { + const element = CloudCmd.Edit.getElement(); + + DOM.Events.addOnce('contextmenu', element, setMenu); +} + +async function applyNames() { + const dir = Info.dirPath; + const from = getActiveNames(); + const nameIndex = from.indexOf(Info.name); + + const editor = CloudCmd.Edit.getEditor(); + const to = editor + .getValue() + .split('\n'); + + const root = CloudCmd.config('root'); + + const response = rename(dir, from, to, root); + const [error] = await tryToCatch(refresh, to, nameIndex, response); + + if (error) + alert(error); +} + +function refresh(to, nameIndex, res) { + if (res.status === 404) { + const error = res.text(); + throw error; + } + + const currentName = to[nameIndex]; + + CloudCmd.refresh({ + currentName, + }); +} + +function getDir(root, dir) { + if (root === '/') + return dir; + + return root + dir; +} + +function rename(path, from, to, root) { + const dir = getDir(root, path); + const {prefix} = CloudCmd; + + return fetch(`${prefix}/rename`, { + method: 'put', + credentials: 'include', + body: JSON.stringify({ + from, + to, + dir, + }), + }); +} + +function setMenu(event) { + const position = { + x: event.clientX, + y: event.clientY, + }; + + event.preventDefault(); + + if (Menu) + return; + + const editor = CloudCmd.Edit.getEditor(); + + const options = { + beforeShow: (params) => { + params.x -= 18; + params.y -= 27; + }, + + afterClick: () => { + editor.focus(); + }, + }; + + const menuData = { + 'Save Ctrl+S': async () => { + await applyNames(); + hide(); + }, + 'Go To Line Ctrl+G': () => { + editor.goToLine(); + }, + 'Cut Ctrl+X': () => { + editor.cutToClipboard(); + }, + 'Copy Ctrl+C': () => { + editor.copyToClipboard(); + }, + 'Paste Ctrl+V': () => { + editor.pasteFromClipboard(); + }, + 'Delete Del': () => { + editor.remove('right'); + }, + 'Select All Ctrl+A': () => { + editor.selectAll(); + }, + 'Close Esc': hide, + }; + + const element = CloudCmd.Edit.getElement(); + + Menu = supermenu(element, options, menuData); + + Menu.addContextMenuListener(); + Menu.show(position.x, position.y); +} + +export async function isChanged() { + const editor = CloudCmd.Edit.getEditor(); + const msg = 'Apply new names?'; + + if (!editor.isChanged()) + return; + + const [cancel] = await Dialog.confirm(msg); + + !cancel && await applyNames(); +} diff --git a/client/modules/edit.js b/client/modules/edit.js index 52a6bc19..593a11cb 100644 --- a/client/modules/edit.js +++ b/client/modules/edit.js @@ -1,138 +1,127 @@ /* global CloudCmd */ +import {montag} from 'montag'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import createElement from '@cloudcmd/create-element'; +import load from 'load.js'; +import {MAX_FILE_SIZE as maxSize} from '#common/cloudfunc'; +import {time, timeEnd} from '#common/util'; -'use strict'; - -const exec = require('execon'); -const currify = require('currify/legacy'); - -const load = require('../dom/load'); - -const {MAX_FILE_SIZE: maxSize} = require('../../common/cloudfunc'); -const {time, timeEnd} = require('../../common/util'); - -CloudCmd.Edit = EditProto; - -function EditProto(callback) { - const Name = 'Edit'; - const EditorName = CloudCmd.config('editor'); - const loadFiles = currify(_loadFiles); - - let Loading = true; - let Element; - let editor; - - const ConfigView = { - afterShow: () => { - editor - .moveCursorTo(0, 0) - .focus(); - } - }; - - const Edit = exec.bind(); - - function init(callback) { - const element = createElement(); - - exec.series([ - CloudCmd.View, - loadFiles(element) - ], callback); - } - - function createElement() { - const element = load({ - name: 'div', - style: - 'width : 100%;' + - 'height : 100%;' + - 'font-family: "Droid Sans Mono";' + - 'position : absolute;', - notAppend: true - }); - - Element = element; - - return element; - } - - function checkFn(name, fn) { - if (typeof fn !== 'function') - throw Error(name + ' should be a function!'); - } - - function initConfig(options = {}) { - const config = Object.assign({}, options, ConfigView); - - if (!options.afterShow) - return config; - - checkFn('options.afterShow', options.afterShow); - - const afterShow = {config}; - - config.afterShow = () => { - afterShow(); - options.afterShow(); - }; - - return config; - } - - Edit.show = (options) => { - if (Loading) - return; - - CloudCmd.View.show(Element, initConfig(options)); - - Edit.getEditor() - .setOptions({ - fontSize: 16, - }); - - return Edit; - }; - - Edit.getEditor = () => { - return editor; - }; - - Edit.getElement = () => { - return Element; - }; - - Edit.hide = () => { - CloudCmd.View.hide(); - return Edit; - }; - - function _loadFiles(element, callback) { - const socketPath = CloudCmd.PREFIX; - const prefix = socketPath + '/' + EditorName; - const url = prefix + '/' + EditorName + '.js'; - - time(Name + ' load'); - - load.js(url, () => { - const word = window[EditorName]; - const options = { - maxSize, - prefix, - socketPath, - }; - - word(element, options, (ed) => { - timeEnd(Name + ' load'); - editor = ed; - Loading = false; - - exec(callback); - }); - }); - } - - init(callback); - - return Edit; +export function getEditor() { + return editor; } +const isFn = (a) => typeof a === 'function'; +const loadJS = load.js; + +const Name = 'Edit'; + +CloudCmd[Name] = { + init, + show, + hide, + getEditor, + getElement, +}; + +const EditorName = CloudCmd.config('editor'); + +let Loading = true; +let Element; +let editor; + +const ConfigView = { + afterShow: () => { + editor + .moveCursorTo(0, 0) + .focus(); + }, +}; + +export async function init() { + const element = create(); + + await CloudCmd.View(); + await loadFiles(element); +} + +function create() { + const element = createElement('div', { + style: montag` + width: 100%; + height: 100%; + font-family: "Droid Sans Mono"; + `, + notAppend: true, + }); + + Element = element; + + return element; +} + +function checkFn(name, fn) { + if (!isFn(fn)) + throw Error(`${name} should be a function!`); +} + +function initConfig(options = {}) { + const config = { + ...options, + ...ConfigView, + }; + + if (!options.afterShow) + return config; + + checkFn('options.afterShow', options.afterShow); + + config.afterShow = () => { + ConfigView.afterShow(); + options.afterShow(); + }; + + return config; +} + +export function show(options) { + if (Loading) + return; + + CloudCmd.View.show(Element, initConfig(options)); + + getEditor().setOptions({ + fontSize: 16, + }); +} + +export function getElement() { + return Element; +} + +export function hide() { + CloudCmd.View.hide(); +} + +const loadFiles = async (element) => { + const prefix = `${CloudCmd.prefix}/${EditorName}`; + const socketPath = CloudCmd.prefix; + const prefixSocket = `${CloudCmd.prefixSocket}/${EditorName}`; + const url = `${prefix}/${EditorName}.js`; + + time(`${Name} load`); + + await loadJS(url); + + const word = promisify(window[EditorName]); + const [ed] = await tryToCatch(word, element, { + maxSize, + prefix, + prefixSocket, + socketPath, + }); + + timeEnd(`${Name} load`); + editor = ed; + Loading = false; +}; diff --git a/client/modules/help.js b/client/modules/help.js index 29de96fa..74657a90 100644 --- a/client/modules/help.js +++ b/client/modules/help.js @@ -1,34 +1,27 @@ -'use strict'; +import * as Images from '#dom/images'; -/* global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.Help = HelpProto; +CloudCmd.Help = { + init, + show, + hide, +}; -const Images = require('../dom/images'); - -function HelpProto() { +export function init() { Images.show.load('top'); - show(); - - return exports; } -module.exports.show = show; -module.exports.hide = hide; - -function show() { +export function show() { const positionLoad = 'top'; const relative = true; - CloudCmd - .Markdown - .show('/HELP.md', { - positionLoad, - relative, - }); + CloudCmd.Markdown.show('/HELP.md', { + positionLoad, + relative, + }); } -function hide() { +export function hide() { CloudCmd.View.hide(); } - diff --git a/client/modules/konsole.js b/client/modules/konsole.js index 574d8070..e9a465af 100644 --- a/client/modules/konsole.js +++ b/client/modules/konsole.js @@ -1,158 +1,143 @@ -'use strict'; - /* global CloudCmd */ /* global Util */ /* global DOM */ /* global Console */ +import exec from 'execon'; +import currify from 'currify'; +import {tryToCatch} from 'try-to-catch'; +import {js as loadJS} from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import * as Images from '#dom/images'; -const exec = require('execon'); -const currify = require('currify/legacy'); -const Images = require('../dom/images'); -const { - Dialog, - CurrentInfo:Info, -} = DOM; +CloudCmd.Konsole = { + init, + show, + hide, +}; + +const {Dialog, CurrentInfo: Info} = DOM; const rmLastSlash = (a) => a.replace(/\/$/, '') || '/'; -CloudCmd.Konsole = ConsoleProto; +let konsole; +const {config} = CloudCmd; -function ConsoleProto() { - let konsole; - const {config} = CloudCmd; - - const noop = () => {}; - const cd = currify((fn, dir) => fn(`cd ${rmLastSlash(dir)}`)); - +const cd = currify((fn, dir) => fn(`cd ${rmLastSlash(dir)}`)); + +const Name = 'Konsole'; + +let Element; +let Loaded; + +export async function init() { if (!config('console')) - return { - show: noop - }; + return; - const Name = 'Konsole'; - const TITLE = 'Console'; + Images.show.load('top'); - let Element; - let Loaded; - - const Konsole = this; - - function init() { - Images.show.load('top'); - - exec.series([ - CloudCmd.View, - load, - create, - Konsole.show, - ]); - - Element = DOM.load({ - name : 'div', - className : 'console' - }); - } - - this.hide = () => { - CloudCmd.View.hide(); - }; - - this.clear = () => { - konsole.clear(); - }; - - function getPrefix() { - return CloudCmd.PREFIX + '/console'; - } - - function getEnv() { - return { - ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), - PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), - CURRENT_NAME: DOM.getCurrentName.bind(DOM), - CURRENT_PATH: () => { - return Info.path; - } - }; - } - - function onPath(path) { - if (Info.dirPath === path) - return; - - CloudCmd.loadDir({ - path, - }); - } - - const getDirPath = () => { - if (config('syncConsolePath')) - return Info.dirPath; - }; - - function create(callback) { - const options = { - cwd: getDirPath(), - env: getEnv(), - prefix: getPrefix(), - socketPath: CloudCmd.PREFIX, - }; - - konsole = Console(Element, options, (spawn) => { - spawn.on('connect', exec.with(authCheck, spawn)); - spawn.on('path', config.if('syncConsolePath', onPath)); - - CloudCmd.on('active-dir', config.if('syncConsolePath', cd(spawn.handler))); - - exec(callback); - }); - - konsole.addShortCuts({ - 'P': () => { - const command = konsole.getPromptText(); - const path = DOM.getCurrentDirPath(); - - konsole.setPromptText(command + path); - } - }); - } - - function authCheck(spawn) { - spawn.emit('auth', config('username'), config('password')); - - spawn.on('reject', () => { - Dialog.alert(TITLE, 'Wrong credentials!'); - }); - } - - this.show = (callback) => { - if (!Loaded) - return; - - CloudCmd.View.show(Element, { - afterShow: () => { - konsole.focus(); - exec(callback); - } - }); - }; - - function load(callback) { - const prefix = getPrefix(); - const url = prefix + '/console.js'; - - DOM.load.js(url, (error) => { - if (error) - return Dialog.alert(TITLE, error.message); - - Loaded = true; - Util.timeEnd(Name + ' load'); - exec(callback); - }); - - Util.time(Name + ' load'); - } - - init(); + await CloudCmd.View(); + await load(); + await create(); } +export function hide() { + CloudCmd.View.hide(); +} + +export const clear = () => { + konsole.clear(); +}; + +const getPrefix = () => CloudCmd.prefix + '/console'; + +function getPrefixSocket() { + return CloudCmd.prefixSocket + '/console'; +} + +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath.bind(DOM), + PASSIVE_DIR: DOM.getNotCurrentDirPath.bind(DOM), + CURRENT_NAME: DOM.getCurrentName.bind(DOM), + CURRENT_PATH: () => Info.path, +}); + +async function onPath(path) { + if (Info.dirPath === path) + return; + + await CloudCmd.changeDir(path); +} + +const getDirPath = () => { + if (config('syncConsolePath')) + return Info.dirPath; +}; + +const create = async () => { + const options = { + cwd: getDirPath(), + env: getEnv(), + prefix: getPrefix(), + prefixSocket: getPrefixSocket(), + socketPath: CloudCmd.prefix, + }; + + Element = createElement('div', { + className: 'console', + }); + + konsole = await Console(Element, options); + + konsole.on('connect', exec.with(authCheck, konsole)); + konsole.on('path', config.if('syncConsolePath', onPath)); + + CloudCmd.on('active-dir', config.if('syncConsolePath', cd(konsole.handler))); + + konsole.addShortCuts({ + P: () => { + const command = konsole.getPromptText(); + const path = DOM.getCurrentDirPath(); + + konsole.setPromptText(command + path); + }, + }); +}; + +function authCheck(konsole) { + konsole.emit('auth', config('username'), config('password')); + + konsole.on('reject', () => { + Dialog.alert('Wrong credentials!'); + }); +} + +export function show(callback) { + if (!Loaded) + return; + + if (!config('console')) + return; + + CloudCmd.View.show(Element, { + afterShow: () => { + konsole.focus(); + exec(callback); + }, + }); +} + +const load = async () => { + Util.time(`${Name} load`); + + const prefix = getPrefix(); + const url = `${prefix}/console.js`; + const [error] = await tryToCatch(loadJS, url); + + Loaded = true; + Util.timeEnd(`${Name} load`); + + if (error) + return Dialog.alert(error.message, { + cancel: false, + }); +}; diff --git a/client/modules/markdown.js b/client/modules/markdown.js index 7d23af50..c94c64e8 100644 --- a/client/modules/markdown.js +++ b/client/modules/markdown.js @@ -1,58 +1,47 @@ -'use strict'; +import createElement from '@cloudcmd/create-element'; +import * as Images from '#dom/images'; +import {Markdown} from '#dom/rest'; +import {alert} from '#dom/dialog'; -/*global CloudCmd */ +const {CloudCmd} = globalThis; -CloudCmd.Markdown = MarkdownProto; - -const exec = require('execon'); - -const Images = require('../dom/images'); -const load = require('../dom/load'); -const {Markdown} = require('../dom/rest'); - -function MarkdownProto(name, options) { - Images.show.load('top'); - - exec.series([ - CloudCmd.View, - exec.with(show, name, options), - ]); - - return module.exports; -} - -module.exports.show = show; - -module.exports.hide = () => { - CloudCmd.View.hide(); +CloudCmd.Markdown = { + init, + show, + hide, }; +export async function init() { + Images.show.load('top'); + await CloudCmd.View(); +} -function show(name, options = {}) { - const relativeQuery = '?relative'; - const { - positionLoad, - relative, - } = options; +export function hide() { + CloudCmd.View.hide(); +} + +export async function show(name, options = {}) { + const {positionLoad, relative} = options; Images.show.load(positionLoad); if (relative) - name += relativeQuery; + name += '?relative'; - Markdown.read(name, (error, inner) => { - const name = 'div'; - const className = 'help'; - - const div = load({ - name, - className, - inner, + const [error, innerHTML] = await Markdown.read(name); + Images.hide(); + + if (error) + return alert(error.message, { + cancel: false, }); - - Images.hide(); - - CloudCmd.View.show(div); + + const className = 'help'; + + const div = createElement('div', { + className, + innerHTML, }); + + CloudCmd.View.show(div); } - diff --git a/client/modules/menu.js b/client/modules/menu.js deleted file mode 100644 index b94b3a87..00000000 --- a/client/modules/menu.js +++ /dev/null @@ -1,335 +0,0 @@ -/* global CloudCmd, DOM */ - -'use strict'; - -CloudCmd.Menu = MenuProto; - -const exec = require('execon'); -const wrap = require('wraptile/legacy'); -const supermenu = require('supermenu'); - -const {FS} = require('../../common/cloudfunc'); -const load = require('../dom/load'); -const RESTful = require('../dom/rest'); - -function MenuProto(position) { - const config = CloudCmd.config; - const Buffer = DOM.Buffer; - const Info = DOM.CurrentInfo; - const Key = CloudCmd.Key; - const Events = DOM.Events; - const Dialog = DOM.Dialog; - const Images = DOM.Images; - const Menu = this; - const TITLE = 'Cloud Commander'; - const alertNoFiles = wrap(Dialog.alert.noFiles)(TITLE); - const uploadTo = wrap(_uploadTo); - - let MenuShowedName; - let MenuContext; - let MenuContextFile; - - this.ENABLED = false; - - function init(position) { - const {isAuth, menuDataFile} = getFileMenuData(); - - const NOT_FILE = true; - const fm = DOM.getFM(); - const menuData = getMenuData(isAuth); - const options = getOptions(NOT_FILE); - const optionsFile = getOptions(); - - MenuContext = supermenu(fm, options, menuData); - MenuContextFile = supermenu(fm, optionsFile, menuDataFile); - - Menu.show(position); - Events.addKey(listener); - } - - this.hide = () => { - MenuContext.hide(); - MenuContextFile.hide(); - }; - - this.show = (position) => { - const {x, y} = getPosition(position); - - MenuContext.show(x, y); - MenuContextFile.show(x, y); - - Images.hide(); - }; - - function getPosition(position) { - if (position) - return { - x: position.x, - y: position.y, - }; - - return getCurrentPosition(); - } - - function getMenuNameByEl(el) { - if (!el) - return 'context'; - - const name = DOM.getCurrentName(el); - - if (name === '..') - return 'context'; - - return 'contextFile'; - } - - function getOptions(notFile) { - let name, func; - - if (notFile) { - name = 'context'; - func = Key.unsetBind; - } else { - name = 'contextFile'; - } - - const options = { - icon : true, - beforeClose : Key.setBind, - beforeShow : exec.with(beforeShow, func), - beforeClick, - name, - }; - - return options; - } - - function getMenuData(isAuth) { - const menu = { - 'Paste': Buffer.paste, - 'New': { - 'File': DOM.promptNewFile, - 'Directory': DOM.promptNewDir - }, - 'Upload': () => { - CloudCmd.Upload.show(); - }, - 'Upload From Cloud': uploadFromCloud, - '(Un)Select All': DOM.toggleAllSelectedFiles - }; - - if (isAuth) - menu['Log Out'] = CloudCmd.logOut; - - return menu; - } - - function getFileMenuData() { - const isAuth = CloudCmd.config('auth'); - const show = wrap((name) => { - CloudCmd[name].show(); - }); - - const menuBottom = getMenuData(isAuth); - const menuTop = { - 'View': show('View'), - 'Edit': show('EditFile'), - 'Rename': () => { - setTimeout(DOM.renameCurrent, 100); - }, - 'Delete': () => { - CloudCmd.Operation.show('delete'); - }, - 'Pack': () => { - CloudCmd.Operation.show('pack'); - }, - 'Extract': () => { - CloudCmd.Operation.show('extract'); - }, - 'Download': preDownload, - 'Upload To Cloud': uploadTo('Cloud'), - 'Cut': () => { - isCurrent(Buffer.cut, alertNoFiles); - }, - 'Copy': () => { - isCurrent(Buffer.copy, alertNoFiles); - }, - }; - - const menuDataFile = { - ...menuTop, - ...menuBottom, - }; - - return { - isAuth, - menuDataFile, - }; - } - - function isCurrent(yesFn, noFn) { - if (Info.name !== '..') - return yesFn(); - - noFn(); - } - - function isPath(x, y) { - const {panel} = Info; - const isEmptyRoot = !panel; - - if (isEmptyRoot) - return false; - - const el = document.elementFromPoint(x, y); - const elements = panel.querySelectorAll('[data-name="js-path"] *'); - const is = ~[].indexOf.call(elements, el); - - return is; - } - - function beforeShow(callback, params) { - const name = params.name; - let el = DOM.getCurrentByPosition({ - x: params.x, - y: params.y - }); - - const menuName = getMenuNameByEl(el); - let notShow = menuName === 'contextFile'; - - if (params.name === 'contextFile') { - notShow = !notShow; - } - - if (!notShow) - MenuShowedName = name; - - exec(callback); - - if (!notShow) - notShow = isPath(params.x, params.y); - - return notShow; - } - - function beforeClick(name) { - return MenuShowedName !== name; - } - - function _uploadTo(nameModule) { - Info.getData((error, data) => { - if (error) - return; - - const name = Info.name; - const execFrom = CloudCmd.execFromModule; - - execFrom(nameModule, 'uploadFile', name, data); - }); - - CloudCmd.log('Uploading to ' + name + '...'); - } - - function uploadFromCloud() { - Images.show.load('top'); - - CloudCmd.execFromModule('Cloud', 'saveFile', (currentName, data) => { - const path = DOM.getCurrentDirPath() + currentName; - - RESTful.write(path, data, (error) => { - if (error) - return; - - CloudCmd.refresh({currentName}); - }); - }); - } - - function preDownload() { - download(config('packer')); - } - - function download(type) { - const TIME = 30 * 1000; - const prefixUr = CloudCmd.PREFIX_URL; - const PACK = '/pack'; - const date = Date.now(); - const files = DOM.getActiveFiles(); - - if (!files.length) - return alertNoFiles(); - - files.forEach((file) => { - const selected = DOM.isSelected(file); - const isDir = DOM.isCurrentIsDir(file); - const path = DOM.getCurrentPath(file); - - CloudCmd.log('downloading file ' + path + '...'); - /* - * if we send ajax request - - * no need in hash so we escape # - * and all other characters, like "%" - */ - const encodedPath = encodeURI(path).replace(/#/g, '%23'); - const id = load.getIdBySrc(path); - - let src; - - if (isDir) - src = prefixUr + PACK + encodedPath + DOM.getPackerExt(type); - else - src = prefixUr + FS + encodedPath + '?download'; - - const element = load({ - id : id + '-' + date, - name : 'iframe', - async : false, - className : 'hidden', - src, - }); - - const {body} = document; - const removeChild = body.removeChild.bind(body, element); - - setTimeout(removeChild, TIME); - - if (selected) - DOM.toggleSelectedFile(file); - }); - } - - function getCurrentPosition() { - const current = Info.element; - const rect = current.getBoundingClientRect(); - - const position = { - x: Math.round(rect.left + rect.width / 3), - y: Math.round(rect.top) - }; - - return position; - } - - function listener(event) { - const F9 = Key.F9; - const ESC = Key.ESC; - const key = event.keyCode; - const isBind = Key.isBind(); - - if (!isBind) - return; - - if (key === ESC) - return Menu.hide(); - - if (key === F9) { - const position = getCurrentPosition(); - MenuContext.show(position.x, position.y); - - event.preventDefault(); - } - } - - init(position); -} diff --git a/client/modules/menu/cloudmenu.js b/client/modules/menu/cloudmenu.js new file mode 100644 index 00000000..8f7c1e68 --- /dev/null +++ b/client/modules/menu/cloudmenu.js @@ -0,0 +1,31 @@ +import {supermenu} from 'supermenu'; + +const noop = () => {}; +const {CloudCmd} = globalThis; + +export const createCloudMenu = async (fm, options, menuData) => { + const createMenu = await loadMenu(); + const menu = await createMenu(fm, options, menuData); + + menu.addContextMenuListener = menu.addContextMenuListener || noop; + + return menu; +}; + +async function loadMenu() { + if (CloudCmd.config('menu') === 'aleman') { + const {prefix} = CloudCmd; + const {host, protocol} = globalThis.location; + const url = `${protocol}//${host}${prefix}/node_modules/aleman/menu/menu.js`; + const {createMenu} = await import(/* webpackIgnore: true */url); + + return createMenu; + } + + return createSupermenu; +} + +function createSupermenu(name, options, menuData) { + const element = document.querySelector('[data-name="js-fm"]'); + return supermenu(element, options, menuData); +} diff --git a/client/modules/menu/index.js b/client/modules/menu/index.js new file mode 100644 index 00000000..7235591e --- /dev/null +++ b/client/modules/menu/index.js @@ -0,0 +1,355 @@ +import exec from 'execon'; +import wrap from 'wraptile'; +import createElement from '@cloudcmd/create-element'; +import {getIdBySrc} from '#dom/load'; +import * as RESTful from '#dom/rest'; +import {FS} from '#common/cloudfunc'; + +const {CloudCmd, DOM} = globalThis; +const {config, Key} = CloudCmd; + +const { + Buffer, + Events, + Dialog, + Images, +} = DOM; + +const Info = DOM.CurrentInfo; +const alertNoFiles = Dialog.alert.noFiles; +const uploadTo = wrap(_uploadTo); + +let MenuShowedName; +let MenuContext; +let MenuContextFile; + +export const ENABLED = false; + +CloudCmd.Menu = { + init, + show, + hide, +}; + +export async function init() { + const {isAuth, menuDataFile} = getFileMenuData(); + + const fm = DOM.getFM(); + const menuData = getMenuData(isAuth); + + const options = getOptions({ + type: 'context', + }); + + const optionsFile = getOptions({ + type: 'file', + }); + + const {createCloudMenu} = await import('./cloudmenu.js'); + + const {name} = fm.dataset; + + MenuContext = await createCloudMenu(name, options, menuData); + MenuContextFile = await createCloudMenu(name, optionsFile, menuDataFile); + + MenuContext.addContextMenuListener(); + MenuContextFile.addContextMenuListener(); + + Events.addKey(listener); +} + +export function hide() { + MenuContext.hide(); + MenuContextFile.hide(); +} + +export function show(position) { + const {x, y} = getPosition(position); + + MenuContext.show(x, y); + MenuContextFile.show(x, y); + + Images.hide(); +} + +function getPosition(position) { + if (position) + return { + x: position.x, + y: position.y, + }; + + return getCurrentPosition(); +} + +function getMenuNameByEl(el) { + if (!el) + return 'context'; + + const name = DOM.getCurrentName(el); + + if (name === '..') + return 'context'; + + return 'contextFile'; +} + +function getOptions({type}) { + let name; + let func; + + if (type === 'context') { + name = 'context'; + func = Key.unsetBind; + } else if (type === 'file') { + name = 'contextFile'; + } + + const options = { + icon: true, + infiniteScroll: false, + beforeClose: Key.setBind, + beforeHide: Key.setBind, + beforeShow: exec.with(beforeShow, func), + beforeClick, + name, + }; + + return options; +} + +function getMenuData(isAuth) { + const menu = { + 'Paste': Buffer.paste, + 'New': { + File: DOM.promptNewFile, + Directory: DOM.promptNewDir, + }, + 'Upload': () => { + CloudCmd.Upload.show(); + }, + 'Upload From Cloud': uploadFromCloud, + 'Toggle File Selection': DOM.toggleSelectedFile, + '(Un)Select All': DOM.toggleAllSelectedFiles, + }; + + if (isAuth) + menu['Log Out'] = CloudCmd.logOut; + + return menu; +} + +function getFileMenuData() { + const isAuth = CloudCmd.config('auth'); + + const menuBottom = getMenuData(isAuth); + const menuTop = { + 'View': () => { + CloudCmd.View.show(); + }, + 'Edit': () => { + const name = config('vim') ? 'EditFileVim' : 'EditFile'; + CloudCmd[name].show(); + }, + 'Rename': () => { + setTimeout(DOM.renameCurrent, 100); + }, + 'Delete': () => { + CloudCmd.Operation.show('delete'); + }, + 'Pack': () => { + CloudCmd.Operation.show('pack'); + }, + 'Extract': () => { + CloudCmd.Operation.show('extract'); + }, + 'Download': preDownload, + 'Upload To Cloud': uploadTo('Cloud'), + 'Cut': () => { + isCurrent(Buffer.cut, alertNoFiles); + }, + 'Copy': () => { + isCurrent(Buffer.copy, alertNoFiles); + }, + }; + + const menuDataFile = { + ...menuTop, + ...menuBottom, + }; + + return { + isAuth, + menuDataFile, + }; +} + +function isCurrent(yesFn, noFn) { + if (Info.name !== '..') + return yesFn(); + + noFn(); +} + +function isPath(x, y) { + const {panel} = Info; + const isEmptyRoot = !panel; + + if (isEmptyRoot) + return false; + + const el = document.elementFromPoint(x, y); + const elements = panel.querySelectorAll('[data-name="js-path"] *'); + + return !~[].indexOf.call(elements, el); +} + +function beforeShow(callback, params) { + Key.unsetBind(); + + const { + name, + position = { + x: params.x, + y: params.y, + }, + } = params; + + const {x, y} = position; + + const el = DOM.getCurrentByPosition({ + x, + y, + }); + + const menuName = getMenuNameByEl(el); + + let isShow = menuName !== 'contextFile'; + + if (params.name === 'contextFile') + isShow = !isShow; + + if (isShow) + MenuShowedName = name; + + exec(callback); + + if (isShow) + isShow = isPath(x, y); + + return isShow; +} + +const beforeClick = (name) => MenuShowedName !== name; + +async function _uploadTo(nameModule) { + const [error, data] = await Info.getData(); + + if (error) + return; + + const {name} = Info; + + CloudCmd.execFromModule(nameModule, 'uploadFile', name, data); + CloudCmd.log(`Uploading to ${name}...`); +} + +function uploadFromCloud() { + Images.show.load('top'); + + CloudCmd.execFromModule('Cloud', 'saveFile', async (currentName, data) => { + const path = DOM.getCurrentDirPath() + currentName; + const [e] = await RESTful.write(path, data); + + if (e) + return; + + await CloudCmd.refresh({ + currentName, + }); + }); +} + +function preDownload() { + download(config('packer')); +} + +function download(type) { + const TIME = 30 * 1000; + const {prefixURL} = CloudCmd; + const PACK = '/pack'; + const date = Date.now(); + const files = DOM.getActiveFiles(); + + if (!files.length) + return alertNoFiles(); + + for (const file of files) { + const selected = DOM.isSelected(file); + const isDir = DOM.isCurrentIsDir(file); + const path = DOM.getCurrentPath(file); + + CloudCmd.log(`downloading file ${path}...`); + + /* + * if we send ajax request - + * no need in hash so we escape # + * and all other characters, like "%" + */ + const encodedPath = encodeURI(path).replace(/#/g, '#'); + const id = getIdBySrc(path); + + let src; + + if (isDir) + src = prefixURL + PACK + encodedPath + DOM.getPackerExt(type); + else + src = prefixURL + FS + encodedPath + '?download'; + + const element = createElement('iframe', { + id: id + '-' + date, + async: false, + className: 'hidden', + src, + }); + + const {body} = document; + const removeChild = body.removeChild.bind(body, element); + + setTimeout(removeChild, TIME); + + if (selected) + DOM.toggleSelectedFile(file); + } +} + +function getCurrentPosition() { + const current = Info.element; + const rect = current.getBoundingClientRect(); + + const position = { + x: Math.round(rect.left + rect.width / 3), + y: Math.round(rect.top), + }; + + return position; +} + +function listener(event) { + const {F9, ESC} = Key; + + const key = event.keyCode; + const isBind = Key.isBind(); + + if (key === ESC) { + Key.setBind(); + return hide(); + } + + if (isBind && key === F9) { + const position = getCurrentPosition(); + MenuContext.show(position.x, position.y); + + event.preventDefault(); + } +} diff --git a/client/modules/operation/format.js b/client/modules/operation/format.js new file mode 100644 index 00000000..c639799e --- /dev/null +++ b/client/modules/operation/format.js @@ -0,0 +1,6 @@ +export const format = (operation, from, to) => { + if (!to) + return `${operation} ${from}`; + + return `${operation} ${from} -> ${to}`; +}; diff --git a/client/modules/operation/get-next-current-name.js b/client/modules/operation/get-next-current-name.js index dc2bed5e..63445888 100644 --- a/client/modules/operation/get-next-current-name.js +++ b/client/modules/operation/get-next-current-name.js @@ -1,19 +1,16 @@ -'use strict'; +import currify from 'currify'; -const currify = require('currify/legacy'); - -const not = currify((array, value) => !~array.indexOf(value)); +const not = currify((array, value) => !array.includes(value)); const notOneOf = currify((a, b) => a.filter(not(b))); -module.exports = (currentName, names, removedNames) => { +export const getNextCurrentName = (currentName, names, removedNames) => { const i = names.indexOf(currentName); const nextNames = notOneOf(names, removedNames); - const length = nextNames.length; + const {length} = nextNames; if (nextNames[i]) return nextNames[i]; return nextNames[length - 1]; }; - diff --git a/client/modules/operation/get-next-current-name.spec.js b/client/modules/operation/get-next-current-name.spec.js deleted file mode 100644 index 514ecc29..00000000 --- a/client/modules/operation/get-next-current-name.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const test = require('tape'); -const getNextCurrentName = require('./get-next-current-name'); - -test('get-next-current-name', (t) => { - const names = [ - '..', - 'hello', - '1', - '2', - ]; - - const removedNames = [ - '1' - ]; - - const name = getNextCurrentName('hello', names, removedNames); - - t.equal(name, 'hello', 'should equal'); - t.end(); -}); - -test('get-next-current-name: all files removed', (t) => { - const names = [ - '..', - 'hello', - '1', - '2', - ]; - - const removedNames = [ - '1', - '2', - 'hello', - ]; - - const name = getNextCurrentName('2', names, removedNames); - - t.equal(name, '..', 'should equal'); - t.end(); -}); - diff --git a/client/modules/operation/index.js b/client/modules/operation/index.js index c577ab8c..f349f7d8 100644 --- a/client/modules/operation/index.js +++ b/client/modules/operation/index.js @@ -1,476 +1,516 @@ -/* global CloudCmd */ -/* global Util */ -/* global DOM */ -/* global fileop */ +import currify from 'currify'; +import wraptile from 'wraptile'; +import {promisify} from 'es6-promisify'; +import exec from 'execon'; +import load from 'load.js'; +import {tryToCatch} from 'try-to-catch'; +import {encode} from '#common/entity'; +import {removeExtension} from './remove-extension.js'; +import {setListeners} from './set-listeners.js'; +import {getNextCurrentName} from './get-next-current-name.js'; -'use strict'; - -CloudCmd.Operation = OperationProto; - -const currify = require('currify/legacy'); -const wraptile = require('wraptile/legacy'); -const exec = require('execon'); - -const { - encode, -} = require('../../../common/entity'); - -const RESTful = require('../../dom/rest'); -const removeExtension = require('./remove-extension'); -const setListeners = require('./set-listeners'); -const getNextCurrentName = require('./get-next-current-name'); +const {DOM, CloudCmd} = globalThis; const removeQuery = (a) => a.replace(/\?.*/, ''); -function OperationProto(operation, data) { - const Name = 'Operation'; - const { - TITLE, - config, - } = CloudCmd; - const {Dialog, Images} = DOM; - const initOperations = wraptile(_initOperations); - const authCheck = wraptile(_authCheck); +const Name = 'Operation'; + +const {config} = CloudCmd; +const {Dialog, Images} = DOM; + +const authCheck = wraptile(_authCheck); +const Operation = {}; + +let Loaded; + +let copyFn; +let moveFn; +let deleteFn; +let extractFn; +let packZipFn; +let packTarFn; + +const Info = DOM.CurrentInfo; +const showLoad = Images.show.load.bind(null, 'top'); + +const processFiles = currify(_processFiles); + +const noFilesCheck = () => { + const {length} = DOM.getActiveFiles(); + const is = Boolean(!length); - let Loaded; + if (is) + return Dialog.alert.noFiles(); - let { - cp: copyFn, - mv: moveFn, - delete: deleteFn, - extract: extractFn, - } = RESTful; + return is; +}; + +export const init = promisify((callback) => { + showLoad(); - let packZipFn = RESTful.pack; - let packTarFn = RESTful.pack; - - const Info = DOM.CurrentInfo; - const showLoad = Images.show.load.bind(null, 'top'); - const Operation = this; - const processFiles = currify(_processFiles); - - const noFilesCheck = () => { - const {length} = DOM.getActiveFiles(); - const is = Boolean(!length); - - if (is) - return Dialog.alert.noFiles(TITLE); - - return is; - }; - - function init() { - showLoad(); - - exec.series([ - DOM.loadSocket, - (callback) => { - if (!config('progress')) - return callback(); - - load(initOperations(CloudCmd.PREFIX, callback)); - }, - () => { - Loaded = true; - Images.hide(); - Operation.show(operation, data); - } - ]); - } - - function _authCheck(spawn, ok) { - const accept = wraptile(ok); - const alertDialog = wraptile(Dialog.alert); - - spawn.on('accept', accept(spawn)); - spawn.on('reject', alertDialog (TITLE, 'Wrong credentials!')); - spawn.emit('auth', config('username'), config('password')); - } - - function _initOperations(socketPrefix, fn) { - const prefix = `${socketPrefix}/fileop`; - fileop({prefix, socketPrefix}, (e, operator) => { - fn(); + exec.series([ + DOM.loadSocket, + async (callback) => { + if (config('dropbox')) + return callback(); - operator.on('connect', authCheck(operator, onConnect)); - operator.on('disconnect', onDisconnect); - }); - } - - function onConnect(operator) { - packTarFn = (data, callback) => { - operator.tar(data.from, data.to, data.names) - .then(setListeners({noContinue: true}, callback)); - }; - - packZipFn = (data, callback) => { - operator.zip(data.from, data.to, data.names) - .then(setListeners({noContinue: true}, callback)); - }; - - deleteFn = (from, files, callback) => { - from = removeQuery(from); - operator.remove(from, files) - .then(setListeners(callback)); - }; - - copyFn = (data, callback) => { - operator.copy(data.from, data.to, data.names) - .then(setListeners(callback)); - }; - - moveFn = (data, callback) => { - operator.move(data.from, data.to, data.names) - .then(setListeners(callback)); - }; - - extractFn = (data, callback) => { - operator.extract(data.from, data.to) - .then(setListeners({noContinue: true}, callback)); - }; - } - - function onDisconnect() { - packZipFn = RESTful.pack; - packTarFn = RESTful.pack; - deleteFn = RESTful.delete; - copyFn = RESTful.cp; - moveFn = RESTful.mv; - extractFn = RESTful.extract; - } - - function getPacker(type) { - if (type === 'zip') - return packZipFn; - - return packTarFn; - } - - this.hide = () => { - CloudCmd.View.hide(); - }; - - this.show = (operation, data) => { - if (!Loaded) - return; - - if (operation === 'copy') - return Operation.copy(data); - - if (operation === 'move') - return Operation.move(data); - - if (operation === 'delete') - return Operation.delete(); - - if (operation === 'delete:silent') - return Operation.deleteSilent(); - - if (operation === 'pack') - return Operation.pack(); - - if (operation === 'extract') - return Operation.extract(); - }; - - this.copy = processFiles({ - type: 'copy', - }); - - this.move = processFiles({ - type: 'move' - }); - - this.delete = () => { - promptDelete(); - }; - - this.deleteSilent = () => { - deleteSilent(); - }; - - this.pack = () => { - const isZip = config('packer') === 'zip'; - twopack('pack', isZip ? 'zip' : 'tar'); - }; - - this.extract = () => { - twopack('extract'); - }; - - /** - * prompt and delete current file or selected files - * - * @currentFile - */ - function promptDelete() { - if (noFilesCheck()) - return; - - const msgAsk = 'Do you really want to delete the '; - const msgSel = 'selected '; - - const files = DOM.getActiveFiles(); - const names = DOM.getFilenames(files); - const n = names.length; - - let msg; - if (n) { - let name = ''; - - for (let i = 0; i < 5 && i < n; i++) - name += '\n' + names[i]; - - if (n >= 5) - name += '\n...'; - - msg = msgAsk + msgSel + n + ' files/directories?\n' + encode(name); - } else { - const current = DOM.getCurrentFile(); - const isDir = DOM.isCurrentIsDir(current); - const getType = (isDir) => { - return isDir ? 'directory' : 'file'; - }; - - const type = getType(isDir) + ' '; - - const name = DOM.getCurrentName(current); - msg = msgAsk + msgSel + type + name + '?'; - } - - const cancel = false; - - Dialog.confirm(TITLE, msg, {cancel}).then(() => { - deleteSilent(files); - }); - } - - /** - * delete current or selected files - * - * @files - */ - function deleteSilent(files = DOM.getActiveFiles()) { - const query = '?files'; - const path = Info.dirPath; - - if (noFilesCheck()) - return; - - showLoad(); - - const removedNames = DOM.getFilenames(files); - const names = DOM.CurrentInfo.files.map(DOM.getCurrentName); - const prevCurrent = DOM.getCurrentName(); - const currentName = getNextCurrentName(prevCurrent, names, removedNames); - - deleteFn(path + query, removedNames, (e) => { - const Storage = DOM.Storage; - const dirPath = Info.dirPath; - - !e && CloudCmd.refresh({ - currentName - }); - - Storage.removeMatch(dirPath); - }); - } - - /* - * process files (copy or move) - * @param data - * @param operation - */ - function _processFiles(options, data) { - let selFiles, files; - let panel; - let shouldAsk; - let sameName; - let ok; - - let from = ''; - let to = ''; - - let names = []; - - if (data) { - from = data.from; - to = data.to; - names = data.names; - panel = Info.panel; - } else { - from = Info.dirPath; - to = DOM.getNotCurrentDirPath(); - selFiles = DOM.getSelectedFiles(); - names = DOM.getFilenames(selFiles); - data = {}; - shouldAsk = true; - panel = Info.panelPassive; - } - - if (!names.length) - names.push(DOM.getCurrentName()); - - const name = names[0]; - - sameName = DOM.getCurrentByName(name, panel); - - if (!data && noFilesCheck()) - return; - - const {type} = options; - - const isCopy = type === 'copy'; - const option = isCopy ? 'confirmCopy' : 'confirmMove'; - const title = isCopy ? 'Copy' : 'Rename/Move'; - const operation = isCopy ? copyFn : moveFn; - - if (shouldAsk && config(option)) - return message(title, to, names.map(encode)) - .then(ask); - - ask(to); - - function ask(to) { - ok = from !== to && to; - - if (ok && !shouldAsk || !sameName) - return go(); - - const str = `"${ name }" already exist. Overwrite?`; - const cancel = false; - - Dialog.confirm(TITLE, str, {cancel}).then(go); - - function go() { - showLoad(); - - files = { - from, - to, - names, - }; - - operation(files, () => { - DOM.Storage.remove(from, () => { - const { - panel, - panelPassive, - } = Info; - - const setCurrent = () => { - const currentName = name || data.names[0]; - DOM.setCurrentByName(currentName); - }; - - if (!Info.isOnePanel) - CloudCmd.refresh({ - panel: panelPassive, - noCurrent: true, - }); - - CloudCmd.refresh({panel}, setCurrent); - }); - }); - } - } - } - - function checkEmpty(name, operation) { - if (!operation) - throw Error(name + ' could not be empty!'); - } - - function twopack(operation, type) { - let op; - let fileFrom; - let currentName = Info.name; - - const Images = DOM.Images; - const path = Info.path; - const dirPath = Info.dirPath; - const activeFiles = DOM.getActiveFiles(); - const names = DOM.getFilenames(activeFiles); - - checkEmpty('operation', operation); - - if (!names.length) - return Dialog.alert.noFiles(TITLE); - - switch(operation) { - case 'extract': - op = extractFn; - - fileFrom = { - from: path, - to: dirPath - }; - - currentName = removeExtension(currentName); - - break; - - case 'pack': - op = getPacker(type); - - if (names.length > 1) - currentName = Info.dir; - - currentName += DOM.getPackerExt(type); - - fileFrom = { - from: dirPath, - to: dirPath + currentName, - names, - }; - break; - } - - Images.show.load('top'); - - op(fileFrom, (error) => { - !error && CloudCmd.refresh({ - currentName - }); - }); - } - - function message(msg, to, names) { - const n = names.length; - const name = names[0]; - - msg += ' '; - - if (names.length > 1) - msg += n + ' file(s)'; - else - msg += '"' + name + '"'; - - msg += ' to'; - - const cancel = false; - - return Dialog.prompt(TITLE, msg, to, {cancel}); - } - - function load(callback) { - const prefix = CloudCmd.PREFIX; - const file = `${prefix}/fileop/fileop.js`; - - DOM.load.js(file, (error) => { - if (error) { - Dialog.alert(TITLE, error.message); - return exec(callback); - } + const {prefix, prefixSocket} = CloudCmd; + await loadAll(); + await initOperations(prefix, prefixSocket, callback); + }, + (callback) => { Loaded = true; - Util.timeEnd(Name + ' load'); - exec(callback); - }); - - Util.time(Name + ' load'); - } + Images.hide(); + callback(); + }, + ], callback); +}); + +function _authCheck(spawn, ok) { + const accept = wraptile(ok); + const alertDialog = wraptile(Dialog.alert); - init(); + spawn.on('accept', accept(spawn)); + spawn.on('reject', alertDialog('Wrong credentials!')); + spawn.emit('auth', config('username'), config('password')); } +const onConnect = currify((fn, operator) => { + setOperations(operator); + fn(); +}); + +async function initOperations(prefix, socketPrefix, fn) { + socketPrefix = `${socketPrefix}/fileop`; + + const operator = await globalThis.fileop({ + prefix, + socketPrefix, + }); + + operator.on('connect', authCheck(operator, onConnect(fn))); +} + +function setOperations(operator) { + packTarFn = ({from, to, names}, callback) => { + const operation = 'Tar'; + const listen = setListeners({ + operation, + callback, + noContinue: true, + from, + to, + }); + + operator + .tar(from, to, names) + .then(listen); + }; + + packZipFn = ({from, to, names}, callback) => { + const operation = 'Zip'; + const listen = setListeners({ + operation, + callback, + noContinue: true, + from, + to, + }); + + operator + .zip(from, to, names) + .then(listen); + }; + + deleteFn = (from, files, callback) => { + from = removeQuery(from); + + const operation = 'Delete'; + const listen = setListeners({ + operation, + callback, + from, + }); + + operator + .remove(from, files) + .then(listen); + }; + + copyFn = ({from, to, names}, callback) => { + const operation = 'Copy'; + const listen = setListeners({ + operation, + callback, + from, + to, + names, + }); + + operator + .copy(from, to, names) + .then(listen); + }; + + moveFn = ({from, to, names}, callback) => { + const operation = 'Move'; + const listen = setListeners({ + operation, + callback, + from, + to, + }); + + operator + .move(from, to, names) + .then(listen); + }; + + extractFn = ({from, to}, callback) => { + const operation = 'Extract'; + const listen = setListeners({ + operation, + callback, + noContinue: true, + from, + to, + }); + + operator + .extract(from, to) + .then(listen); + }; +} + +function getPacker(type) { + if (type === 'zip') + return packZipFn; + + return packTarFn; +} + +export function hide() { + CloudCmd.View.hide(); +} + +export function show(operation, data) { + if (!Loaded) + return; + + if (operation === 'copy') + return Operation.copy(data); + + if (operation === 'move') + return Operation.move(data); + + if (operation === 'delete') + return Operation.delete(); + + if (operation === 'delete:silent') + return Operation.deleteSilent(); + + if (operation === 'pack') + return Operation.pack(); + + if (operation === 'extract') + return Operation.extract(); +} + +Operation.copy = processFiles({ + type: 'copy', +}); + +Operation.move = processFiles({ + type: 'move', +}); + +Operation.delete = promptDelete; + +Operation.deleteSilent = deleteSilent; + +Operation.pack = () => { + const isZip = config('packer') === 'zip'; + twopack('pack', isZip ? 'zip' : 'tar'); +}; + +Operation.extract = () => { + twopack('extract'); +}; + +/** + * prompt and delete current file or selected files + * + * @currentFile + */ +async function promptDelete() { + if (noFilesCheck()) + return; + + const msgAsk = 'Do you really want to delete the '; + const msgSel = 'selected '; + + const files = DOM.getActiveFiles(); + const names = DOM.getFilenames(files); + const n = names.length; + + let msg; + + if (n) { + let name = ''; + + for (let i = 0; i < 5 && i < n; i++) + name += '\n' + names[i]; + + if (n >= 5) + name += '\n...'; + + msg = msgAsk + msgSel + n + ' files/directories?\n' + encode(name); + } else { + const current = DOM.getCurrentFile(); + const isDir = DOM.isCurrentIsDir(current); + const getType = (isDir) => isDir ? 'directory' : 'file'; + + const type = getType(isDir) + ' '; + + const name = DOM.getCurrentName(current); + + msg = msgAsk + msgSel + type + name + '?'; + } + + const [cancel] = await Dialog.confirm(msg); + + if (cancel) + return; + + deleteSilent(files); +} + +/** + * delete current or selected files + * + * @files + */ +function deleteSilent(files = DOM.getActiveFiles()) { + const query = '?files'; + const path = Info.dirPath; + + if (noFilesCheck()) + return; + + showLoad(); + + const removedNames = DOM.getFilenames(files); + const names = DOM.CurrentInfo.files.map(DOM.getCurrentName); + const currentName = DOM.getCurrentName(); + const nextCurrentName = getNextCurrentName(currentName, names, removedNames); + + deleteFn(path + query, removedNames, async () => { + await CloudCmd.refresh(); + + const names = Info.files.map(DOM.getCurrentName); + const isCurrent = names.includes(currentName); + + const name = isCurrent ? currentName : nextCurrentName; + + DOM.setCurrentByName(name); + }); +} + +/* + * process files (copy or move) + * @param data + * @param operation + */ +async function _processFiles(options, data) { + let selFiles; + let files; + let panel; + let shouldAsk; + let ok; + + let from = ''; + let to = ''; + + let names = []; + + if (data) { + ({ + from, + to, + names, + } = data); + + ({panel} = Info); + } else { + from = Info.dirPath; + to = DOM.getNotCurrentDirPath(); + selFiles = DOM.getSelectedFiles(); + names = DOM.getFilenames(selFiles); + data = {}; + shouldAsk = true; + panel = Info.panelPassive; + } + + if (!names.length) + names.push(DOM.getCurrentName()); + + const [name] = names; + + const sameName = DOM.getCurrentByName(name, panel); + + if (!data && noFilesCheck()) + return; + + const {type} = options; + + const isCopy = type === 'copy'; + const option = isCopy ? 'confirmCopy' : 'confirmMove'; + const title = isCopy ? 'Copy' : 'Rename/Move'; + const operation = isCopy ? copyFn : moveFn; + + if (shouldAsk && config(option)) { + const [cancel, newTo] = await prompt(title, to, names.map(encode)); + + if (!cancel) + ask(newTo); + + return; + } + + ask(to); + + function ask(to) { + ok = from !== to && to; + + if (ok && !shouldAsk || !sameName) + return go(); + + const str = `"${name}" already exist. Overwrite?`; + const cancel = false; + + Dialog + .confirm(str, { + cancel, + }) + .then(go); + + function go() { + showLoad(); + + files = { + from, + to, + names, + }; + + operation(files, async () => { + await DOM.Storage.remove(from); + + const {panel, panelPassive} = Info; + + if (!Info.isOnePanel) + CloudCmd.refresh({ + panel: panelPassive, + noCurrent: true, + }); + + CloudCmd.refresh({ + panel, + }); + }); + } + } +} + +function checkEmpty(name, operation) { + if (!operation) + throw Error(`${name} could not be empty!`); +} + +function twopack(operation, type) { + let op; + let fileFrom; + let currentName = Info.name; + + const {path, dirPath} = Info; + + const activeFiles = DOM.getActiveFiles(); + const names = DOM.getFilenames(activeFiles); + + checkEmpty('operation', operation); + + if (!names.length) + return Dialog.alert.noFiles(); + + switch(operation) { + case 'extract': + op = extractFn; + + fileFrom = { + from: path, + to: dirPath, + }; + + currentName = removeExtension(currentName); + + break; + + case 'pack': + op = getPacker(type); + + if (names.length > 1) + currentName = Info.dir; + + currentName += DOM.getPackerExt(type); + + fileFrom = { + from: dirPath, + to: dirPath + currentName, + names, + }; + break; + } + + showLoad(); + + op(fileFrom, (error) => { + !error && CloudCmd.refresh({ + currentName, + }); + }); +} + +async function prompt(msg, to, names) { + const n = names.length; + const [name] = names; + + msg += ' '; + + if (names.length > 1) + msg += `${n} file(s)`; + else + msg += `"${name}"`; + + msg += ' to'; + + return await Dialog.prompt(msg, to); +} + +globalThis.CloudCmd[Name] = { + init, + hide, + show, +}; + +async function loadAll() { + const {prefix} = globalThis.CloudCmd; + const file = `${prefix}/fileop/fileop.js`; + + const [error] = await tryToCatch(load.js, file); + + if (error) + Dialog.alert(error.message); + + Loaded = true; +} diff --git a/client/modules/operation/remove-extension.js b/client/modules/operation/remove-extension.js index 52772c30..e6ce2c46 100644 --- a/client/modules/operation/remove-extension.js +++ b/client/modules/operation/remove-extension.js @@ -1,20 +1,17 @@ -'use strict'; +import {getExt} from '#common/util'; -const {getExt} = require('../../../common/util'); - -module.exports = (name) => { +export const removeExtension = (name) => { const ext = getExtension(name); return name.replace(ext, ''); }; function getExtension(name) { - if (/\.tar\.gz$/.test(name)) + if (name.endsWith('.tar.gz')) return '.tar.gz'; - if (/\.tar\.bz2$/.test(name)) + if (name.endsWith('.tar.bz2')) return '.tar.bz2'; return getExt(name); } - diff --git a/test/client/modules/operation/remove-extension.js b/client/modules/operation/remove-extension.spec.js similarity index 81% rename from test/client/modules/operation/remove-extension.js rename to client/modules/operation/remove-extension.spec.js index 6491f052..fa11e9e2 100644 --- a/test/client/modules/operation/remove-extension.js +++ b/client/modules/operation/remove-extension.spec.js @@ -1,9 +1,5 @@ -'use strict'; - -const test = require('tape'); -const dir = '../../../../client/modules/operation'; - -const removeExtension = require(`${dir}/remove-extension`); +import test from 'supertape'; +import {removeExtension} from './remove-extension.js'; test('cloudcmd: client: modules: operation: removeExtension: .tar.gz', (t) => { const name = 'hello'; @@ -16,7 +12,7 @@ test('cloudcmd: client: modules: operation: removeExtension: .tar.gz', (t) => { test('cloudcmd: client: modules: operation: removeExtension: .tar.bz2', (t) => { const name = 'hello'; const fullName = `${name}.tar.bz2`; - + t.equal(removeExtension(fullName), name, 'should remove .tar.bz2'); t.end(); }); @@ -24,8 +20,7 @@ test('cloudcmd: client: modules: operation: removeExtension: .tar.bz2', (t) => { test('cloudcmd: client: modules: operation: removeExtension: .bz2', (t) => { const name = 'hello'; const fullName = `${name}.bz2`; - + t.equal(removeExtension(fullName), name, 'should remove .bz2'); t.end(); }); - diff --git a/client/modules/operation/set-listeners.js b/client/modules/operation/set-listeners.js index 33b7d802..e54e7d14 100644 --- a/client/modules/operation/set-listeners.js +++ b/client/modules/operation/set-listeners.js @@ -1,65 +1,75 @@ -'use strict'; - /* global DOM */ -/* global CloudCmd */ +import forEachKey from 'for-each-key'; +import wraptile from 'wraptile'; +import {format} from './format.js'; -const { - Images, - Dialog, -} = DOM; +const {Dialog, Images} = DOM; -const forEachKey = require('for-each-key/legacy'); -const { - TITLE, -} = CloudCmd; - -module.exports = (options, callback) => (emitter) => { - if (!callback) { - callback = options; - options = {}; - } +export const setListeners = (options) => (emitter) => { + const { + operation, + callback, + noContinue, + from, + to, + } = options; let done; - let lastError; + + const onAbort = wraptile(({emitter, operation}) => { + emitter.abort(); + + const msg = `${operation} aborted`; + + Dialog.alert(msg, { + cancel: false, + }); + }); const removeListener = emitter.removeListener.bind(emitter); const on = emitter.on.bind(emitter); + const message = format(operation, from, to); + const progress = Dialog.progress(message); + + progress.catch(onAbort({ + emitter, + operation, + })); + const listeners = { progress: (value) => { done = value === 100; - Images.setProgress(value); + progress.setProgress(value); }, end: () => { - Images - .hide() - .clearProgress(); - + Images.hide(); forEachKey(removeListener, listeners); + progress.remove(); - if (lastError || done) - callback(); + callback(); }, - error: (error) => { - lastError = error; - - if (options.noContinue) { + error: async (error) => { + if (noContinue) { listeners.end(error); - Dialog.alert(TITLE, error); + Dialog.alert(error); + progress.remove(); + return; } - Dialog.confirm(TITLE, error + '\n Continue?') - .then(() => { - emitter.continue(); - }, () => { - emitter.abort(); - }); - } + const [cancel] = await Dialog.confirm(`${error} + Continue?`); + + if (!done && !cancel) + return emitter.continue(); + + emitter.abort(); + progress.remove(); + }, }; forEachKey(on, listeners); }; - diff --git a/client/modules/polyfill.js b/client/modules/polyfill.js index fb0997a2..90b62689 100644 --- a/client/modules/polyfill.js +++ b/client/modules/polyfill.js @@ -1,9 +1,15 @@ -'use strict'; +import _scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'; -/* global DOM */ +globalThis.DOM = globalThis.DOM || {}; -require('domtokenlist-shim'); - -const scrollIntoViewIfNeeded = require('scroll-into-view-if-needed').default; -DOM.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; +export const scrollIntoViewIfNeeded = (el, overrides = {}) => { + const { + scroll = _scrollIntoViewIfNeeded, + } = overrides; + + return scroll(el, { + block: 'nearest', + }); +}; +globalThis.DOM.scrollIntoViewIfNeeded = scrollIntoViewIfNeeded; diff --git a/client/modules/polyfill.spec.js b/client/modules/polyfill.spec.js new file mode 100644 index 00000000..740947eb --- /dev/null +++ b/client/modules/polyfill.spec.js @@ -0,0 +1,20 @@ +import {test, stub} from 'supertape'; +import {scrollIntoViewIfNeeded} from './polyfill.js'; + +test('cloudcmd: client: polyfill: scrollIntoViewIfNeeded', (t) => { + const scroll = stub(); + const el = {}; + + scrollIntoViewIfNeeded(el, { + scroll, + }); + + const args = [ + el, { + block: 'nearest', + }, + ]; + + t.calledWith(scroll, args, 'should call scrollIntoViewIfNeeded'); + t.end(); +}); diff --git a/client/modules/terminal-run.js b/client/modules/terminal-run.js new file mode 100644 index 00000000..029415ea --- /dev/null +++ b/client/modules/terminal-run.js @@ -0,0 +1,147 @@ +import '../../css/terminal.css'; +import {promisify} from 'es6-promisify'; +import {tryToCatch} from 'try-to-catch'; +import {fullstore} from 'fullstore'; +import exec from 'execon'; +import load from 'load.js'; +import DOM from '#dom'; +import * as Images from '#dom/images'; + +const {Dialog} = DOM; +const {CloudCmd} = globalThis; +const {Key, config} = CloudCmd; + +CloudCmd.TerminalRun = { + init, + show, + hide, +}; + +let Loaded; +let Terminal; +let Socket; + +const exitCodeStore = fullstore(); + +const loadAll = async () => { + const {prefix} = CloudCmd; + + const prefixGritty = getPrefix(); + const js = `${prefixGritty}/gritty.js`; + const css = `${prefix}/dist/terminal.css`; + + const [e] = await tryToCatch(load.parallel, [js, css]); + + if (e) { + const src = e.target.src.replace(globalThis.location.href, ''); + return Dialog.alert(`file ${src} could not be loaded`); + } + + Loaded = true; +}; + +export async function init() { + if (!config('terminal')) + return; + + Images.show.load('top'); + + await CloudCmd.View(); + await loadAll(); +} + +export async function show(options = {}) { + return await runTerminal(options); +} + +const runTerminal = promisify((options, fn) => { + if (!Loaded) + return fn(null, -1); + + if (!config('terminal')) + return; + + create(options); + + CloudCmd.View.show(Terminal.element, { + afterShow: () => { + Terminal.focus(); + }, + afterClose: (/* exec.series args */) => { + fn(null, exitCodeStore()); + }, + }); +}); + +export function hide() { + CloudCmd.View.hide(); +} + +const getPrefix = () => CloudCmd.prefix + '/gritty'; + +function getPrefixSocket() { + return CloudCmd.prefixSocket + '/gritty'; +} + +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, +}); + +function create(createOptions) { + const { + cwd = DOM.getCurrentDirPath(), + command, + autoClose, + closeMessage = 'Press any key to close Terminal...', + } = createOptions; + + const options = { + cwd, + env: getEnv(), + prefix: getPrefixSocket(), + socketPath: CloudCmd.prefix, + fontFamily: 'Droid Sans Mono', + command, + autoRestart: false, + }; + + let commandExit = false; + + const {socket, terminal} = globalThis.gritty(document.body, options); + + Socket = socket; + Terminal = terminal; + + Terminal.onKey(({domEvent}) => { + const {keyCode, shiftKey} = domEvent; + + if (commandExit) + hide(); + + if (shiftKey && keyCode === Key.ESC) + hide(); + }); + + Socket.on('exit', (code) => { + exitCodeStore(code); + + if (autoClose) + return hide(); + + terminal.write(`\n${closeMessage}`); + commandExit = true; + }); + + Socket.on('connect', exec.with(authCheck, socket)); +} + +function authCheck(spawn) { + spawn.emit('auth', config('username'), config('password')); + + spawn.on('reject', () => { + Dialog.alert('Wrong credentials!'); + }); +} diff --git a/client/modules/terminal.js b/client/modules/terminal.js index 574f18f6..186ee0b2 100644 --- a/client/modules/terminal.js +++ b/client/modules/terminal.js @@ -1,126 +1,113 @@ -'use strict'; +import '#css/terminal.css'; +import {tryToCatch} from 'try-to-catch'; +import exec from 'execon'; +import load from 'load.js'; +import * as Images from '#dom/images'; +import DOM from '#dom'; -/* global CloudCmd, gritty */ +const loadParallel = load.parallel; -require('../../css/terminal.css'); +const {CloudCmd} = globalThis; -const exec = require('execon'); -const load = require('../dom/load'); -const DOM = require('../dom'); -const Images = require('../dom/images'); const {Dialog} = DOM; +const {Key, config} = globalThis.CloudCmd; -const TITLE = 'Terminal'; +CloudCmd.Terminal = { + init, + show, + hide, +}; -CloudCmd.Terminal = TerminalProto; - -const {Key} = CloudCmd; - -let Element; let Loaded; let Terminal; +let Socket; -const {config} = CloudCmd; - -function TerminalProto() { - const noop = () => {}; +const loadAll = async () => { + const {prefix} = CloudCmd; + const prefixGritty = getPrefix(); + const js = `${prefixGritty}/gritty.js`; + const css = `${prefix}/dist/terminal.css`; + + const [e] = await tryToCatch(loadParallel, [js, css]); + + if (e) { + const src = e.target.src.replace(globalThis.location.href, ''); + return Dialog.alert(`file ${src} could not be loaded`); + } + + Loaded = true; +}; + +export async function init() { if (!config('terminal')) - return { - show: noop - }; + return; Images.show.load('top'); - exec.series([ - CloudCmd.View, - loadAll, - create, - show, - ]); - - Element = load({ - name: 'div', - className : 'terminal', - }); - - return module.exports; + await CloudCmd.View(); + await loadAll(); + create(); } -module.exports.show = show; -module.exports.hide = hide; - -function hide () { +export function hide() { CloudCmd.View.hide(); } -function getPrefix() { - return CloudCmd.PREFIX + '/gritty'; +const getPrefix = () => CloudCmd.prefix + '/gritty'; + +function getPrefixSocket() { + return CloudCmd.prefixSocket + '/gritty'; } -function getEnv() { - return { - ACTIVE_DIR: DOM.getCurrentDirPath, - PASSIVE_DIR: DOM.getNotCurrentDirPath, - CURRENT_NAME: DOM.getCurrentName, - CURRENT_PATH: DOM.getCurrentPath, - }; -} +const getEnv = () => ({ + ACTIVE_DIR: DOM.getCurrentDirPath, + PASSIVE_DIR: DOM.getNotCurrentDirPath, + CURRENT_NAME: DOM.getCurrentName, + CURRENT_PATH: DOM.getCurrentPath, +}); -function create(callback) { +function create() { const options = { env: getEnv(), - prefix: getPrefix(), - socketPath: CloudCmd.PREFIX, + prefix: getPrefixSocket(), + socketPath: CloudCmd.prefix, fontFamily: 'Droid Sans Mono', }; - const {socket, terminal} = gritty(Element, options); + const {socket, terminal} = globalThis.gritty(document.body, options); + Socket = socket; Terminal = terminal; - terminal.on('key', (char, {keyCode, shiftKey}) => { - if (shiftKey && keyCode === Key.ESC) { + Terminal.onKey(({domEvent}) => { + const {keyCode, shiftKey} = domEvent; + + if (shiftKey && keyCode === Key.ESC) hide(); - } }); - socket.on('connect', exec.with(authCheck, socket)); - exec(callback); + Socket.on('connect', exec.with(authCheck, socket)); } function authCheck(spawn) { spawn.emit('auth', config('username'), config('password')); spawn.on('reject', () => { - Dialog.alert(TITLE, 'Wrong credentials!'); + Dialog.alert('Wrong credentials!'); }); } -function show(callback) { +export function show() { if (!Loaded) return; - CloudCmd.View.show(Element, { - afterShow: () => { - if (Terminal) - Terminal.focus(); - - exec(callback); - } - }); -} - -function loadAll(callback) { - const prefix = getPrefix(); - const url = prefix + '/gritty.js'; + if (!config('terminal')) + return; - DOM.load.js(url, (error) => { - if (error) - return Dialog.alert(TITLE, error.message); - - Loaded = true; - exec(callback); + CloudCmd.View.show(Terminal.element, { + afterShow: () => { + Terminal.focus(); + }, }); } - diff --git a/client/modules/upload.js b/client/modules/upload.js index 0337e47f..8433fbf5 100644 --- a/client/modules/upload.js +++ b/client/modules/upload.js @@ -1,60 +1,53 @@ /* global CloudCmd, DOM */ +import createElement from '@cloudcmd/create-element'; +import * as Files from '#dom/files'; +import {uploadFiles} from '#dom/upload-files'; +import * as Images from '#dom/images'; -'use strict'; +CloudCmd.Upload = { + init, + show, + hide, +}; -const exec = require('execon'); - -const load = require('../dom/load'); -const Files = require('../dom/files'); -const Images = require('../dom/images'); -const uploadFiles = require('../dom/upload-files'); - -CloudCmd.Upload = UploadProto; - -function UploadProto() { +export async function init() { Images.show.load('top'); - - exec.series([ - CloudCmd.View, - show - ]); - - return exports; + await CloudCmd.View(); } -module.exports.show = show; -module.exports.hide = hide; - -function show() { +export async function show() { Images.show.load('top'); - Files.get('upload', (error, data) => { - const autoSize = true; - - CloudCmd.View.show(data, { - autoSize, - afterShow, - }); + const innerHTML = await Files.get('upload'); + const autoSize = true; + + const el = createElement('div', { + innerHTML, + }); + + CloudCmd.View.show(el, { + autoSize, + afterShow, }); const fontFamily = [ '"Droid Sans Mono"', '"Ubuntu Mono"', '"Consolas"', - 'monospace' + 'monospace', ].join(', '); - load.style({ - id : 'upload-css', - inner : '[data-name=js-upload-file-button] {' + - `font-family: ${fontFamily};` + - 'font-size: 20px;' + - 'width: 97%' + - '}' + createElement('style', { + dataName: 'upload-css', + innerText: `[data-name=js-upload-file-button] { + font-family: ${fontFamily}; + font-size: 16px; + margin: 10px 0 10px 0; + }`, }); } -function hide() { +export function hide() { CloudCmd.View.hide(); } @@ -71,4 +64,3 @@ function afterShow() { uploadFiles(files); }); } - diff --git a/client/modules/user-menu/get-user-menu.js b/client/modules/user-menu/get-user-menu.js new file mode 100644 index 00000000..aa3a1076 --- /dev/null +++ b/client/modules/user-menu/get-user-menu.js @@ -0,0 +1,8 @@ +export const getUserMenu = (menuFn) => { + const module = {}; + const fn = Function('module', menuFn); + + fn(module); + + return module.exports; +}; diff --git a/client/modules/user-menu/get-user-menu.spec.js b/client/modules/user-menu/get-user-menu.spec.js new file mode 100644 index 00000000..606eb820 --- /dev/null +++ b/client/modules/user-menu/get-user-menu.spec.js @@ -0,0 +1,18 @@ +import test from 'supertape'; +import {getUserMenu} from './get-user-menu.js'; + +test('user-menu: getUserMenu', (t) => { + const menu = `module.exports = { + 'F2 - Rename file': ({DOM}) => { + const {element} = DOM.CurrentInfo; + DOM.renameCurrent(element); + } + }`; + + const result = getUserMenu(menu); + + const [key] = Object.keys(result); + + t.equal(key, 'F2 - Rename file'); + t.end(); +}); diff --git a/client/modules/user-menu/index.js b/client/modules/user-menu/index.js new file mode 100644 index 00000000..60d32d95 --- /dev/null +++ b/client/modules/user-menu/index.js @@ -0,0 +1,192 @@ +import '../../../css/user-menu.css'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {fullstore} from 'fullstore'; +import load from 'load.js'; +import createElement from '@cloudcmd/create-element'; +import {tryCatch} from 'try-catch'; +import {tryToCatch} from 'try-to-catch'; +import {codeFrameColumns} from '@babel/code-frame'; +import * as Dialog from '#dom/dialog'; +import * as Images from '#dom/images'; +import {getUserMenu} from './get-user-menu.js'; +import {navigate} from './navigate.js'; +import {parseError} from './parse-error.js'; +import {parseUserMenu} from './parse-user-menu.js'; +import {runSelected} from './run.js'; + +const loadCSS = load.css; +const sourceStore = fullstore(); + +const { + CloudCmd, + DOM, + CloudFunc, +} = globalThis; + +const Name = 'UserMenu'; + +CloudCmd[Name] = { + init, + show, + hide, +}; + +const {Key} = CloudCmd; + +export async function init() { + await Promise.all([ + loadCSS(`${CloudCmd.prefix}/dist/user-menu.css`), + CloudCmd.View(), + ]); +} + +const {CurrentInfo} = DOM; + +export async function show() { + Images.show.load('top'); + + const {dirPath} = CurrentInfo; + const res = await fetch(`${CloudCmd.prefix}/api/v1/user-menu?dir=${dirPath}`); + const source = await res.text(); + const [error, userMenu] = tryCatch(getUserMenu, source); + + Images.hide(); + + if (error) + return Dialog.alert(getCodeFrame({ + error, + source, + })); + + sourceStore(source); + + const { + names, + keys, + items, + settings, + } = parseUserMenu(userMenu); + + if (settings.run) + return runSelected(settings.select, items, runUserMenu); + + const button = createElement('button', { + className: 'cloudcmd-user-menu-button', + innerText: 'User Menu', + notAppend: true, + }); + + const select = createElement('select', { + className: 'cloudcmd-user-menu', + innerHTML: fillTemplate(names), + notAppend: true, + size: 10, + }); + + button.addEventListener('click', onButtonClick(userMenu, select)); + select.addEventListener('dblclick', onDblClick(userMenu)); + select.addEventListener('keydown', onKeyDown({ + keys, + userMenu, + })); + + const afterShow = () => select.focus(); + const autoSize = true; + + CloudCmd.View.show([button, select], { + autoSize, + afterShow, + }); +} + +function fillTemplate(options) { + const result = []; + + for (const option of options) + result.push(``); + + return result.join(''); +} + +export function hide() { + CloudCmd.View.hide(); +} + +const onDblClick = currify(async (items, e) => { + const {value} = e.target; + await runUserMenu(items[value]); +}); + +const onButtonClick = wraptile(async (items, {value}) => { + await runUserMenu(items[value]); +}); + +const onKeyDown = currify(async ({keys, userMenu}, e) => { + const {keyCode, target} = e; + + const keyName = e.key.toUpperCase(); + + e.preventDefault(); + e.stopPropagation(); + + let value; + + if (keyCode === Key.ESC) + return hide(); + + if (keyCode === Key.ENTER) + value = userMenu[target.value]; + else if (keys[keyName]) + value = keys[keyName]; + else + return navigate(target, e); + + await runUserMenu(value); +}); + +const runUserMenu = async (fn) => { + hide(); + + const [error] = await tryToCatch(fn, { + DOM, + CloudCmd, + CloudFunc, + tryToCatch, + }); + + if (!error) + return; + + const source = sourceStore(); + + return Dialog.alert(getCodeFrame({ + error, + source, + })); +}; + +function getCodeFrame({error, source}) { + const {code} = error; + + if (!code || code === 'frame') + return error.message; + + const [line, column] = parseError(error); + + const start = { + line, + column, + }; + + const location = { + start, + }; + + const frame = codeFrameColumns(source, location, { + message: error.message, + highlightCode: false, + }); + + return `
${frame}
`; +} diff --git a/client/modules/user-menu/navigate.js b/client/modules/user-menu/navigate.js new file mode 100644 index 00000000..0fa4d672 --- /dev/null +++ b/client/modules/user-menu/navigate.js @@ -0,0 +1,53 @@ +import {fullstore} from 'fullstore'; +import { + J, + K, + UP, + DOWN, +} from '../../key/key.js'; + +const store = fullstore(1); +const isDigit = (a) => /^\d+$/.test(a); + +export const navigate = (el, {key, keyCode}) => { + if (isDigit(key)) + store(Number(key)); + + if (keyCode === DOWN || keyCode === J) { + const count = store(); + store(1); + + return down(el, count); + } + + if (keyCode === UP || keyCode === K) { + const count = store(); + store(1); + + return up(el, count); + } +}; + +function down(el, count) { + const {length} = el; + + if (el.selectedIndex === length - 1) + el.selectedIndex = 0; + else + el.selectedIndex += count; + + if (el.selectedIndex < 0) + el.selectedIndex = length - 1; +} + +function up(el, count) { + const {length} = el; + + if (!el.selectedIndex) + el.selectedIndex = length - 1; + else + el.selectedIndex -= count; + + if (el.selectedIndex < 0) + el.selectedIndex = 0; +} diff --git a/client/modules/user-menu/navigate.spec.js b/client/modules/user-menu/navigate.spec.js new file mode 100644 index 00000000..6539c8e9 --- /dev/null +++ b/client/modules/user-menu/navigate.spec.js @@ -0,0 +1,163 @@ +import test from 'supertape'; +import {navigate} from './navigate.js'; +import { + UP, + DOWN, + J, + K, +} from '../../key/key.js'; + +test('cloudcmd: user-menu: navigate: DOWN', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: DOWN, + }); + + t.equal(el.selectedIndex, 1); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: J', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: J, + }); + + t.equal(el.selectedIndex, 1); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: DOWN: bottom', (t) => { + const el = { + length: 3, + selectedIndex: 2, + }; + + navigate(el, { + keyCode: DOWN, + }); + + t.equal(el.selectedIndex, 0); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: K', (t) => { + const el = { + length: 3, + selectedIndex: 2, + }; + + navigate(el, { + keyCode: K, + }); + + t.equal(el.selectedIndex, 1); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: UP', (t) => { + const el = { + length: 3, + selectedIndex: 2, + }; + + navigate(el, { + keyCode: UP, + }); + + t.equal(el.selectedIndex, 1); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: UP: top', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: UP, + }); + + t.equal(el.selectedIndex, 2); + t.end(); +}); + +test('cloudcmd: user-menu: navigate', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: 0, + }); + + t.equal(el.selectedIndex, 0, 'should not change'); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: DOWN: count', (t) => { + const el = { + length: 3, + selectedIndex: 0, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: DOWN, + }); + + t.equal(el.selectedIndex, 5); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: J: count: to big', (t) => { + const el = { + length: 3, + selectedIndex: -Infinity, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: J, + }); + + t.equal(el.selectedIndex, 2); + t.end(); +}); + +test('cloudcmd: user-menu: navigate: K: count: to small', (t) => { + const el = { + length: 3, + selectedIndex: -Infinity, + }; + + navigate(el, { + keyCode: 53, + key: '5', + }); + + navigate(el, { + keyCode: K, + }); + + t.equal(el.selectedIndex, 0); + t.end(); +}); diff --git a/client/modules/user-menu/parse-error.js b/client/modules/user-menu/parse-error.js new file mode 100644 index 00000000..23d605e0 --- /dev/null +++ b/client/modules/user-menu/parse-error.js @@ -0,0 +1,21 @@ +const isNumber = (a) => typeof a === 'number'; + +export const parseError = (error) => { + const {lineNumber, columnNumber} = error; + + // thank you firefox + if (isNumber(lineNumber) && isNumber(columnNumber)) + return [lineNumber, columnNumber]; + + const before = error.stack.indexOf('>'); + const str = error.stack.slice(before + 1); + const after = str.indexOf(')'); + const newStr = str.slice(1, after); + + const [line, column] = newStr.split(':'); + + return [ + Number(line), + Number(column), + ]; +}; diff --git a/client/modules/user-menu/parse-error.spec.js b/client/modules/user-menu/parse-error.spec.js new file mode 100644 index 00000000..5ec8da45 --- /dev/null +++ b/client/modules/user-menu/parse-error.spec.js @@ -0,0 +1,33 @@ +import test from 'supertape'; +import {parseError} from './parse-error.js'; + +test('user-menu: parse-error', (t) => { + const result = parseError({ + lineNumber: 1, + columnNumber: 2, + }); + + const expected = [1, 2]; + + t.deepEqual(result, expected); + t.end(); +}); + +test('user-menu: parse-error: stack', (t) => { + const stack = ` + ReferenceError: s is not defined + at eval (eval at module.exports (get-user-menu.js:NaN), :1:2) + at module.exports (get-user-menu.js:6) + at tryCatch (VM12611 try-catch.js:7) + at AsyncFunction.show (index.js:67) + `; + + const result = parseError({ + stack, + }); + + const expected = [1, 2]; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/client/modules/user-menu/parse-user-menu.js b/client/modules/user-menu/parse-user-menu.js new file mode 100644 index 00000000..e4ad39e3 --- /dev/null +++ b/client/modules/user-menu/parse-user-menu.js @@ -0,0 +1,31 @@ +const {entries, assign} = Object; + +export const parseUserMenu = (userMenu) => { + const names = []; + const keys = {}; + const items = {}; + const settings = {}; + + for (const [str, fn] of entries(userMenu)) { + if (str === '__settings') { + assign(settings, userMenu[str]); + continue; + } + + if (str.startsWith('_')) + continue; + + names.push(str); + const [key, name] = str.split(' - '); + + keys[key] = fn; + items[name] = fn; + } + + return { + names, + keys, + items, + settings, + }; +}; diff --git a/client/modules/user-menu/parse-user-menu.spec.js b/client/modules/user-menu/parse-user-menu.spec.js new file mode 100644 index 00000000..fb763209 --- /dev/null +++ b/client/modules/user-menu/parse-user-menu.spec.js @@ -0,0 +1,35 @@ +import {test, stub} from 'supertape'; +import {parseUserMenu} from './parse-user-menu.js'; + +test('cloudcmd: user menu: parse', (t) => { + const fn = stub(); + const __settings = {}; + + const result = parseUserMenu({ + __settings, + 'F2 - Rename file': fn, + '_f': fn, + }); + + const names = [ + 'F2 - Rename file', + ]; + + const keys = { + F2: fn, + }; + + const items = { + 'Rename file': fn, + }; + + const expected = { + names, + keys, + items, + settings: __settings, + }; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/client/modules/user-menu/run.js b/client/modules/user-menu/run.js new file mode 100644 index 00000000..51e9fe02 --- /dev/null +++ b/client/modules/user-menu/run.js @@ -0,0 +1,5 @@ +export const runSelected = async (selectedItems, items, runUserMenu) => { + for (const selected of selectedItems) { + await runUserMenu(items[selected]); + } +}; diff --git a/client/modules/user-menu/run.spec.js b/client/modules/user-menu/run.spec.js new file mode 100644 index 00000000..86738712 --- /dev/null +++ b/client/modules/user-menu/run.spec.js @@ -0,0 +1,17 @@ +import {test, stub} from 'supertape'; +import {runSelected} from './run.js'; + +test('cloudcmd: client: user menu: run', async (t) => { + const runUserMenu = stub(); + const fn = stub(); + const selected = ['hello']; + + const items = { + hello: fn, + }; + + await runSelected(selected, items, runUserMenu); + + t.calledWith(runUserMenu, [fn]); + t.end(); +}); diff --git a/client/modules/view.js b/client/modules/view.js deleted file mode 100644 index befc0419..00000000 --- a/client/modules/view.js +++ /dev/null @@ -1,404 +0,0 @@ -'use strict'; - -/* global CloudCmd, DOM, $ */ - -require('../../css/view.css'); - -const itype = require('itype/legacy'); -const rendy = require('rendy/legacy'); -const exec = require('execon'); -const currify = require('currify/legacy'); - -const {time} = require('../../common/util'); -const {FS} = require('../../common/cloudfunc'); - -const Files = require('../dom/files'); -const Events = require('../dom/events'); -const load = require('../dom/load'); -const Images = require('../dom/images'); - -const testRegExp = currify((name, reg) => reg.test(name)); -const lifo = currify((fn, el, cb, name) => fn(name, el, cb)); - -const addEvent = lifo(Events.add); -const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); - -CloudCmd.View = ViewProto; - -module.exports = exec.bind(); - -module.exports.show = show; -module.exports.hide = hide; - -let Loading = false; - -const Name = 'View'; -const Info = DOM.CurrentInfo; -const Key = CloudCmd.Key; -const basename = (a) => a.split('/').pop(); - -let El, TemplateAudio, Overlay; - -const Config = { - beforeShow: (callback) => { - Images.hide(); - Key.unsetBind(); - showOverlay(); - exec(callback); - }, - beforeClose: (callback) => { - Events.rmKey(listener); - Key.setBind(); - exec(callback); - hideOverlay(); - }, - afterShow: (callback) => { - El.focus(); - exec(callback); - }, - afterClose : exec, - fitToView : true, - loop : false, - openEffect : 'none', - closeEffect : 'none', - autoSize : false, - height : '100%', - width : '100%', - minWidth : 0, - minHeight : 0, - padding : 0, - preload : 0, - keys : null, - mouseWheel : false, - arrows : false, - helpers : { - overlay : null, - title : null - } -}; - -function ViewProto(callback) { - const func = callback || exec.with(show, null); - - Loading = true; - - exec.series([ - DOM.loadJquery, - loadAll, - (callback) => { - Loading = false; - exec(callback); - } - ], func); - - Config.parent = Overlay = load({ - id : 'js-view', - name : 'div', - className : 'fancybox-overlay fancybox-overlay-fixed' - }); - - const events = [ - 'click', - 'contextmenu', - ]; - - events.forEach(addEvent(Overlay, onOverLayClick)); - - return module.exports; -} - -function show(data, options) { - const prefixUrl = CloudCmd.PREFIX_URL + FS; - - if (Loading) - return; - - if (!options || options.bindKeys !== false) - Events.addKey(listener); - - El = $('
'); - - if (data) { - const element = $(El).append(data); - $.fancybox.open(element, initConfig(Config, options)); - return; - } - - Images.show.load(); - - const path = prefixUrl + Info.path; - const type = getType(path); - - switch(type) { - default: - return Info.getData((error, data) => { - if (error) - return Images.hide(); - - const element = document.createTextNode(data); - /* add margin only for view text documents */ - El.css('margin', '2%'); - - $.fancybox.open(El.append(element), Config); - }); - - case 'image': - return showImage(path, prefixUrl); - - case 'media': - return getMediaElement(path, (element) => { - const media = DOM.getByDataName('js-media', element); - const onKey = exec.with(onMediaKey, media); - - $.fancybox.open(element, { - parent : Overlay, - beforeShow : () => { - Config.beforeShow(); - Events.addKey(onKey); - }, - beforeClose : () => { - Config.beforeClose(); - Events.rmKey(onKey); - }, - afterShow: () => { - element - .querySelector('audio, video') - .focus(); - }, - helpers: { - overlay : null, - title : null - } - }); - }); - } -} - -function initConfig(Config, options) { - const config = Object.assign({}, Config); - - if (!options) - return config; - - Object.keys(options).forEach((name) => { - const isConfig = !!config[name]; - const item = options[name]; - const isFunc = itype.function(item); - - if (!isFunc || !isConfig) { - config[name] = options[name]; - return; - } - - const func = config[name]; - config[name] = () => { - exec.series([func, item]); - }; - }); - - return config; -} - -function hide() { - $.fancybox.close(); -} - -function showImage(href, prefixUrl) { - const makeTitle = (path) => { - return { - href: prefixUrl + path, - title: basename(path), - }; - }; - - const names = Info.files - .map(DOM.getCurrentPath) - .filter(isImage); - - const titles = names - .map(makeTitle); - - const index = names.indexOf(Info.path); - const imageConfig = { - index, - autoSize : true, - type : 'image', - prevEffect : 'none', - nextEffect : 'none', - arrows : true, - keys : true, - helpers : { - overlay : null, - title : {} - } - }; - - const config = { - ...Config, - ...imageConfig, - }; - - $.fancybox.open(titles, config); -} - -function isImage(name) { - const images = [ - 'jp(e|g|eg)', - 'gif', - 'png', - 'bmp', - 'webp', - 'svg', - 'ico' - ]; - - return images - .map(getRegExp) - .some(testRegExp(name)); -} - -function isMedia(name) { - return isAudio(name) || isVideo(name); -} - -function isAudio(name) { - return /\.(mp3|ogg|m4a)$/i.test(name); -} - -function isVideo(name) { - return /\.(mp4|avi)$/i.test(name); -} - -function getType(name) { - if (isImage(name)) - return 'image'; - - if (isMedia(name)) - return 'media'; -} - -function getMediaElement(src, callback) { - check(src, callback); - - Files.get('view/media-tmpl', (error, template) => { - const {name} = Info; - - if (error) - return alert(error); - - if (!TemplateAudio) - TemplateAudio = template; - - const is = isAudio(name); - const type = is ? 'audio' : 'video'; - - const rendered = rendy(TemplateAudio, { - src, - type, - name, - }); - - const [element] = $(rendered); - callback(element); - }); -} - -function check(src, callback) { - if (typeof src !== 'string') - throw Error('src should be a string!'); - - if (typeof callback !== 'function') - throw Error('callback should be a function'); -} - -function onMediaKey(media, event) { - const {keyCode} = event; - - if (keyCode === Key.SPACE) { - if (media.paused) - media.play(); - else - media.pause(); - } -} - -/** - * function loads css and js of FancyBox - * @callback - executes, when everything loaded - */ -function loadAll(callback) { - time(Name + ' load'); - - DOM.loadRemote('fancybox', () => { - const {PREFIX} = CloudCmd; - - load.css(PREFIX + '/dist/view.css', callback); - - load.style({ - id : 'view-inlince-css', - inner : [ - '.fancybox-title-float-wrap .child {', - '-webkit-border-radius: 0;', - '-moz-border-radius: 0;', - 'border-radius: 0;', - '}' - ].join('') - }); - }); -} - -function onOverLayClick(event) { - const {target} = event; - const isOverlay = target === Overlay; - const position = { - x: event.clientX, - y: event.clientY - }; - - if (!isOverlay) - return; - - hideOverlay(); - hide(); - - setCurrentByPosition(position); -} - -function setCurrentByPosition(position) { - const element = DOM.getCurrentByPosition(position); - - if (!element) - return; - - const { - files, - filesPassive, - } = Info; - - const isFiles = ~files.indexOf(element); - const isFilesPassive = ~filesPassive.indexOf(element); - - if (!isFiles && !isFilesPassive) - return; - - const isCurrent = DOM.isCurrentFile(element); - - if (isCurrent) - return; - - DOM.setCurrentFile(element); -} - -function hideOverlay() { - Overlay.classList.remove('view-overlay'); -} - -function showOverlay() { - Overlay.classList.add('view-overlay'); -} - -function listener({keyCode}) { - if (keyCode === Key.ESC) - hide(); -} - diff --git a/client/modules/view/get-type.js b/client/modules/view/get-type.js new file mode 100644 index 00000000..a061399f --- /dev/null +++ b/client/modules/view/get-type.js @@ -0,0 +1,51 @@ +import currify from 'currify'; + +const testRegExp = currify((name, reg) => reg.test(name)); +const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); + +const isPDF = (a) => /\.pdf$/i.test(a); +const isHTML = (a) => a.endsWith('.html'); +const isMarkdown = (a) => /.\.md$/.test(a); + +export default (name) => { + if (isPDF(name)) + return 'pdf'; + + if (isImage(name)) + return 'image'; + + if (isMedia(name)) + return 'media'; + + if (isHTML(name)) + return 'html'; + + if (isMarkdown(name)) + return 'markdown'; +}; + +function isImage(name) { + const images = [ + 'jp(e|g|eg)', + 'gif', + 'png', + 'bmp', + 'webp', + 'svg', + 'ico', + ]; + + return images + .map(getRegExp) + .some(testRegExp(name)); +} + +function isMedia(name) { + return isAudio(name) || isVideo(name); +} + +const isAudio = (name) => /\.(mp3|ogg|m4a)$/i.test(name); + +function isVideo(name) { + return /\.(mp4|avi|webm)$/i.test(name); +} diff --git a/client/modules/view/index.js b/client/modules/view/index.js new file mode 100644 index 00000000..6626f1aa --- /dev/null +++ b/client/modules/view/index.js @@ -0,0 +1,372 @@ +import '#css/view.css'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import wraptile from 'wraptile'; +import {tryToCatch} from 'try-to-catch'; +import load from 'load.js'; +import * as _modal from '@cloudcmd/modal'; +import _createElement from '@cloudcmd/create-element'; +import {time} from '#common/util'; +import * as Files from '#dom/files'; +import * as Events from '#dom/events'; +import {FS} from '#common/cloudfunc'; +import * as Images from '#dom/images'; +import {encode} from '#common/entity'; +import { + isImage, + isAudio, + getType, +} from './types.js'; + +const CloudCmd = globalThis.CloudCmd || {}; +const DOM = globalThis.DOM || {}; +const isString = (a) => typeof a === 'string'; +const {assign} = Object; +const {isArray} = Array; + +const lifo = currify((fn, el, cb, name) => fn(name, el, cb)); +const series = wraptile((...a) => { + for (const f of a) + f(); +}); + +const isFn = (a) => typeof a === 'function'; + +const noop = () => {}; +const addEvent = lifo(Events.add); + +const loadCSS = load.css; + +let Loading = false; + +const Name = 'View'; + +CloudCmd[Name] = { + init, + show, + hide, +}; + +const Info = DOM.CurrentInfo; +const {Key} = CloudCmd; + +const basename = (a) => a + .split('/') + .pop(); + +let El; +let TemplateAudio; +let Overlay; + +const Config = { + beforeShow: () => { + Images.hide(); + Key.unsetBind(); + }, + + beforeClose: () => { + Events.rmKey(listener); + Key.setBind(); + }, + + afterShow: () => { + El.focus(); + }, + + onOverlayClick, + afterClose: noop, + autoSize: false, + + helpers: { + title: {}, + }, +}; + +export const _Config = Config; + +export async function init() { + await loadAll(); + + const events = [ + 'click', + 'contextmenu', + ]; + + events.forEach(addEvent( + Overlay, + onOverlayClick, + )); +} + +export async function show(data, options = {}) { + const prefixURL = CloudCmd.prefixURL + FS; + + if (Loading) + return; + + if (!options || options.bindKeys !== false) + Events.addKey(listener); + + El = _createElement('div', { + className: 'view', + notAppend: true, + }); + + El.tabIndex = 0; + + if (data) { + if (isArray(data)) + El.append(...data); + else + El.append(data); + + _modal.open(El, initConfig(options)); + return; + } + + Images.show.load(); + + const path = prefixURL + Info.path; + const type = options.raw ? '' : await getType(path); + + switch(type) { + default: + return await viewFile(); + + case 'markdown': + return await CloudCmd.Markdown.show(Info.path); + + case 'html': + return viewHtml(path); + + case 'image': + return viewImage(Info.path, prefixURL); + + case 'media': + return await viewMedia(path); + + case 'pdf': + return viewPDF(path); + } +} + +export const _createIframe = createIframe; + +function createIframe(src, overrides = {}) { + const { + createElement = _createElement, + } = overrides; + + const element = createElement('iframe', { + src, + width: '100%', + height: '100%', + }); + + element.addEventListener('load', () => { + element.contentWindow.addEventListener('keydown', listener); + }); + + return element; +} + +export const _viewHtml = viewHtml; + +function viewHtml(src, overrides = {}) { + const {modal = _modal} = overrides; + modal.open(createIframe(src), Config); +} + +function viewPDF(src) { + const element = createIframe(src); + + const options = assign({}, Config); + + if (CloudCmd.config('showFileName')) + options.title = Info.name; + + _modal.open(element, options); +} + +async function viewMedia(path) { + const [e, element] = await getMediaElement(path); + + if (e) + return alert(e); + + const allConfig = { + ...Config, + ...{ + autoSize: true, + afterShow: () => { + element + .querySelector('audio, video') + .focus(); + }, + }, + }; + + _modal.open(element, allConfig); +} + +async function viewFile() { + const [error, data] = await Info.getData(); + + if (error) + return Images.hide(); + + const element = document.createTextNode(data); + const options = Config; + + if (CloudCmd.config('showFileName')) + options.title = Info.name; + + El.append(element); + _modal.open(El, options); +} + +const copy = (a) => assign({}, a); + +export const _initConfig = initConfig; + +function initConfig(options) { + const config = copy(Config); + + if (!options) + return config; + + const names = Object.keys(options); + + for (const name of names) { + const isConfig = Boolean(config[name]); + const item = options[name]; + + if (!isFn(item) || !isConfig) { + config[name] = options[name]; + continue; + } + + const fn = config[name]; + + config[name] = series(fn, item); + } + + return config; +} + +export function hide() { + _modal.close(); +} + +function viewImage(path, prefixURL) { + const isSupportedImage = (a) => isImage(a) || a === path; + const makeTitle = (path) => ({ + href: `${prefixURL}${path}`, + title: encode(basename(path)), + }); + + const names = Info.files + .map(DOM.getCurrentPath) + .filter(isSupportedImage); + + const titles = names.map(makeTitle); + + const index = names.indexOf(Info.path); + + const imageConfig = { + index, + autoSize: true, + arrows: true, + keys: true, + helpers: { + title: {}, + }, + }; + + const config = { + ...Config, + ...imageConfig, + }; + + _modal.open(titles, config); +} + +async function getMediaElement(src) { + check(src); + + const [error, template] = await tryToCatch(Files.get, 'view/media-tmpl'); + + if (error) + return [error]; + + const {name} = Info; + + if (!TemplateAudio) + TemplateAudio = template; + + const is = isAudio(name); + const type = is ? 'audio' : 'video'; + + const innerHTML = rendy(TemplateAudio, { + src, + type, + name, + }); + + const element = _createElement('div', { + innerHTML, + }); + + return [null, element]; +} + +function check(src) { + if (!isString(src)) + throw Error('src should be a string!'); +} + +async function loadAll() { + const {DIR_DIST} = CloudCmd; + + time(`${Name} load`); + + Loading = true; + await loadCSS(`${DIR_DIST}/view.css`); + Loading = false; +} + +function onOverlayClick(event) { + const position = { + x: event.clientX, + y: event.clientY, + }; + + setCurrentByPosition(position); +} + +function setCurrentByPosition(position) { + const element = DOM.getCurrentByPosition(position); + + if (!element) + return; + + const {files, filesPassive} = Info; + + const isFiles = files.includes(element); + const isFilesPassive = filesPassive.includes(element); + + if (!isFiles && !isFilesPassive) + return; + + const isCurrent = DOM.isCurrentFile(element); + + if (isCurrent) + return; + + DOM.setCurrentFile(element); +} + +function listener({keyCode}) { + if (keyCode === Key.ESC) + hide(); +} diff --git a/client/modules/view/index.spec.js b/client/modules/view/index.spec.js new file mode 100644 index 00000000..abfbe3f4 --- /dev/null +++ b/client/modules/view/index.spec.js @@ -0,0 +1,98 @@ +import autoGlobals from 'auto-globals'; +import {stub} from '@cloudcmd/stub'; +import {test as tape} from 'supertape'; +import { + _initConfig, + _viewHtml, + _Config, + _createIframe, +} from './index.js'; + +const test = autoGlobals(tape); + +test('cloudcmd: client: view: initConfig', (t) => { + let config; + let i = 0; + + const afterClose = () => ++i; + const options = { + afterClose, + }; + + config = _initConfig(options); + config.afterClose(); + + config = _initConfig(options); + config.afterClose(); + + t.equal(i, 2, 'should not change default config'); + t.end(); +}); + +test('cloudcmd: client: view: initConfig: no options', (t) => { + const config = _initConfig(); + const result = typeof config; + const expected = 'object'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudcmd: client: view: html', (t) => { + const open = stub(); + const modal = { + open, + }; + + const src = '/hello.html'; + + _viewHtml(src, { + modal, + }); + + const [first] = open.args; + const [arg] = first; + + t.deepEqual(first, [arg, _Config]); + t.end(); +}); + +test('cloudcmd: client: view: createIframe', (t) => { + const addEventListener = stub(); + const el = { + addEventListener, + }; + + const createElement = stub().returns(el); + const src = '/hello.html'; + + _createIframe(src, { + createElement, + }); + + const expected = { + src, + height: '100%', + width: '100%', + }; + + t.calledWith(createElement, ['iframe', expected]); + t.end(); +}); + +test('cloudcmd: client: view: createIframe: returns', (t) => { + const addEventListener = stub(); + const el = { + addEventListener, + }; + + const createElement = stub().returns(el); + + const src = '/hello.html'; + const result = _createIframe(src, { + createElement, + }); + + t.equal(result, el); + t.end(); +}); diff --git a/client/modules/view/types.js b/client/modules/view/types.js new file mode 100644 index 00000000..320a4bc8 --- /dev/null +++ b/client/modules/view/types.js @@ -0,0 +1,74 @@ +import {extname} from 'node:path'; +import currify from 'currify'; + +export const isAudio = (name) => /\.(mp3|ogg|m4a|flac)$/i.test(name); + +const testRegExp = currify((name, reg) => reg.test(name)); +const getRegExp = (ext) => RegExp(`\\.${ext}$`, 'i'); + +const isPDF = (a) => /\.pdf$/i.test(a); +const isHTML = (a) => a.endsWith('.html'); +const isMarkdown = (a) => /.\.md$/.test(a); + +export const getType = async (path) => { + const ext = extname(path); + + if (!ext) + path = await detectType(path); + + if (isPDF(path)) + return 'pdf'; + + if (isImage(path)) + return 'image'; + + if (isMedia(path)) + return 'media'; + + if (isHTML(path)) + return 'html'; + + if (isMarkdown(path)) + return 'markdown'; +}; + +export function isImage(name) { + const images = [ + 'jp(e|g|eg)', + 'gif', + 'png', + 'bmp', + 'webp', + 'svg', + 'ico', + ]; + + return images + .map(getRegExp) + .some(testRegExp(name)); +} + +function isMedia(name) { + return isAudio(name) || isVideo(name); +} + +function isVideo(name) { + return /\.(mp4|avi|webm)$/i.test(name); +} + +export const _detectType = detectType; + +async function detectType(path) { + const {headers} = await fetch(path, { + method: 'HEAD', + }); + + for (const [name, value] of headers) { + if (name === 'content-type') + return `.${value + .split('/') + .pop()}`; + } + + return ''; +} diff --git a/client/modules/view/types.spec.js b/client/modules/view/types.spec.js new file mode 100644 index 00000000..f0542c18 --- /dev/null +++ b/client/modules/view/types.spec.js @@ -0,0 +1,51 @@ +import {test, stub} from 'supertape'; +import {isAudio, _detectType} from './types.js'; + +test('cloudcmd: client: view: types: isAudio', (t) => { + const result = isAudio('hello.mp3'); + + t.ok(result); + t.end(); +}); + +test('cloudcmd: client: view: types: isAudio: flac', (t) => { + const result = isAudio('hello.flac'); + + t.ok(result); + t.end(); +}); + +test('cloudcmd: client: view: types: isAudio: no', (t) => { + const result = isAudio('hello'); + + t.notOk(result); + t.end(); +}); + +test('cloudcmd: client: view: types: detectType', async (t) => { + const fetch = stub().returns({ + headers: [], + }); + + globalThis.fetch = fetch; + await _detectType('/hello'); + + const expected = ['/hello', { + method: 'HEAD', + }]; + + t.calledWith(fetch, expected); + t.end(); +}); + +test('cloudcmd: client: view: types: detectType: found', async (t) => { + globalThis.fetch = stub().returns({ + headers: [ + ['content-type', 'image/png'], + ], + }); + const result = await _detectType('/hello'); + + t.equal(result, '.png'); + t.end(); +}); diff --git a/client/sort.js b/client/sort.js index adf769c4..ed0971f3 100644 --- a/client/sort.js +++ b/client/sort.js @@ -1,35 +1,32 @@ -'use strict'; - /* global CloudCmd */ -const DOM = require('./dom'); +import {fullstore} from 'fullstore'; +import DOM from '#dom'; -const Info = DOM.CurrentInfo; -const sort = CloudCmd.sort; -const order = CloudCmd.order; -const position = DOM.getPanelPosition(); - -let sortPrevious = sort[position]; +const sortPrevious = fullstore(); const {getPanel} = DOM; -CloudCmd.sortPanel = (name, panel = getPanel()) => { - const position = panel - .dataset - .name - .replace('js-', ''); +export const initSortPanel = () => { + const {sort} = CloudCmd; + const position = DOM.getPanelPosition(); - if (name !== sortPrevious) { + sortPrevious(sort[position]); +}; + +export const sortPanel = (name, panel = getPanel()) => { + const {sort, order} = CloudCmd; + const Info = DOM.CurrentInfo; + const position = panel.dataset.name.replace('js-', ''); + + if (name !== sortPrevious()) + order[position] = 'asc'; + else if (order[position] === 'asc') + order[position] = 'desc'; + else order[position] = 'asc'; - } else { - if (order[position] === 'asc') - order[position] = 'desc'; - else - order[position] = 'asc'; - } - sortPrevious = + sortPrevious(name); sort[position] = name; - const noCurrent = position !== Info.panelPosition; CloudCmd.refresh({ @@ -37,4 +34,3 @@ CloudCmd.sortPanel = (name, panel = getPanel()) => { noCurrent, }); }; - diff --git a/client/sw/register.js b/client/sw/register.js index 73019c30..928d63c9 100644 --- a/client/sw/register.js +++ b/client/sw/register.js @@ -1,10 +1,10 @@ -'use strict'; +import {tryToCatch} from 'try-to-catch'; -module.exports.registerSW = registerSW; -module.exports.unregisterSW = unregisterSW; +export const listenSW = (sw, ...args) => { + sw?.addEventListener(...args); +}; -async function registerSW(prefix) { - prefix = prefix ? `/${prefix}/` : `/`; +export async function registerSW(prefix) { if (!navigator.serviceWorker) return; @@ -14,11 +14,17 @@ async function registerSW(prefix) { if (!isHTTPS && !isLocalhost) return; - return navigator.serviceWorker.register(`${prefix}sw.js`); + const {serviceWorker} = navigator; + const register = serviceWorker.register.bind(serviceWorker); + const [e, sw] = await tryToCatch(register, `${prefix}/sw.js`); + + if (e) + return null; + + return sw; } -async function unregisterSW() { - const reg = await registerSW(); - return reg.unregister(); +export async function unregisterSW(prefix) { + const reg = await registerSW(prefix); + reg?.unregister(prefix); } - diff --git a/client/sw/register.spec.js b/client/sw/register.spec.js index 9a83d341..52699f0b 100644 --- a/client/sw/register.spec.js +++ b/client/sw/register.spec.js @@ -1,155 +1,103 @@ -'use strict'; +import autoGlobals from 'auto-globals'; +import tape from 'supertape'; +import {stub} from '@cloudcmd/stub'; +import {tryCatch} from 'try-catch'; +import { + listenSW, + registerSW, + unregisterSW, +} from './register.js'; -const test = require('tape'); -const sinon = require('sinon'); -const mock = require('mock-require'); +const test = autoGlobals(tape); -test('sw: register: registerSW: no serviceWorker', async (t) => { - const {navigator} = global; - global.navigator = {}; +test('sw: listen', (t) => { + const addEventListener = stub(); + const sw = { + addEventListener, + }; - const { - registerSW, - } = mock.reRequire('./register'); + listenSW(sw, 'hello', 'world'); + + t.calledWith(addEventListener, ['hello', 'world'], 'should call addEventListener'); + t.end(); +}); + +test('sw: lesten: no sw', (t) => { + const [e] = tryCatch(listenSW, null, 'hello', 'world'); + + t.notOk(e, 'should not throw'); + t.end(); +}); + +test('sw: register: registerSW: no serviceWorker', async (t, {navigator}) => { + delete navigator.serviceWorker; await registerSW(); - global.navigator = navigator; - t.pass('should not call register'); t.end(); }); -test('sw: register: registerSW: no https', async (t) => { - const { - navigator, - location, - } = global; +test('sw: register: registerSW: no https', async (t, {location, navigator}) => { + const {register} = navigator.serviceWorker; - const register = sinon.stub(); - - global.navigator = getNavigator({ - register, - }); - - global.location = { - protocol: 'http:' - }; - - const { - registerSW, - } = mock.reRequire('./register'); + location.protocol = 'http:'; await registerSW(); - global.location = location; - global.navigator = navigator; - - t.notOk(register.called, 'should not call register'); + t.notCalled(register, 'should not call register'); t.end(); }); -test('sw: register: registerSW: no localhost', async (t) => { - const { - navigator, - location, - } = global; - - global.location = { +test('sw: register: registerSW: http', async (t, {location, navigator}) => { + Object.assign(location, { protocol: 'http:', hostname: 'cloudcmd.io', - }; - - const register = sinon.stub(); - - global.navigator = getNavigator({ - register, }); - const { - registerSW, - } = mock.reRequire('./register'); + const {register} = navigator.serviceWorker; await registerSW(); - global.location = location; - global.navigator = navigator; - - t.notOk(register.called, 'should not call register'); + t.notCalled(register, 'should not call register'); t.end(); }); -test('sw: register: registerSW', async (t) => { - const { - navigator, - location, - } = global; - - global.location = { - hostname: 'localhost', - }; - - const register = sinon.stub(); - - global.navigator = getNavigator({ - register, +test('sw: register: registerSW: https self-signed', async (t, {location, navigator}) => { + Object.assign(location, { + protocol: 'https:', + hostname: 'self-signed.badssl.com', }); - const { - registerSW, - } = mock.reRequire('./register'); + const {register} = navigator.serviceWorker; + register.throws(Error('Cannot register service worker!')); - await registerSW(); + const result = await registerSW(); - global.location = location; - global.navigator = navigator; - - t.ok(register.calledWith(), 'should call register'); + t.notOk(result, 'should not throw'); t.end(); }); -test('sw: register: unregisterSW', async (t) => { - const { - navigator, - location, - } = global; +test('sw: register: registerSW', async (t, {location, navigator}) => { + location.hostname = 'localhost'; - global.location = { - hostname: 'localhost', - }; + const {register} = navigator.serviceWorker; + await registerSW('/hello'); - const reg = { - unregister: sinon.stub() - }; - - const register = sinon.stub() - .returns(Promise.resolve(reg)); - - global.navigator = getNavigator({ - register, - }); - - const { - unregisterSW, - } = mock.reRequire('./register'); - - await unregisterSW(); - - global.location = location; - global.navigator = navigator; - - t.ok(register.calledWith(), 'should call register'); + t.calledWith(register, ['/hello/sw.js'], 'should call register'); t.end(); }); - -function getNavigator({register, unregister}) { - unregister = unregister || sinon.stub(); +test('sw: register: unregisterSW', async (t, {location, navigator}) => { + location.hostname = 'localhost'; - return { - serviceWorker: { - register, - unregister, - } - }; -} + const {serviceWorker} = navigator; + const {register} = serviceWorker; + + register.returns(serviceWorker); + + await unregisterSW('/hello'); + + t.calledWith(register, ['/hello/sw.js'], 'should call register'); + t.end(); +}); diff --git a/client/sw/sw.js b/client/sw/sw.js index 4126bc19..5b5643a3 100644 --- a/client/sw/sw.js +++ b/client/sw/sw.js @@ -1,21 +1,44 @@ -'use strict'; +import process from 'node:process'; +import codegen from 'codegen.macro'; +import {tryToCatch} from 'try-to-catch'; +import currify from 'currify'; -const preval = require('preval.macro'); -const tryToCatch = require('try-to-catch/legacy'); -const currify = require('currify/legacy'); - -const wait = currify((f, e) => e.waitUntil(f())); -const respondWith = currify((f, e) => e.respondWith(f(e))); -const getPathName = (url) => new URL(url).pathname; - -const date = preval`module.exports = Date()`; -const NAME = `cloudcmd: ${date}`; +const isDev = process.env.NODE_ENV === 'development'; const isGet = (a) => a.method === 'GET'; const isBasic = (a) => a.type === 'basic'; +const wait = currify((f, e) => e.waitUntil(f())); +const respondWith = currify((f, e) => { + const {request} = e; + const {url} = request; + const pathname = getPathName(url); + + if (url.endsWith('/') || /\^\/fs/.test(pathname)) + return; + + if (!isGet(request)) + return; + + if (!isBasic(request)) + return; + + if (pathname.startsWith('/api')) + return; + + if (/^socket.io/.test(pathname)) + return; + + e.respondWith(f(e)); +}); + +const getPathName = (url) => new URL(url).pathname; + +const date = codegen`module.exports = '"' + Date() + '"'`; +const NAME = `cloudcmd: ${date}`; + const createRequest = (a) => new Request(a, { - credentials: 'same-origin' + credentials: 'same-origin', }); const getRequest = (a, request) => { @@ -25,14 +48,14 @@ const getRequest = (a, request) => { return createRequest('/'); }; -self.addEventListener('install', wait(onInstall)); -self.addEventListener('fetch', respondWith(onFetch)); -self.addEventListener('activate', wait(onActivate)); +globalThis.addEventListener('install', wait(onInstall)); +globalThis.addEventListener('fetch', respondWith(onFetch)); +globalThis.addEventListener('activate', wait(onActivate)); async function onActivate() { console.info(`cloudcmd: sw: activate: ${NAME}`); - await self.clients.claim(); + await globalThis.clients.claim(); const keys = await caches.keys(); const deleteCache = caches.delete.bind(caches); @@ -42,48 +65,30 @@ async function onActivate() { async function onInstall() { console.info(`cloudcmd: sw: install: ${NAME}`); - await self.skipWaiting(); - - const cache = await caches.open(NAME); - - const urls = [ - '/favicon.ico', - ]; - - const requests = urls.map(createRequest); - return cache.addAll(requests); + await globalThis.skipWaiting(); } async function onFetch(event) { - const url = event.request.url; + const {request} = event; + const {url} = request; const pathname = getPathName(url); - const request = getRequest(pathname, event.request); + const newRequest = getRequest(pathname, event.request); const cache = await caches.open(NAME); const response = await cache.match(request); - if (response) + if (!isDev && response) return response; - - const [e, resp] = await tryToCatch(fetch, request.clone()); + + const [e, resp] = await tryToCatch(fetch, newRequest, { + credentials: 'same-origin', + }); if (e) - return console.error(e, response, pathname); - - if (!isGet(request) || !resp.ok || !isBasic(resp)) - return resp; - - if (/\/$/.test(pathname)) - return resp; - - if (/^\/api/.test(pathname)) - return resp; - - if (/^socket.io/.test(pathname)) - return resp; + return new Response(e.message); await addToCache(request, resp.clone()); - + return resp; } @@ -91,4 +96,3 @@ async function addToCache(request, response) { const cache = await caches.open(NAME); return cache.put(request, response); } - diff --git a/common/btoa.js b/common/btoa.js deleted file mode 100644 index 0293ebbc..00000000 --- a/common/btoa.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -/* global btoa */ - -module.exports = (str) => { - if (typeof btoa === 'function') - return btoa(str); - - return Buffer - .from(str) - .toString('base64'); -}; - diff --git a/common/callbackify.js b/common/callbackify.js new file mode 100644 index 00000000..71160ac7 --- /dev/null +++ b/common/callbackify.js @@ -0,0 +1,9 @@ +const success = (f) => (data) => f(null, data); + +export default (promise) => (...a) => { + const fn = a.pop(); + + promise(...a) + .then(success(fn)) + .catch(fn); +}; diff --git a/common/callbackify.spec.js b/common/callbackify.spec.js new file mode 100644 index 00000000..d1586d5a --- /dev/null +++ b/common/callbackify.spec.js @@ -0,0 +1,26 @@ +import {promisify} from 'node:util'; +import {tryToCatch} from 'try-to-catch'; +import {test, stub} from 'supertape'; +import callbackify from './callbackify.js'; + +test('cloudcmd: common: callbackify: error', async (t) => { + const promise = stub().rejects(Error('hello')); + + const fn = callbackify(promise); + const newPromise = promisify(fn); + const [error] = await tryToCatch(newPromise); + + t.equal(error.message, 'hello'); + t.end(); +}); + +test('cloudcmd: common: callbackify', async (t) => { + const promise = stub().resolves('hi'); + + const fn = callbackify(promise); + const promiseAgain = promisify(fn); + const data = await promiseAgain(); + + t.equal(data, 'hi'); + t.end(); +}); diff --git a/common/cloudfunc.js b/common/cloudfunc.js index bb806ef6..7884d069 100644 --- a/common/cloudfunc.js +++ b/common/cloudfunc.js @@ -1,48 +1,47 @@ -'use strict'; +import {rendy} from 'rendy'; +import currify from 'currify'; +import {fullstore} from 'fullstore'; +import {encode} from '#common/entity'; -const rendy = require('rendy/legacy'); -const currify = require('currify/legacy'); -const store = require('fullstore/legacy'); -const encode = require('./entity').encode; -const btoa = require('./btoa'); - -const getHeaderField = currify(_getHeaderField); - -/* КОНСТАНТЫ (общие для клиента и сервера)*/ - -/* название программы */ +const id = (a) => a; const NAME = 'Cloud Commander'; -const FS = '/fs'; -const Path = store(); + +export const dateFormatter = fullstore(id); +export const getHeaderField = currify(_getHeaderField); +export const FS = '/fs'; + +const Path = fullstore(); Path('/'); -module.exports.FS = FS; -module.exports.apiURL = '/api/v1'; -module.exports.MAX_FILE_SIZE = 500 * 1024; -module.exports.getHeaderField = getHeaderField; -module.exports.getPathLink = getPathLink; -module.exports.getDotDot = getDotDot; +const filterOutDotFiles = ({showDotFiles}) => ({name}) => { + if (showDotFiles) + return true; + + return !name.startsWith('.'); +}; -module.exports.formatMsg = (msg, name, status) => { +export const apiURL = '/api/v1'; +export const MAX_FILE_SIZE = 500 * 1024; + +export const formatMsg = (msg, name, status) => { status = status || 'ok'; name = name || ''; if (name) - name = '("' + name + '")'; + name = `("${name}")`; - return msg + ': ' + status + name; + return `${msg}: ${status}${name}`; }; /** * Функция возвращает заголовок веб страницы * @path */ -module.exports.getTitle = (options) => { +export const getTitle = (options) => { options = options || {}; - const path = options.path || Path(); - const name = options.name; + const {path = Path(), name} = options; const array = [ name || NAME, @@ -58,7 +57,7 @@ module.exports.getTitle = (options) => { * возвращаеться массив каталогов * @param url - адрес каталога */ -function getPathLink(url, prefix, template) { +export function getPathLink(url, prefix, template) { if (!url) throw Error('url could not be empty!'); @@ -69,55 +68,68 @@ function getPathLink(url, prefix, template) { .split('/') .slice(1, -1); - const allNames = ['/'].concat(names); - const length = allNames.length - 1; + const allNames = [ + '/', + ...names, + ]; + + const lines = []; + const n = allNames.length; let path = '/'; - const pathHTML = allNames.map((name, index) => { - const isLast = index === length; + for (let i = 0; i < n; i++) { + const name = allNames[i]; + const isLast = i === n - 1; - if (index) - path += name + '/'; + if (i) + path += `${name}/`; - if (index && isLast) - return name + '/'; + if (i && isLast) { + lines.push(`${name}/`); + continue; + } - const slash = index ? '/' : ''; + const slash = i ? '/' : ''; - return rendy(template, { + lines.push(rendy(template, { path, name, slash, prefix, - }); - }).join(''); + })); + } - return pathHTML; + return lines.join(''); } -const getDataName = (name) => { +export function _getDataName(name) { const encoded = btoa(encodeURI(name)); return `data-name="js-file-${encoded}" `; -}; +} /** * Функция строит таблицу файлв из JSON-информации о файлах * @param params - информация о файлах * */ -module.exports.buildFromJSON = (params) => { - const prefix = params.prefix; - const template = params.template; +export const buildFromJSON = (params) => { + const { + prefix, + template, + sort = 'name', + order = 'asc', + showDotFiles, + } = params; + + const formatDate = dateFormatter(); + const templateFile = template.file; const templateLink = template.link; const json = params.data; const path = encode(json.path); - const files = json.files; - - const sort = params.sort || 'name'; - const order = params.order || 'asc'; + const {files} = json; /* * Строим путь каталога в котором мы находимся @@ -126,9 +138,9 @@ module.exports.buildFromJSON = (params) => { const htmlPath = getPathLink(path, prefix, template.pathLink); let fileTable = rendy(template.path, { - link : prefix + FS + path, - fullPath : path, - path : htmlPath, + link: prefix + FS + path, + fullPath: path, + path: htmlPath, }); const owner = 'owner'; @@ -139,15 +151,17 @@ module.exports.buildFromJSON = (params) => { const name = getFieldName('name'); const size = getFieldName('size'); const date = getFieldName('date'); + const time = getFieldName('time'); const header = rendy(templateFile, { - tag : 'div', - attribute : 'data-name="js-fm-header" ', - className : 'fm-header', - type : '', + tag: 'div', + attribute: 'data-name="js-fm-header" ', + className: 'fm-header', + type: '', name, size, date, + time, owner, mode, }); @@ -155,7 +169,8 @@ module.exports.buildFromJSON = (params) => { /* сохраняем путь */ Path(path); - fileTable += header + '
    '; + fileTable += `${header}
      `; + /* Если мы не в корне */ if (path !== '/') { const dotDot = getDotDot(path); @@ -163,89 +178,105 @@ module.exports.buildFromJSON = (params) => { const linkResult = rendy(template.link, { link, - title : '..', - name : '..' + title: '..', + name: '..', }); - const dataName = 'data-name="js-file-.." '; - const attribute = 'draggable="true" ' + dataName; + const dataName = _getDataName('..'); + const attribute = `draggable="true" ${dataName}`; /* Сохраняем путь к каталогу верхнего уровня*/ fileTable += rendy(template.file, { - tag : 'li', - attribute, - className : '', - type : 'directory', - name : linkResult, - size : '<dir>', - date : '--.--.----', - owner : '.', - mode : '--- --- ---' - }); - } - - fileTable += files.map((file) => { - const name = encode(file.name); - const link = prefix + FS + path + name; - - const type = getType(file.size); - const size = getSize(file.size); - - const date = file.date || '--.--.----'; - const owner = file.owner || 'root'; - const mode = file.mode; - - const linkResult = rendy(templateLink, { - link, - title: name, - name, - attribute: getAttribute(file.size) - }); - - const dataName = getDataName(file.name); - const attribute = `draggable="true" ${dataName}`; - - return rendy(templateFile, { tag: 'li', attribute, className: '', - type, + type: 'directory', name: linkResult, - size, - date, - owner, - mode, + size: '<dir>', + date: '--.--.----', + time: '--:--:--', + owner: '.', + mode: '--- --- ---', }); - }).join(''); + } + + fileTable += files + .filter(filterOutDotFiles({ + showDotFiles, + })) + .map(updateField) + .map((file) => { + const name = encode(file.name); + const link = prefix + FS + path + name; + + const { + type, + mode, + date, + time, + owner, + size, + } = file; + + const linkResult = rendy(templateLink, { + link, + title: name, + name, + attribute: getAttribute(file.type), + }); + + const dataName = _getDataName(file.name); + const attribute = `draggable="true" ${dataName}`; + + return rendy(templateFile, { + tag: 'li', + attribute, + className: '', + type, + name: linkResult, + size, + date: formatDate(date), + time, + owner, + mode, + }); + }) + .join(''); fileTable += '
    '; return fileTable; }; -function getType(size) { - if (size === 'dir') - return 'directory'; - - return 'text-file'; -} +const updateField = (file) => ({ + ...file, + date: file.date || '--.--.----', + time: file.time || '--:--:--', + owner: file.owner || 'root', + size: getSize(file), +}); -function getAttribute(size) { - if (size === 'dir') +function getAttribute(type) { + if (type === 'directory') return ''; return 'target="_blank" '; } -function getSize(size) { - if (size === 'dir') +export const _getSize = getSize; + +function getSize({size, type}) { + if (type === 'directory') return '<dir>'; + if (/link/.test(type)) + return '<link>'; + return size; } function _getHeaderField(sort, order, name) { - const arrow = order === 'asc' ? '↑' : '↓'; + const arrow = order === 'asc' ? '↑' : '↓'; if (sort !== name) return name; @@ -256,7 +287,7 @@ function _getHeaderField(sort, order, name) { return `${name}${arrow}`; } -function getDotDot(path) { +export function getDotDot(path) { // убираем последний слеш и каталог в котором мы сейчас находимся const lastSlash = path.substr(path, path.lastIndexOf('/')); const dotDot = lastSlash.substr(lastSlash, lastSlash.lastIndexOf('/')); @@ -266,4 +297,3 @@ function getDotDot(path) { return dotDot; } - diff --git a/common/cloudfunc.spec.js b/common/cloudfunc.spec.js new file mode 100644 index 00000000..b679feeb --- /dev/null +++ b/common/cloudfunc.spec.js @@ -0,0 +1,364 @@ +import {readFileSync} from 'node:fs'; +import test from 'supertape'; +import {montag} from 'montag'; +import * as cheerio from 'cheerio'; +import {tryCatch} from 'try-catch'; +import { + _getSize, + getPathLink, + buildFromJSON, + _getDataName, + dateFormatter, + formatMsg, + getTitle, + getDotDot, + getHeaderField, +} from '#common/cloudfunc'; + +const templatePath = new URL('../tmpl/fs', import.meta.url).pathname; + +const template = { + pathLink: readFileSync(`${templatePath}/pathLink.hbs`, 'utf8'), + path: readFileSync(`${templatePath}/path.hbs`, 'utf8'), + file: readFileSync(`${templatePath}/file.hbs`, 'utf8'), + link: readFileSync(`${templatePath}/link.hbs`, 'utf8'), +}; + +test('cloudfunc: buildFromJSON: ..', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: 'floppy', + owner: 'root', + size: '7b', + type: 'directory-link', + }], + }; + + const html = buildFromJSON({ + prefix: '', + template, + data, + }); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-Li4="]'); + + const result = el + .find('[data-name="js-name"]') + .text(); + + const expected = '..'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getPathLink: /', (t) => { + const {pathLink} = template; + const result = getPathLink('/', '', pathLink); + + const expected = montag` + / + `; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getPathLink: /hello/world', (t) => { + const {pathLink} = template; + const result = getPathLink('/hello/world', '', pathLink); + + const expected = montag` + /hello/ + `; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getPathLink: prefix', (t) => { + const {pathLink} = template; + const result = getPathLink('/hello/world', '/cloudcmd', pathLink); + + const expected = montag` + /hello/ + `; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getSize: dir', (t) => { + const type = 'directory'; + const size = 0; + + const result = _getSize({ + type, + size, + }); + + const expected = '<dir>'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getSize: link: dir', (t) => { + const type = 'directory-link'; + const size = 0; + + const result = _getSize({ + type, + size, + }); + + const expected = '<link>'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getSize: link: file', (t) => { + const type = 'file-link'; + const size = 0; + + const result = _getSize({ + type, + size, + }); + + const expected = '<link>'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: getSize: file', (t) => { + const type = 'file'; + const size = '100.00kb'; + + const result = _getSize({ + type, + size, + }); + + const expected = '100.00kb'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: buildFromJSON: showDotFiles: false', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '.floppy', + owner: 'root', + size: '7b', + type: 'directory-link', + }], + }; + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-LmZsb3BweQ=="]'); + + const result = el + .find('[data-name="js-name"]') + .text(); + + const expected = ''; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: buildFromJSON: name: {{ }}', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '{{}}', + owner: 'root', + size: '7b', + type: 'file', + }], + }; + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-JTdCJTdCJTdEJTdE"]'); + + const result = el + .find('[data-name="js-name"]') + .text(); + + const expected = '{{}}'; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: _getDataName', (t) => { + const result = _getDataName('s'); + const expected = 'data-name="js-file-cw==" '; + + t.equal(result, expected); + t.end(); +}); + +test('cloudfunc: formatMsg: name', (t) => { + const result = formatMsg('hello', 'world'); + const expected = 'hello: ok("world")'; + + t.equal(result, expected, 'should format message with name'); + t.end(); +}); + +test('cloudfunc: formatMsg: no name', (t) => { + const result = formatMsg('hello'); + const expected = 'hello: ok'; + + t.equal(result, expected, 'should format message without name'); + t.end(); +}); + +test('cloudfunc: getTitle: no options', (t) => { + const result = getTitle(); + + t.ok(result, 'should return a title string even without options'); + t.end(); +}); + +test('cloudfunc: getTitle: with name', (t) => { + const result = getTitle({ + name: 'MyName', + }); + + t.match(result, 'MyName', 'should return title with name'); + t.end(); +}); + +test('cloudfunc: getHeaderField: sort not name', (t) => { + const result = getHeaderField('size', 'asc', 'name'); + + t.equal(result, 'name', 'should return plain name when sort does not match'); + t.end(); +}); + +test('cloudfunc: getHeaderField: sort name asc', (t) => { + const result = getHeaderField('name', 'asc', 'name'); + + t.equal(result, 'name', 'should return plain name when name asc'); + t.end(); +}); + +test('cloudfunc: getDotDot: root', (t) => { + const result = getDotDot('/'); + + t.equal(result, '/', 'should return / for root path'); + t.end(); +}); + +test('cloudfunc: getPathLink: /a/b/c', (t) => { + const {pathLink} = template; + const result = getPathLink('/a/b/c/', '', pathLink); + + t.ok(result, 'should build path link for 3-segment path'); + t.end(); +}); + +test('cloudfunc: getPathLink: no url', (t) => { + const [error] = tryCatch(getPathLink); + + t.equal(error.message, 'url could not be empty!', 'should throw when url is empty'); + t.end(); +}); + +test('cloudfunc: getPathLink: no template', (t) => { + const [error] = tryCatch(getPathLink, '/'); + + t.equal(error.message, 'template could not be empty!', 'should throw when template is empty'); + t.end(); +}); + +test('cloudfunc: getHeaderField: sort name desc', (t) => { + const result = getHeaderField('name', 'desc', 'name'); + const expected = 'name↓'; + + t.equal(result, expected, 'should return name with down arrow'); + t.end(); +}); + +test('cloudfunc: getDotDot: normal path', (t) => { + const result = getDotDot('/hello/world/'); + const expected = '/hello'; + + t.equal(result, expected, 'should return parent directory'); + t.end(); +}); + +test('cloudfunc: buildFromJSON: formatDate', (t) => { + const data = { + path: '/media/', + files: [{ + date: '30.08.2016', + mode: 'rwx rwx rwx', + name: '{{}}', + owner: 'root', + size: '7b', + type: 'file', + }], + }; + + const oldFormatter = dateFormatter(); + + const formatDate = (str) => { + const [day, month, year] = str.split('.'); + const date = new Date(year, month - 1, day); + + return date.toLocaleDateString('en-US'); + }; + + dateFormatter(formatDate); + + const html = buildFromJSON({ + prefix: '', + template, + data, + showDotFiles: false, + }); + + dateFormatter(oldFormatter); + + const $ = cheerio.load(html); + const el = $('[data-name="js-file-JTdCJTdCJTdEJTdE"]'); + + const result = el + .find('[data-name="js-date"]') + .text(); + + const expected = '8/30/2016'; + + t.equal(result, expected); + t.end(); +}); diff --git a/common/datetime.js b/common/datetime.js new file mode 100644 index 00000000..d7982ab4 --- /dev/null +++ b/common/datetime.js @@ -0,0 +1,31 @@ +import shortdate from 'shortdate'; + +export default (date) => { + date = date || new Date(); + check(date); + + const timeStr = shorttime(date); + const dateStr = shortdate(date); + + return `${dateStr} ${timeStr}`; +}; + +const addZero = (a) => { + if (a > 9) + return a; + + return `0${a}`; +}; + +function shorttime(date) { + const seconds = addZero(date.getSeconds()); + const minutes = addZero(date.getMinutes()); + const hours = addZero(date.getHours()); + + return `${hours}:${minutes}:${seconds}`; +} + +function check(date) { + if (!(date instanceof Date)) + throw Error('date should be instanceof Date!'); +} diff --git a/common/datetime.spec.js b/common/datetime.spec.js new file mode 100644 index 00000000..e3edb2ef --- /dev/null +++ b/common/datetime.spec.js @@ -0,0 +1,50 @@ +import {test} from 'supertape'; +import {tryCatch} from 'try-catch'; +import datetime from './datetime.js'; + +test('common: datetime', (t) => { + const dateStr = 'Fri, 17 Aug 2018 10:56:48'; + const result = datetime(new Date(dateStr)); + + const expected = '2018.08.17 10:56:48'; + + t.equal(result, expected); + t.end(); +}); + +test('common: datetime: no arg', (t) => { + const {Date} = globalThis; + + let called = false; + + globalThis.Date = class extends Date { + constructor() { + super(); + called = true; + } + }; + + datetime(); + + globalThis.Date = Date; + + t.ok(called, 'should call new Date'); + t.end(); +}); + +test('common: 0 before number', (t) => { + const dateStr = 'Fri, 17 Aug 2018 10:56:08'; + const result = datetime(new Date(dateStr)); + + const expected = '2018.08.17 10:56:08'; + + t.equal(result, expected); + t.end(); +}); + +test('common: datetime: wrong args', (t) => { + const [error] = tryCatch(datetime, {}); + + t.equal(error.message, 'date should be instanceof Date!', 'should throw'); + t.end(); +}); diff --git a/common/entity.js b/common/entity.js index c3c718c4..690bbd7a 100644 --- a/common/entity.js +++ b/common/entity.js @@ -1,33 +1,31 @@ -'use strict'; - const Entities = { - ' ': ' ', '<': '<', '>': '>', '"': '"', + '{': '{', + '}': '}', }; const keys = Object.keys(Entities); -module.exports.encode = (str) => { - keys.forEach((code) => { +export const encode = (str) => { + for (const code of keys) { const char = Entities[code]; const reg = RegExp(char, 'g'); str = str.replace(reg, code); - }); + } return str; }; -module.exports.decode = (str) => { - keys.forEach((code) => { +export const decode = (str) => { + for (const code of keys) { const char = Entities[code]; const reg = RegExp(code, 'g'); str = str.replace(reg, char); - }); + } return str; }; - diff --git a/test/common/entity.js b/common/entity.spec.js similarity index 53% rename from test/common/entity.js rename to common/entity.spec.js index 43b54d48..79313f7c 100644 --- a/test/common/entity.js +++ b/common/entity.spec.js @@ -1,29 +1,34 @@ -'use strict'; - -const test = require('tape'); -const entity = require('../../common/entity'); +import {test} from 'supertape'; +import * as entity from '#common/entity'; test('cloudcmd: entity: encode', (t) => { const result = entity.encode(' '); - const expected = '<hello> '; + const expected = '<hello> '; + + t.equal(result, expected, 'should encode entity'); + t.end(); +}); + +test('cloudcmd: entity: {{}}', (t) => { + const result = entity.encode('{{}}'); + const expected = '{{}}'; t.equal(result, expected, 'should encode entity'); t.end(); }); test('cloudcmd: entity: decode', (t) => { - const result = entity.decode('<hello> '); + const result = entity.decode('<hello> '); const expected = ' '; t.equal(result, expected, 'should decode entity'); t.end(); }); -test('cloudcmd: entity: encode', (t) => { +test('cloudcmd: entity: encode quote', (t) => { const result = entity.encode('"hello"'); const expected = '"hello"'; t.equal(result, expected, 'should encode entity'); t.end(); }); - diff --git a/common/omit.js b/common/omit.js new file mode 100644 index 00000000..284c2f5b --- /dev/null +++ b/common/omit.js @@ -0,0 +1,12 @@ +const difference = (a, b) => new Set(a).difference(new Set(b)); +const {keys} = Object; + +export const omit = (a, list) => { + const result = {}; + + for (const key of difference(keys(a), list)) { + result[key] = a[key]; + } + + return result; +}; diff --git a/common/omit.spec.js b/common/omit.spec.js new file mode 100644 index 00000000..ff70541c --- /dev/null +++ b/common/omit.spec.js @@ -0,0 +1,18 @@ +import {test} from 'supertape'; +import {omit} from '#common/omit'; + +test('cloudcmd: common: omit', (t) => { + const a = { + hello: 1, + world: 2, + }; + + const result = omit(a, ['world']); + + const expected = { + hello: 1, + }; + + t.deepEqual(result, expected); + t.end(); +}); diff --git a/common/try-to-promise-all.js b/common/try-to-promise-all.js new file mode 100644 index 00000000..1dcfa3a8 --- /dev/null +++ b/common/try-to-promise-all.js @@ -0,0 +1,12 @@ +import {tryToCatch} from 'try-to-catch'; + +const all = Promise.all.bind(Promise); + +export default async (a) => { + const [e, result = []] = await tryToCatch(all, a); + + return [ + e, + ...result, + ]; +}; diff --git a/common/try-to-promise-all.spec.js b/common/try-to-promise-all.spec.js new file mode 100644 index 00000000..eafb036c --- /dev/null +++ b/common/try-to-promise-all.spec.js @@ -0,0 +1,29 @@ +import {test} from 'supertape'; +import tryToPromiseAll from './try-to-promise-all.js'; + +const resolve = Promise.resolve.bind(Promise); +const reject = Promise.reject.bind(Promise); + +test('commons: try-to-promise-all', async (t) => { + const [, ...result] = await tryToPromiseAll([ + resolve('a'), + resolve('b'), + ]); + + const expected = [ + 'a', + 'b', + ]; + + t.deepEqual(result, expected); + t.end(); +}); + +test('commons: try-to-promise-all: error', async (t) => { + const [e] = await tryToPromiseAll([ + reject('a'), + ]); + + t.equal(e, 'a'); + t.end(); +}); diff --git a/common/util.js b/common/util.js index 3580b685..422b3c22 100644 --- a/common/util.js +++ b/common/util.js @@ -1,12 +1,9 @@ -'use strict'; +import exec from 'execon'; -const exec = require('execon'); +const isString = (a) => typeof a === 'string'; -module.exports.getStrBigFirst = getStrBigFirst; -module.exports.kebabToCamelCase = kebabToCamelCase; - -module.exports.escapeRegExp = (str) => { - const isStr = typeof str === 'string'; +export const escapeRegExp = (str) => { + const isStr = isString(str); if (isStr) str = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); @@ -17,27 +14,24 @@ module.exports.escapeRegExp = (str) => { /** * get regexp from wild card */ -module.exports.getRegExp = (wildcard) => { - wildcard = wildcard || '*'; - - const escaped = '^' + wildcard // search from start of line - .replace('.', '\\.') - .replace('*', '.*') - .replace('?', '.?') + '$'; // search to end of line +export const getRegExp = (wildcard) => { + const escaped = `^${wildcard // search from start of line + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace('?', '.?')}$`; + // search to end of line return RegExp(escaped); }; -module.exports.exec = exec; - /** * function gets file extension * * @param name * @return ext */ -module.exports.getExt = (name) => { - const isStr = typeof name === 'string'; +export const getExt = (name) => { + const isStr = isString(name); if (!isStr) return ''; @@ -51,22 +45,22 @@ module.exports.getExt = (name) => { }; /** - * find object by name in arrray + * find object by name in array * * @param array * @param name */ -module.exports.findObjByNameInArr = (array, name) => { +export const findObjByNameInArr = (array, name) => { let ret; if (!Array.isArray(array)) throw Error('array should be array!'); - - if (typeof name !== 'string') + + if (!isString(name)) throw Error('name should be string!'); array.some((item) => { - let is = item.name === name; + const is = item.name === name; const isArray = Array.isArray(item); if (is) { @@ -94,7 +88,7 @@ module.exports.findObjByNameInArr = (array, name) => { * start timer * @param name */ -module.exports.time = (name) => { +export const time = (name) => { exec.ifExist(console, 'time', [name]); }; @@ -102,27 +96,6 @@ module.exports.time = (name) => { * stop timer * @param name */ -module.exports.timeEnd = (name) => { +export const timeEnd = (name) => { exec.ifExist(console, 'timeEnd', [name]); }; - -function getStrBigFirst(str) { - if (!str) - throw Error('str could not be empty!'); - - const first = str[0].toUpperCase(); - - return first + str.slice(1); -} - -function kebabToCamelCase(str) { - if (!str) - throw Error('str could not be empty!'); - - return str - .split('-') - .map(getStrBigFirst) - .join('') - .replace(/.js$/, ''); -} - diff --git a/common/util.spec.js b/common/util.spec.js new file mode 100644 index 00000000..5a19a8be --- /dev/null +++ b/common/util.spec.js @@ -0,0 +1,138 @@ +import test from 'supertape'; +import {tryCatch} from 'try-catch'; +import { + findObjByNameInArr, + getRegExp, + escapeRegExp, + getExt, + time, + timeEnd, +} from '#common/util'; + +test('getExt: no extension', (t) => { + const EXT = ''; + const name = 'file-without-extension'; + const ext = getExt(name); + + t.equal(ext, EXT, 'should return "" when extension is none'); + t.end(); +}); + +test('getExt: return extension', (t) => { + const EXT = '.png'; + const name = 'picture.png'; + const ext = getExt(name); + + t.equal(ext, EXT, 'should return ".png" in files "picture.png"'); + t.end(); +}); + +test('util: getExt: no name', (t) => { + const ext = getExt(); + + t.equal(ext, '', 'should return empty string'); + t.end(); +}); + +test('util: findObjByNameInArr: no array', (t) => { + const [error] = tryCatch(findObjByNameInArr); + + t.equal(error.message, 'array should be array!', 'should throw when no array'); + t.end(); +}); + +test('util: findObjByNameInArr: no name', (t) => { + const [error] = tryCatch(findObjByNameInArr, []); + + t.equal(error.message, 'name should be string!', 'should throw when no array'); + t.end(); +}); + +test('util: findObjByNameInArr: object', (t) => { + const name = 'hello'; + const obj = { + name, + }; + + const array = [obj]; + + const result = findObjByNameInArr(array, name); + + t.equal(result, obj, 'should return obj'); + t.end(); +}); + +test('util: findObjByNameInArr: array', (t) => { + const name = 'hello'; + const data = 'abc'; + + const item = { + name, + data, + }; + + const obj = { + name: 'world', + }; + + const array = [ + name, + [obj, item], + ]; + + const result = findObjByNameInArr(array, name); + + t.equal(result, data, 'should return data'); + t.end(); +}); + +test('util: getRegExp', (t) => { + const reg = getRegExp('help?o.*'); + + t.deepEqual(reg, /^help.?o\..*$/, 'should return regexp'); + t.end(); +}); + +test('util: getRegExp: dots', (t) => { + const reg = getRegExp('h.*el?o.*'); + + t.deepEqual(reg, /^h\..*el.?o\..*$/, 'should return regexp'); + t.end(); +}); + +test('util: getRegExp: no', (t) => { + const reg = getRegExp(''); + + t.deepEqual(reg, /^$/, 'should return regexp'); + t.end(); +}); + +test('util: escapeRegExp: no str', (t) => { + const result = escapeRegExp(1); + const expected = 1; + + t.equal(result, expected); + t.end(); +}); + +test('util: escapeRegExp', (t) => { + const result = escapeRegExp('#hello'); + const expected = '\\#hello'; + + t.equal(result, expected); + t.end(); +}); + +test('util: time', (t) => { + const [error] = tryCatch(time, 'test'); + + t.notOk(error, 'should not throw'); + t.end(); +}); + +test('util: timeEnd', (t) => { + const [error] = tryCatch(timeEnd, 'test'); + + t.notOk(error, 'should not throw'); + t.end(); +}); diff --git a/css/columns/name-size-date-time.css b/css/columns/name-size-date-time.css new file mode 100644 index 00000000..f46e63f8 --- /dev/null +++ b/css/columns/name-size-date-time.css @@ -0,0 +1,26 @@ +.name { + width: 35%; +} + +.size { + float: none; +} + +.owner { + display: none; +} + +.mode { + display: none; +} + +.date { + float: right; + width: 19%; +} + +.time { + display: inline; + float: right; + width: 20%; +} diff --git a/css/columns/name-size-date.css b/css/columns/name-size-date.css index c56f3015..9248f700 100644 --- a/css/columns/name-size-date.css +++ b/css/columns/name-size-date.css @@ -1,5 +1,5 @@ .name { - width: 59%; + width: 55%; } .size { @@ -19,3 +19,7 @@ width: 19%; } +.time { + display: none; +} + diff --git a/css/columns/name-size-time.css b/css/columns/name-size-time.css new file mode 100644 index 00000000..1bbd59de --- /dev/null +++ b/css/columns/name-size-time.css @@ -0,0 +1,24 @@ +.name { + width: 55%; +} + +.size { + float: none; +} + +.owner { + display: none; +} + +.mode { + display: none; +} + +.date { + display: none; +} + +.time { + float: right; + width: 19%; +} diff --git a/css/columns/name-size.css b/css/columns/name-size.css index e58d1844..c02472a5 100644 --- a/css/columns/name-size.css +++ b/css/columns/name-size.css @@ -18,3 +18,8 @@ .date { display: none; } + +.time { + display: none; +} + diff --git a/css/config.css b/css/config.css index ab456d97..4804374a 100644 --- a/css/config.css +++ b/css/config.css @@ -1,7 +1,7 @@ .config { white-space: normal; overflow: hidden; - width : 250px; + width: 250px; } .list li { @@ -18,15 +18,24 @@ padding: 0 12px; font-size: 16px; line-height: 1.5; - color: #555; + color: var(--column-color); + background: var(--internal-background); border: 1px solid #ccc; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - -moz-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; - transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + -webkit-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); + -moz-box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); + box-shadow: inset 0 1px 1px rgb(0 0 0 / 7.5%); + -webkit-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + -moz-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + -o-transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; + transition: + border-color ease-in-out 0.15s, + box-shadow ease-in-out 0.15s; } .config .form-control::-moz-placeholder { @@ -44,9 +53,15 @@ .config .form-control:focus { border-color: #66afe9; outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(102, 175, 233, 0.6); + -webkit-box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); + -moz-box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); + box-shadow: + inset 0 1px 1px rgb(0 0 0 / 7.5%), + 0 0 1px rgb(102 175 233 / 60%); } .config .form-control:focus:invalid:focus { @@ -57,8 +72,8 @@ } .config .list { - padding : 0; - margin : 5%; + padding: 0; + margin: 5%; } .config .full-width { diff --git a/css/help.css b/css/help.css index 42602618..6728c589 100644 --- a/css/help.css +++ b/css/help.css @@ -1,12 +1,12 @@ .help { - margin : 25px; - white-space : normal; + margin: 25px; + white-space: normal; } .help li { - list-style-type : disc; + list-style-type: disc; } .help img { - max-width : 100%; + max-width: 100%; } diff --git a/css/icons.css b/css/icons.css index 4f979db7..ca4a1181 100644 --- a/css/icons.css +++ b/css/icons.css @@ -1,134 +1,144 @@ .icon-help::before { - font-family : 'Fontello'; - content : '\e801 '; + font-family: 'Fontello'; + content: '\e801 '; } .icon-rename::before { - font-family : 'Fontello'; - content : '\e802 '; + font-family: 'Fontello'; + content: '\e802 '; } .icon-view::before { - font-family : 'Fontello'; - content : '\e803 '; + font-family: 'Fontello'; + content: '\e803 '; } .icon-edit::before { - font-family : 'Fontello'; - content : '\e804 '; + font-family: 'Fontello'; + content: '\e804 '; } .icon-copy::before { - font-family : 'Fontello'; - content : '\e805 '; + font-family: 'Fontello'; + content: '\e805 '; } .icon-move::before { - font-family : 'Fontello'; - content : '\e806 '; + font-family: 'Fontello'; + content: '\e806 '; } .icon-directory::before { - font-family : 'Fontello'; - content : '\e807 '; + font-family: 'Fontello'; + content: '\e807 '; } .icon-delete::before { - font-family : 'Fontello'; - content : '\e808 '; + font-family: 'Fontello'; + content: '\e808 '; } .icon-menu::before { - font-family : 'Fontello'; - content : '\e809 '; + font-family: 'Fontello'; + content: '\e809 '; } .icon-config::before { - font-family : 'Fontello'; - content : '\e80a '; + font-family: 'Fontello'; + content: '\e80a '; } .icon-console::before { - font-family : 'Fontello'; - content : '\e80b '; + font-family: 'Fontello'; + content: '\e80b '; } .icon-contact::before { - font-family : 'Fontello'; - content : '\e80c '; + font-family: 'Fontello'; + content: '\e80c '; } .icon-file::before { - font-family : 'Fontello'; - content : '\e80d '; + font-family: 'Fontello'; + content: '\e80d '; } .icon-upload-to-cloud::before { - font-family : 'Fontello'; - content : '\e80e '; + font-family: 'Fontello'; + content: '\e80e '; } .icon-upload-from-cloud::before { - font-family : 'Fontello'; - content : '\e80f '; + font-family: 'Fontello'; + content: '\e80f '; } .icon-download::before { - font-family : 'Fontello'; - content : '\e810 '; + font-family: 'Fontello'; + content: '\e810 '; } .icon-new::before { - font-family : 'Fontello'; - content : '\e811 '; + font-family: 'Fontello'; + content: '\e811 '; +} + +.icon-toggle-file-selection::before { + font-family: 'Fontello'; + content: '\e81f '; } .icon-unselect-all::before { - font-family : 'Fontello'; - content : '\e812 '; + font-family: 'Fontello'; + content: '\e812 '; } .icon-pack::before { - font-family : 'Fontello'; - content : '\e813 '; + font-family: 'Fontello'; + content: '\e813 '; } .icon-extract::before { - font-family : 'Fontello'; - content : '\e814 '; + font-family: 'Fontello'; + content: '\e814 '; } .icon-copy-to-clipboard::before { - font-family : 'Fontello'; - content : '\e815 '; + font-family: 'Fontello'; + content: '\e815 '; } .icon-refresh::before { - font-family : 'Fontello'; - content : '\e816 '; + font-family: 'Fontello'; + content: '\e816 '; } .icon-cut::before { - font-family : 'Fontello'; - content : '\e817 '; + font-family: 'Fontello'; + content: '\e817 '; } .icon-paste::before { - font-family : 'Fontello'; - content : '\e818 '; + font-family: 'Fontello'; + content: '\e818 '; } .icon-upload::before { - font-family : 'Fontello'; - content : '\e819 '; + font-family: 'Fontello'; + content: '\e819 '; } .icon-log-out::before { - font-family : 'Fontello'; - content : '\e81a '; + font-family: 'Fontello'; + content: '\e81a '; } .icon-terminal::before { - font-family : 'Fontello'; - content : '\e81b '; + font-family: 'Fontello'; + content: '\e81b '; +} + +.icon-user-menu::before { + font-family: 'Fontello'; + content: '\e81c '; } diff --git a/css/main.css b/css/main.css index ebaafe19..6b901a8e 100644 --- a/css/main.css +++ b/css/main.css @@ -1,9 +1,7 @@ -@import './urls.css'; -@import './reset.css'; -@import './style.css'; -@import './icons.css'; -@import './help.css'; -@import './query.css'; -@import './supports.css'; -@import './terminal.css'; - +@import url(./reset.css); +@import url(./urls.css); +@import url(./style.css); +@import url(./icons.css); +@import url(./help.css); +@import url(./query.css); +@import url(./supports.css); diff --git a/css/nojs.css b/css/nojs.css index 1df4bd12..350ac4e6 100644 --- a/css/nojs.css +++ b/css/nojs.css @@ -1,4 +1,5 @@ -.path-icon, .keyspanel { +.path-icon, +.keyspanel { display: none; } diff --git a/css/query.css b/css/query.css index bda5f93c..509f1b45 100644 --- a/css/query.css +++ b/css/query.css @@ -1,4 +1,31 @@ -@media only screen and (max-height: 900px) and (max-width: 600px) { +@media only screen and (width >= 1600px) { + .name { + width: 40%; + } + + .size { + width: 10%; + } + + .date { + width: 15%; + } + + .owner { + width: 12%; + } + + .mode { + width: 15%; + } +} + +:root { + --min-one-panel-width: 1155px; + --is-mobile: 0; +} + +@media only screen and (height <= 900px) and (width <= 600px) { .fm { height: 85%; } @@ -8,7 +35,7 @@ } } -@media only screen and (min-height: 550px) and (max-width: 600px) { +@media only screen and (height >= 550px) and (width <= 600px) { .fm { height: 80%; } @@ -18,43 +45,49 @@ } } -@media only screen and (max-height: 750px) and (max-width: 600px) { +@media only screen and (height <= 750px) and (width <= 600px) { .fm { height: 75%; } } -@media only screen and (max-height: 450px) and (max-width: 600px) { +@media only screen and (height <= 450px) and (width <= 600px) { .fm { height: 75%; } } -@media only screen and (max-height: 550px) and (max-width: 600px) { +@media only screen and (width <= 600px) { + :root { + --is-mobile: 1; + } +} + +@media only screen and (height <= 550px) and (width <= 600px) { .fm { height: 65%; } } -@media only screen and (max-height: 550px) and (max-width: 550px) { +@media only screen and (height <= 550px) and (width <= 550px) { .fm { height: 70%; } } -@media only screen and (min-height: 850px) and (min-width: 650px) { +@media only screen and (height >= 850px) and (width >= 650px) { .fm { height: 95%; } } -@media only screen and (max-height: 850px) { +@media only screen and (height <= 850px) { .files { height: 90%; } } -@media only screen and (max-height: 700px) and (min-width: 600px) { +@media only screen and (height <= 700px) and (width >= 600px) { .fm { height: 85%; } @@ -64,13 +97,13 @@ } } -@media only screen and (max-height: 450px) { +@media only screen and (height <= 450px) { .fm { height: 65%; } } -@media only screen and (max-height: 640px) and (max-width: 360px) { +@media only screen and (height <= 640px) and (width <= 360px) { .fm { height: 75%; } @@ -81,8 +114,8 @@ } /* iphone 6 landscape */ -@media only screen and (min-device-width: 375px) and (max-device-width: 667px) and (orientation: landscape), - @media only screen and (max-height: 360px) and (max-width: 640px) { +@media only screen and (device-width >= 375px) and (device-width <= 667px) and (orientation: landscape), + only screen and (height <= 360px) and (width <= 640px) { .fm { height: 55%; } @@ -92,14 +125,14 @@ } } -@media only screen and (max-width: 600px) { +@media only screen and (width <= 600px) { .panel { font-size: 26px; } /* текущий файл под курсором */ .current-file { - background-color: rgba(49, 123, 249, .40); + background-color: var(--border-color); color: white; } /* делаем иконки под курсом белыми */ @@ -107,25 +140,30 @@ color: white; } - .text-file::before { - color: rgba(26, 224, 124, 0.56); + .file::before { + color: rgb(26 224 124 / 56%); content: '\e80d'; } - .current-file .text-file::before { - color: white; + .file-link::before { + color: rgb(26 224 124 / 56%); + content: '\e81d'; } /* меняем иконки на шрифтовые */ .mini-icon { - color : rgba(246, 224, 124, 0.56); - font : 16px 'Fontello'; + color: rgb(246 224 124 / 56%); + font: 16px 'Fontello'; background-image: none; - padding : 1%; + padding: 1%; } - .size, .date, .owner, .mode { - display: none; + .size, + .date, + .owner, + .time, + .mode { + display: none !important; } .name { @@ -137,10 +175,26 @@ content: '\e807'; } - .text-file { + .directory-link::before { + content: '\e81e'; + } + + .file, + .file-link { background-image: none; } + .archive, + .archive-link { + background-image: none; + } + + .archive::before, + .archive-link { + color: rgb(26 224 124 / 56%); + content: '\e81d'; + } + /* убираем заголовок */ .fm-header { display: none; @@ -152,24 +206,26 @@ } } -@media only screen and (min-width: 601px) and (max-width: 785px) { +@media only screen and (width >= 601px) and (width <= 785px) { .cmd-button { width: 13%; } } -@media only screen and (min-width: 786px) and (max-width: 1155px) { +@media only screen and (width >= 786px) and (width <= 1155px) { .cmd-button { width: 10%; } } -@media only screen and (max-width: 1155px) { +@media only screen and (width <= 1155px) { .panel { width: 98%; } /* если правая панель не помещаеться - прячем её */ - .panel-right, .cmd-button#f5, .cmd-button#f6 { + .panel-right, + .cmd-button#f5, + .cmd-button#f6 { display: none; } } @@ -182,7 +238,8 @@ border: none; } - .keyspanel, .panel-right { + .keyspanel, + .panel-right { display: none; } diff --git a/css/style.css b/css/style.css index 9e762a37..10820444 100644 --- a/css/style.css +++ b/css/style.css @@ -1,17 +1,19 @@ html { - height : 94%; + height: 94%; } body { - width : 100%; - height : 95%; - overflow : hidden; - background-color : white; + width: 100%; + height: 95%; + overflow: hidden; + background-color: var(--background); } -body, pre, code { - font-family : 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; - font-size : 16px; +body, +pre, +code { + font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 16px; } .hidden { @@ -22,31 +24,34 @@ body, pre, code { display: none; } -.fm, .keyspanel { - cursor : default; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-user-select : none; - -moz-user-select : none; - -ms-user-select : none; - -o-user-select : none; - user-select : none; +.fm, +.keyspanel { + cursor: default; + -webkit-tap-highlight-color: rgb(0 0 0 / 0%); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; } .links { - -webkit-user-select : initial; - -moz-user-select : initial; - -ms-user-select : initial; - -o-user-select : initial; - user-select : text; + -webkit-user-select: initial; + -moz-user-select: initial; + -ms-user-select: initial; + -o-user-select: initial; + user-select: text; + color: var(--column-color); } -.panel, .cmd-button { +.panel, +.cmd-button { border: 1px solid; - border-color: rgba(49, 123, 249, 0.4); + border-color: var(--border-color); } .icon { - margin-left : 0.5%; + margin-left: 0.5%; cursor: default; } @@ -56,7 +61,7 @@ body, pre, code { .path-icon { position: relative; - color: #222; + color: var(--icon-color); } .path-icon:active { @@ -64,41 +69,41 @@ body, pre, code { } .path-icon:hover { - color : #06e; + color: #06e; cursor: pointer; } .error::before { - font-family : 'Fontello'; - font-size : 14px; - color : rgb(222, 41, 41); - cursor : default; - content : '\e800'; + font-family: 'Fontello'; + font-size: 14px; + color: rgb(222 41 41); + cursor: default; + content: '\e800'; } .loading { - position : relative; - display : inline-block; - width : 16px; - height : 16px; - vertical-align : middle; + position: relative; + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; } .loading::after { - position : relative; - bottom : 5px; - left : 16px; - font-size : 10px; - color : rgb(49, 123, 249); - content : attr(data-progress); + position: relative; + bottom: 5px; + left: 16px; + font-size: 10px; + color: var(--link-color); + content: attr(data-progress); } .cmd-button { width: 5%; height: 30px; margin: 20px 2px 0; - color: #222; - background-color: white; + color: var(--icon-color); + background-color: var(--button-background); transition: ease 0.1s; } @@ -109,7 +114,7 @@ body, pre, code { .cmd-button:active { color: white; - background-color: rgb(49, 123, 249); + background-color: var(--link-color); transition: ease 0.1s; } @@ -121,7 +126,8 @@ a { text-decoration: none; } -a:hover, a:active { +a:hover, +a:active { color: #06e; text-decoration: none; } @@ -145,13 +151,14 @@ a:hover, a:active { } .fm { - width : 98%; + width: 98%; height: 90%; margin: 26px auto 0; } .fm-header { font-weight: bold; + color: var(--column-color); } .panel-left { @@ -159,15 +166,30 @@ a:hover, a:active { } .current-file { - box-shadow: 0 0 0 1px rgba(49, 123, 249, 0.4) inset; + box-shadow: 0 0 0 1px var(--border-color) inset; } .cut-file { opacity: 0.7; } -.selected-file, .selected-file .name > a { - color: rgb(254, 159, 224); +.name { + float: left; + width: 26%; +} + +.name a { + color: var(--link-color); +} + +.name a:hover { + cursor: default; +} + +.selected-file, +.selected-file > span, +.selected-file .name > a { + color: rgb(254 159 224); } .panel-right { @@ -185,54 +207,55 @@ a:hover, a:active { } .selected-panel { - border-color: rgba(254, 159, 224, .40); + border-color: rgb(254 159 224 / 40%); } .keyspanel { text-align: center; } -.name { - float: left; - width: 26%; -} - -.name a:hover { - cursor: default; -} - .size { float: left; width: 12%; margin-right: 27px; text-align: right; + color: var(--column-color); } .date { float: left; width: 19%; + color: var(--column-color); +} + +.time { + color: var(--column-color); + display: none; } .owner { - display : inline-block; - width : 13%; + display: inline-block; + width: 12%; + /* when inline-block * vertical align should be * set top to prevent additional * spaces behind lines */ - vertical-align : top; + vertical-align: top; + color: var(--column-color); } .mode { float: right; - width: 18%; + width: 22%; + color: var(--column-color); } .reduce-text { - overflow : hidden; - text-overflow : ellipsis; - white-space : nowrap; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .files { @@ -247,4 +270,3 @@ a:hover, a:active { .files li { overflow: hidden; } - diff --git a/css/supports.css b/css/supports.css index bf470c2d..ea2084d2 100644 --- a/css/supports.css +++ b/css/supports.css @@ -1,6 +1,6 @@ @supports (overflow: overlay) { .files { - overflow-y: overlay; + overflow-y: auto; } .fm-header { diff --git a/css/terminal.css b/css/terminal.css index 20cf94b4..e7b1fea6 100644 --- a/css/terminal.css +++ b/css/terminal.css @@ -1,4 +1,3 @@ .terminal { height: 100%; } - diff --git a/css/themes/dark.css b/css/themes/dark.css new file mode 100644 index 00000000..311a8093 --- /dev/null +++ b/css/themes/dark.css @@ -0,0 +1,49 @@ +:root { + --link-color: #317bf9; + --border-color: rgb(49 123 249 / 40%); + --background: #22272e; + --column-color: #727e8c; + --icon-color: #478be6; + --button-background: #22272e; + --internal-background: #373e47; +} + +.view { + background: var(--internal-background) !important; + color: var(--column-color) !important; +} +.view a { + color: var(--link-color) !important; +} + +.smalltalk .page, +.smalltalk header, +.smalltalk .button-strip button, +.smalltalk input { + background: var(--internal-background) !important; + color: var(--link-color) !important; + text-shadow: none !important; +} + +.cloudcmd-user-menu, +.cloudcmd-user-menu-button { + background: var(--internal-background) !important; + color: var(--link-color) !important; +} + +.jqconsole { + background: #373e47 !important; +} + +.jqconsole-prompt { + color: var(--column-color) !important; +} + +.log-msg { + color: var(--column-color) !important; +} + +.menu { + color: var(--link-color) !important; + background: var(--internal-background) !important; +} diff --git a/css/themes/light.css b/css/themes/light.css new file mode 100644 index 00000000..e95eb441 --- /dev/null +++ b/css/themes/light.css @@ -0,0 +1,9 @@ +:root { + --link-color: blue; + --selected-menu-item-color: #317bf9; + --border-color: rgb(49 123 249 / 40%); + --background: white; + --column-color: black; + --icon-color: #222; + --button-background: white; +} diff --git a/css/urls.css b/css/urls.css index e071784c..be90e235 100644 --- a/css/urls.css +++ b/css/urls.css @@ -17,28 +17,47 @@ } @font-face { - font-family : 'Droid Sans Mono'; - src : url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot); - src : + font-family: 'Droid Sans Mono'; + src: url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot); + src: local('Droid Sans Mono'), local('DroidSansMono'), url(../font/DroidSansMono.eot) format('embedded-opentype'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix) format('embedded-opentype'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot) format('embedded-opentype'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot?#iefix) + format('embedded-opentype'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJTwtzT4qNq-faudv5qbO9-U.eot) + format('embedded-opentype'), url(../font/DroidSansMono.woff2) format('woff2'), url(../font/DroidSansMono.woff) format('woff'), - url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff) format('woff'), + url(https://themes.googleusercontent.com/static/fonts/droidsansmono/v5/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff) + format('woff'), local('Consolas'); - font-style : normal; - font-weight : 400; + font-style: normal; + font-weight: 400; } .directory { - background-image: url(../img/dir.png); + background-image: url(../img/directory.png); } -.text-file { - background-image: url(../img/txt.png); +.directory-link { + background-image: url(../img/directory-link.png); +} + +.file { + background-image: url(../img/file.png); +} + +.file-link { + background-image: url(../img/file-link.png); +} + +.archive { + background-image: url(../img/archive.png); +} + +.archive-link { + background-image: url(../img/archive-link.png); } .loading-svg { @@ -48,4 +67,3 @@ .loading-gif { background: url(../img/spinner.gif); } - diff --git a/css/user-menu.css b/css/user-menu.css new file mode 100644 index 00000000..792747f4 --- /dev/null +++ b/css/user-menu.css @@ -0,0 +1,23 @@ +.cloudcmd-user-menu { + font-size: 16px; + font-family: 'Droid Sans Mono', 'Ubuntu Mono', 'Consolas', monospace; + border: 0; +} + +.cloudcmd-user-menu:focus { + outline: 0; +} + +.cloudcmd-user-menu > option:checked { + box-shadow: 20px -20px 0 2px var(--selected-menu-item-color) inset; +} + +.cloudcmd-user-menu-button { + display: block; + width: 100%; + font-size: 16px; + padding: 2px; + -webkit-appearance: none; + border: 0; + overflow: auto; +} diff --git a/css/view.css b/css/view.css index 381075d4..b6fb20ed 100644 --- a/css/view.css +++ b/css/view.css @@ -2,6 +2,7 @@ font-size: 16px; white-space: pre; height: 100%; + overflow: auto; } .view:focus { @@ -14,23 +15,12 @@ } .view-overlay { - display : block; - background : rgba(255, 255, 255, 0.1); + display: block; + background: rgb(255 255 255 / 10%); } -.fancybox-wrap { - /* - * when search element with - * document.elementFromPoint - */ - z-index: 1; -} - -.fancybox-overlay { - z-index: 0; -} - -.media, video { +.media, +video { width: 100%; } diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..64c1bde2 --- /dev/null +++ b/deno.json @@ -0,0 +1,14 @@ +{ + "tasks": { + "start": "deno run -P=cloudcmd bin/cloudcmd.mjs" + }, + "permissions": { + "cloudcmd": { + "env": true, + "read": true, + "sys": true, + "net": true, + "run": true + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index e358cb83..0fb72a34 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '2' +version: "2" services: web: ports: @@ -7,4 +7,3 @@ services: - ~:/root - /:/mnt/fs image: coderaiser/cloudcmd - diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..7c5d7b64 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,29 @@ +FROM node + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/src/cloudcmd + +WORKDIR /usr/src/cloudcmd + +COPY package.json /usr/src/cloudcmd/ + +RUN curl -fsSL https://bun.com/install | bash && \ + ln -s /root/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm + +COPY . /usr/src/cloudcmd + +WORKDIR / + +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine new file mode 100644 index 00000000..5e38ab8a --- /dev/null +++ b/docker/Dockerfile.alpine @@ -0,0 +1,34 @@ +FROM node:alpine + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/src/cloudcmd + +WORKDIR /usr/src/cloudcmd + +COPY package.json /usr/src/cloudcmd/ + +RUN apk update && \ + apk add --no-cache curl bash make g++ python3 && \ + curl -fsSL https://bun.sh/install | bash && \ + ln -s ~/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm && \ + apk del make g++ python3 && \ + rm -rf /usr/include /tmp/* /var/cache/apk/* + +COPY . /usr/src/cloudcmd + +WORKDIR / + +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false +ENV cloudcmd_vim=true + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.io b/docker/Dockerfile.io new file mode 100644 index 00000000..d5a24459 --- /dev/null +++ b/docker/Dockerfile.io @@ -0,0 +1,113 @@ +FROM ubuntu:resolute + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +RUN mkdir -p /usr/local/share/cloudcmd + +WORKDIR /usr/local/share/cloudcmd + +COPY package.json /usr/local/share/cloudcmd/ + +ENV DEBIAN_FRONTEND=noninteractive \ + NVM_DIR=/usr/local/share/nvm \ + npm_config_cache=/tmp/npm-cache \ + GOPATH=/usr/local/share/go \ + PATH=/usr/local/share/bun/bin:$PATH \ + BUN_INSTALL=/usr/local/share/bun \ + NPM_CONFIG_CACHE=/tmp/.npm \ + NPM_CONFIG_PREFIX=/usr/local \ + NPM_CONFIG_PACKAGE_LOCK=false \ + PALABRA_DIR=/usr/local/share \ + XDG_CONFIG_HOME=/usr/local/etc + +ARG UBUNTU_DEPS="libatomic1 curl wget git net-tools iproute2 software-properties-common" +ARG RUST_DEPS="build-essential" +ARG DEPS="pv gcc gdb strace upx-ucl less ffmpeg net-tools netcat-openbsd mc far2l iputils-ping vim bat fzf locales sudo command-not-found ncdu aptitude htop btop hexyl tmux" +ARG PALABRA_DEPS="nvm node rust go deno fasm nvchad rizin yara gdu f4 typos shellcheck gh" +ARG BUN_DEPS="palabra wisdom nupdate version-io redrun superc8 supertape madrun redlint putout renamify-cli runny redfork cline" + +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get autoremove && \ + apt-get install -y ${UBUNTU_DEPS} ${RUST_DEPES} ${DEPS} && \ + echo "> Install git" && \ + add-apt-repository ppa:git-core/ppa -y && \ + echo "> Update command-not-found database. Run 'sudo apt update' to populate it." && \ + apt-get update && \ + apt-get upgrade -y && \ + apt-get autoremove && \ + apt-get clean && \ + echo "> create user" && \ + useradd -m -s /bin/bash -u 1337 instalador && \ + chown -R instalador /usr/local && \ + chown -R instalador /tmp + +USER instalador + +RUN echo "> install bun" && \ + curl https://bun.sh/install | bash && \ + echo "> install npm globals" && \ + bun i ${BUN_DEPS} -g && \ + echo "> install rust go deno bun fasm nvim" && \ + bun ${BUN_INSTALL}/bin/palabra i ${PALABRA_DEPS} && \ + echo "> install node" && \ + . $NVM_DIR/nvm.sh + +USER root + +RUN echo "> remove user" && \ + userdel -r instalador && \ + echo "> install gritty" && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + bun pm cache rm && \ + echo "> setup cloudcmd" && \ + ln -s /usr/local/share/cloudcmd/bin/cloudcmd.js /usr/local/bin/cloudcmd && \ + echo "> setup git" && \ + git config --global core.whitespace -trailing-space && \ + git config --global pull.rebase true && \ + git config --global init.defaultBranch master && \ + echo "> configure bash" && \ + echo "alias ls='ls --color=auto'" >> /etc/bash.bashrc && \ + echo "alias buni='bun i --no-save'" >> /etc/bash.bashrc && \ + echo "alias bat='batcat'" >> /etc/bash.bashrc && \ + echo ". /usr/local/share/nvm/nvm.sh" >> /etc/bash.bashrc && \ + echo ". /usr/share/bash-completion/completions/git" >> /etc/bash.bashrc && \ + echo 'PS1="\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ "' >> /etc/bash.bashrc && \ + echo "> setup inputrc" && \ + echo "set editing-mode vi" >> /etc/inputrc && \ + echo "TAB: menu-complete" >> /etc/inputrc && \ + echo "set UTF-8" && \ + echo " > configure languages" && \ + echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ + echo "ru_RU.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "uk_UA.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "es_ES.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen && \ + echo "el_GR.UTF-8 UTF-8" >> /etc/locale.gen && \ + locale-gen + +COPY . /usr/local/share/cloudcmd + +WORKDIR / + +ENV cloudcmd_terminal=true \ + cloudcmd_terminal_path=gritty \ + cloudcmd_vim=true \ + cloudcmd_open=false \ + PATH=node_modules/.bin:$PATH \ + PATH=~/.local/bin:$PATH \ + BUN_INSTALL_CACHE_DIR=/tmp/bun-cache \ + DENO_DIR=/tmp/deno-cache \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ + TERM=xterm-256color \ + XDG_CACHE_HOME=/tmp \ + XDG_DATA_HOME=/usr/local/share \ + XDG_CONFIG_HOME=~/.config + +EXPOSE 8000 + +ENTRYPOINT ["/usr/local/share/cloudcmd/bin/cloudcmd.js"] diff --git a/docker/Dockerfile.slim b/docker/Dockerfile.slim new file mode 100644 index 00000000..c6f273b8 --- /dev/null +++ b/docker/Dockerfile.slim @@ -0,0 +1,39 @@ +FROM node:slim AS build + +RUN mkdir -p /usr/src/cloudcmd/ + +WORKDIR /usr/src/cloudcmd + +COPY package.json /usr/src/cloudcmd/ + +RUN apt-get update && \ + apt-get install -y build-essential python3 libncurses5-dev pkg-config && \ + apt-get install -y --no-install-recommends curl ca-certificates unzip && \ + curl -fsSL https://bun.sh/install | bash && \ + ln -s ~/.bun/bin/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + bun r gritty --omit dev && \ + bun i gritty --omit dev && \ + ~/.bun/bin/bun pm cache rm && \ + rm -rf /var/lib/apt/lists/* + +COPY . /usr/src/cloudcmd + +FROM node:slim AS runtime + +COPY --from=build /usr/src/cloudcmd /usr/src/cloudcmd +COPY --from=build /root/.bun /root/.bun + +LABEL maintainer="Coderaiser" +LABEL org.opencontainers.image.source="https://github.com/coderaiser/cloudcmd" + +WORKDIR / + +ENV cloudcmd_terminal=true +ENV cloudcmd_terminal_path=gritty +ENV cloudcmd_open=false +ENV PATH="/root/.bun/bin:$PATH" + +EXPOSE 8000 + +ENTRYPOINT ["/usr/src/cloudcmd/bin/cloudcmd.js"] diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..22add654 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,31 @@ +import {safeAlign} from 'eslint-plugin-putout'; +import {defineConfig} from 'eslint/config'; +import globals from 'globals'; +import {matchToFlat} from '@putout/eslint-flat'; + +export const match = { + 'bin/release.js': { + 'no-console': 'off', + 'n/hashbang': 'off', + }, + 'client/dom/index.*': { + 'no-multi-spaces': 'off', + }, + 'client/**': { + 'n/no-unsupported-features/node-builtins': 'off', + }, +}; +export default defineConfig([ + safeAlign, { + ignores: ['**/fixture'], + rules: { + 'key-spacing': 'off', + }, + }, { + files: ['{client,common,static}/**/*.js'], + languageOptions: { + globals: globals.browser, + }, + }, + ...matchToFlat(match), +]); diff --git a/font/fontello.eot b/font/fontello.eot index 4274ddd2..e144bd82 100644 Binary files a/font/fontello.eot and b/font/fontello.eot differ diff --git a/font/fontello.json b/font/fontello.json index 61c8d00e..c8bad6be 100644 --- a/font/fontello.json +++ b/font/fontello.json @@ -174,6 +174,30 @@ "code": 59413, "src": "entypo" }, + { + "uid": "f805bb95d40c7ef2bc51b3d50d4f2e5c", + "css": "th-list", + "code": 59420, + "src": "fontawesome" + }, + { + "uid": "e15f0d620a7897e2035c18c80142f6d9", + "css": "link-ext", + "code": 59421, + "src": "fontawesome" + }, + { + "uid": "e35de5ea31cd56970498e33efbcb8e36", + "css": "link-ext-alt", + "code": 59422, + "src": "fontawesome" + }, + { + "uid": "12f4ece88e46abd864e40b35e05b11cd", + "css": "ok", + "code": 59423, + "src": "fontawesome" + }, { "uid": "60617c8adc1e7eb3c444a5491dd13f57", "css": "attention-circled-1", diff --git a/font/fontello.svg b/font/fontello.svg index afc5a844..ff664681 100644 --- a/font/fontello.svg +++ b/font/fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2018 by original authors @ fontello.com +Copyright (C) 2024 by original authors @ fontello.com @@ -61,6 +61,14 @@ + + + + + + + + - \ No newline at end of file + diff --git a/font/fontello.ttf b/font/fontello.ttf index 1065b0d3..bfec5c43 100644 Binary files a/font/fontello.ttf and b/font/fontello.ttf differ diff --git a/font/fontello.woff b/font/fontello.woff index 9a4a5952..a23a854d 100644 Binary files a/font/fontello.woff and b/font/fontello.woff differ diff --git a/font/fontello.woff2 b/font/fontello.woff2 index b8c98b96..f0210ad6 100644 Binary files a/font/fontello.woff2 and b/font/fontello.woff2 differ diff --git a/html/index.html b/html/index.html index 0125c7c3..d366ed1c 100644 --- a/html/index.html +++ b/html/index.html @@ -8,19 +8,22 @@ - + + -
    {{ fm }}
    +
    {{ fm }}
    - + @@ -38,5 +41,15 @@ + diff --git a/img/archive-link.png b/img/archive-link.png new file mode 100644 index 00000000..b134c2b8 Binary files /dev/null and b/img/archive-link.png differ diff --git a/img/archive.png b/img/archive.png new file mode 100644 index 00000000..fd4bbccd Binary files /dev/null and b/img/archive.png differ diff --git a/img/directory-link.png b/img/directory-link.png new file mode 100644 index 00000000..a28afa79 Binary files /dev/null and b/img/directory-link.png differ diff --git a/img/dir.png b/img/directory.png similarity index 100% rename from img/dir.png rename to img/directory.png diff --git a/img/favicon/favicon-256.png b/img/favicon/favicon-256.png new file mode 100644 index 00000000..4e01d43c Binary files /dev/null and b/img/favicon/favicon-256.png differ diff --git a/img/file-link.png b/img/file-link.png new file mode 100644 index 00000000..0667fa5e Binary files /dev/null and b/img/file-link.png differ diff --git a/img/txt.png b/img/file.png similarity index 100% rename from img/txt.png rename to img/file.png diff --git a/img/logo/cloudcmd-hq.png b/img/logo/cloudcmd-hq.png new file mode 100644 index 00000000..8e4d1d3b Binary files /dev/null and b/img/logo/cloudcmd-hq.png differ diff --git a/img/screen/config.png b/img/screen/config.png index 755516ca..deb4ed8f 100644 Binary files a/img/screen/config.png and b/img/screen/config.png differ diff --git a/img/screen/console.png b/img/screen/console.png index f3e6a127..665d6668 100644 Binary files a/img/screen/console.png and b/img/screen/console.png differ diff --git a/img/screen/edit.png b/img/screen/edit.png index 0735d2e8..e61c148d 100644 Binary files a/img/screen/edit.png and b/img/screen/edit.png differ diff --git a/img/screen/menu.png b/img/screen/menu.png index 9de75577..9c4ef201 100644 Binary files a/img/screen/menu.png and b/img/screen/menu.png differ diff --git a/img/screen/one-file-panel.png b/img/screen/one-file-panel.png index 9b9648a9..e95fd4bc 100644 Binary files a/img/screen/one-file-panel.png and b/img/screen/one-file-panel.png differ diff --git a/img/screen/terminal.png b/img/screen/terminal.png index 18c88364..d38ab47f 100644 Binary files a/img/screen/terminal.png and b/img/screen/terminal.png differ diff --git a/img/screen/view.png b/img/screen/view.png index d6a81a16..98e3e7e4 100644 Binary files a/img/screen/view.png and b/img/screen/view.png differ diff --git a/json/config.json b/json/config.json index ddbb477b..3bbb65f4 100644 --- a/json/config.json +++ b/json/config.json @@ -5,30 +5,46 @@ "password": "2b64f2e3f9fee1942af9ff60d40aa5a719db33b8ba8dd4864bb4f11e25ca2bee00907de32a59429602336cac832c8f2eeff5177cc14c864dd116c8bf6ca5d9a9", "algo": "sha512WithRSAEncryption", "editor": "edward", + "menu": "aleman", "packer": "tar", "diff": true, - "zip" : true, + "zip": true, "buffer": true, "dirStorage": false, - "online": true, - "open": false, + "online": false, + "open": true, "keysPanel": true, "port": 8000, "ip": null, "root": "/", "prefix": "", - "progress": true, + "prefixSocket": "", "contact": true, "confirmCopy": true, "confirmMove": true, "configDialog": true, + "configAuth": true, + "configPort": true, "oneFilePanel": false, "console": true, "syncConsolePath": false, "terminal": false, "terminalPath": "", + "terminalCommand": "", + "terminalAutoRestart": true, + "showDotFiles": true, "showConfig": false, + "showFileName": false, "vim": false, - "columns": "name-size-date-owner-mode" + "columns": "name-size-date-owner-mode", + "theme": "light", + "export": false, + "exportToken": "root", + "import": false, + "importToken": "root", + "importUrl": "http://localhost:8000", + "importListen": false, + "log": true, + "dropbox": false, + "dropboxToken": "" } - diff --git a/json/help.json b/json/help.json index 12d15938..44f79248 100644 --- a/json/help.json +++ b/json/help.json @@ -8,42 +8,70 @@ "-p, --password ": "set password", "-c, --config ": "configuration file path", "--show-config ": "show config values", + "--show-dot-files ": "show dot files", + "--show-file-name ": "show file name in view and edit", "--editor ": "set editor: \"dword\", \"edward\" or \"deepword\"", "--packer ": "set packer: \"tar\" or \"zip\"", "--root ": "set root directory", "--prefix ": "set url prefix", + "--prefix-socket ": "set prefix for socket connection", "--port ": "set port number", - "--progress ": "show progress of file operations", "--confirm-copy ": "confirm copy", "--confirm-move ": "confirm move", "--open ": "open web browser when server started", "--name ": "set tab name in web browser", + "--menu ": "set menu: \"supermenu\" or \"aleman\"", "--one-file-panel ": "show one file panel", "--keys-panel ": "show keys panel", "--config-dialog ": "enable config dialog", + "--config-auth ": "enable auth change in config dialog", + "--config-port ": "enable port change in config dialog", "--console ": "enable console", "--sync-console-path ": "sync console path", "--contact ": "enable contact", "--terminal ": "enable terminal", "--terminal-path ": "set terminal path", + "--terminal-command ": "set command to run in terminal (shell by default)", + "--terminal-auto-restart ": "restart command on exit", "--vim ": "enable vim hot keys", "--columns ": "set visible columns", + "--theme ": "set theme 'light' or 'dark'", + "--export ": "enable export of config through a server", + "--export-token ": "authorization token used by export server", + "--import ": "enable import of config", + "--import-url ": "url of an export server", + "--import-token ": "authorization token used to connect to export server", + "--import-listen ": "enable listen on config updates from import server", + "--dropbox ": "enable dropbox integration", + "--dropbox-token ": "set dropbox token", + "--log ": "enable logging", + "--no-show-config ": "do not show config values", "--no-server ": "do not start server", "--no-auth ": "disable authorization", "--no-online ": "load scripts from local server", "--no-open ": "do not open web browser when server started", "--no-name ": "set default tab name in web browser", - "--no-one-panel ": "show two file panels", - "--no-keys-panel ": "hide keys panel", "--no-one-file-panel ": "show two file panels", - "--no-progress ": "do not show progress of file operations", + "--no-keys-panel ": "hide keys panel", "--no-confirm-copy ": "do not confirm copy", "--no-confirm-move ": "do not confirm move", "--no-config-dialog ": "disable config dialog", + "--no-config-auth ": "disable auth change in config dialog", + "--no-config-port ": "disable port change in config dialog", "--no-console ": "disable console", "--no-sync-console-path ": "do not sync console path", "--no-contact ": "disable contact", "--no-terminal ": "disable terminal", + "--no-terminal-command ": "set default shell to run in terminal", + "--no-terminal-auto-restart ": "do not restart command on exit", "--no-vim ": "disable vim hot keys", - "--no-columns ": "set default visible columns" + "--no-columns ": "set default visible columns", + "--no-export ": "disable export config through a server", + "--no-import ": "disable import of config", + "--no-import-listen ": "disable listen on config updates from import server", + "--no-show-dot-files ": "do not show dot files", + "--no-show-file-name ": "do not show file name in view and edit", + "--no-dropbox ": "disable dropbox integration", + "--no-dropbox-token ": "unset dropbox token", + "--no-log ": "disable logging" } diff --git a/json/modules.json b/json/modules.json index af621034..6ed586d1 100644 --- a/json/modules.json +++ b/json/modules.json @@ -11,33 +11,20 @@ "markdown", "config", "contact", + "command-line", "upload", "operation", "konsole", "terminal", - "cloud" + "terminal-run", + "cloud", + "user-menu" ], "remote": [{ - "name": "jquery", - "version": "3.3.1", - "local": "/modules/jquery/dist/jquery.min.js", - "remote": "https://code.jquery.com/jquery-{{ version }}.min.js" - }, { "name": "socket", - "version": "2.1.1", + "version": "4.0.1", "local": "/socket.io/socket.io.js", "remote": "https://cdnjs.cloudflare.com/ajax/libs/socket.io/{{ version }}/socket.io.js" - }, { - "name": "fancybox", - "version": "2.1.6", - "local": [ - "/modules/fancybox/source/jquery.fancybox.css", - "/modules/fancybox/source/jquery.fancybox.pack.js" - ], - "remote": [ - "//cdnjs.cloudflare.com/ajax/libs/fancybox/{{ version }}/css/jquery.fancybox.min.css", - "//cdnjs.cloudflare.com/ajax/libs/fancybox/{{ version }}/js/jquery.fancybox.min.js" - ] }], "data": { "FilePicker": { diff --git a/man/cloudcmd.1 b/man/cloudcmd.1 index c3c027f6..dab36a2c 100644 --- a/man/cloudcmd.1 +++ b/man/cloudcmd.1 @@ -31,12 +31,15 @@ programs in browser from any computer, mobile or tablet device. -p, --password set password -c, --config configuration file path --show-config show config values - --editor set editor: "dword", "edward" or "deepword" + --show-dot-files show dot files + --show-file-name show file name in view and edit modes + --editor set editor: "dword", "edward", "deepword" or "qword" + --menu set menu: "supermenu" or "aleman" --packer set packer: "tar" or "zip" --root set root directory --prefix set url prefix + --prefix-socket set prefix for socket connection --port set port number - --progress show progress of file operations --confirm-copy confirm copy --confirm-move confirm move --open open web browser when server started @@ -45,29 +48,56 @@ programs in browser from any computer, mobile or tablet device. --keys-panel show keys panel --contact enable contact --config-dialog enable config dialog + --config-auth enable auth change in config dialog + --config-port enable port change in config dialog --console enable console --sync-console-path sync console path --terminal enable terminal --terminal-path set terminal path + --terminal-command set command to run in terminal (shell by default) + --terminal-auto-restart restart command on exit --vim enable vim hot keys --columns set visible columns - --no-auth disable authorization + --theme set theme 'light' or 'dark' + --export enable export of config through a server + --export-token authorization token used by export server + --import enable import of config + --import-url url of an import server + --import-token authorization token used to connect to export server + --import-listen enable listen on config updates from import server + --dropbox enable dropbox integration + --dropbox-token set dropbox token + --log enable logging + --no-show-config do not show config values + --no-show-dot-files do not show dot files --no-server do not start server + --no-auth disable authorization --no-online load scripts from local server --no-open do not open web browser when server started --no-name set default tab name in web browser --no-one-file-panel show two file panels --no-keys-panel hide keys panel - --no-progress do not show progress of file operations --no-confirm-copy do not confirm copy --no-confirm-move do not confirm move --no-contact disable contact --no-config-dialog disable config dialog + --no-config-auth disable auth change in config dialog + --no-config-port disable port change in config dialog --no-console disable console --no-sync-console-path do not sync console path --no-terminal disable terminal + --no-terminal-command set default shell to run in terminal + --no-terminal-auto-restart do not restart command on exit --no-vim disable vim hot keys --no-columns set visible default columns + --no-export disable export of config through a server + --no-import disable import of config + --no-import-url url of an import server + --no-import-listen disable listen on config updates from import server + --no-show-file-name do not show file name in view and edit modes + --no-dropbox disable dropbox integration + --no-dropbox-token unset dropbox token + --no-log disable logging .SH RESOURCES AND DOCUMENTATION diff --git a/manifest.yml b/manifest.yml index 97b2810f..c10c360d 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,4 +1,3 @@ ---- applications: .: name: cloudcmd @@ -7,7 +6,7 @@ applications: info: mem: 512M description: Node.js Application - exec: + exec: null url: ${name}.${target-base} mem: 128M instances: 2 diff --git a/modules/fancybox/.bower.json b/modules/fancybox/.bower.json deleted file mode 100644 index 6ce4112c..00000000 --- a/modules/fancybox/.bower.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "fancybox", - "main": [ - "source/jquery.fancybox.css", - "source/jquery.fancybox.js", - "source/blank.gif", - "source/fancybox_loading.gif", - "source/fancybox_loading@2x.gif", - "source/fancybox_overlay.png", - "source/fancybox_sprite.png", - "source/fancybox_sprite@2x.png", - "source/jquery.fancybox.pack.js" - ], - "ignore": [ - "**/.*", - "fancybox.jquery.json", - "demo" - ], - "dependencies": { - "jquery": ">=1.10", - "jquery-mousewheel": "~3.1.3" - }, - "homepage": "https://github.com/fancyapps/fancyBox", - "version": "2.1.6", - "_release": "2.1.6", - "_resolution": { - "type": "version", - "tag": "v2.1.6", - "commit": "c00852a5394ef18517598bfa3b9e885cdf5c7450" - }, - "_source": "https://github.com/fancyapps/fancyBox.git", - "_target": "^2.1.6", - "_originalSource": "fancybox" -} \ No newline at end of file diff --git a/modules/fancybox/CHANGELOG.md b/modules/fancybox/CHANGELOG.md deleted file mode 100644 index b0d847e2..00000000 --- a/modules/fancybox/CHANGELOG.md +++ /dev/null @@ -1,125 +0,0 @@ -fancyBox - Changelog -========= - -### Version 2.1.5 - June 14, 2013 -* Fixed #493 - Broken slideshow -* Fixed #556 - Parent option -* Retina graphics (#564) and retina display support (#420) -* Improved "lock" feature - -### Version 2.1.4 - January 10, 2013 -* Update to be compatible with jQuery v1.9 -* Small changes that should fix usability issues for certain users - -### Version 2.1.3 - October 23, 2012 - -* Fixed #426 - Broken IE7 -* Fixed #423 - Background flickering on iOS -* Fixed #418 - Automatically Grow/Shrink and Center -* Updated the script to work with jQuery 1.6 -* Media helper supports YouTube video series - -### Version 2.1.2 - October 15, 2012 - -* Fixed #414 - Don't allow nextClick if there is only one item -* Fixed #397 - Button helper 'Menu' not visible in IE7 -* Overlay can be opened/closed manually: -* $.fancybox.helpers.overlay.open(); -* $.fancybox.helpers.overlay.open({closeClick : false}); -* $.fancybox.helpers.overlay.close(); -* Optimized for Internet Explorer 10 (Windows 8) - -### Version 2.1.1 - October 01, 2012 - -* Fixed #357 - Converting values like 'auto' in getScalar() -* Fixed #358 - Updated overlay background image -* New "fancybox-href" and "fancybox-title" HTML5 data-attributes (#317) -* Improved helpers: -* - now they can have a property 'defaults' that contains default settings -* - updated vimeo and youtube parsers for media helper -* Content locking now can be turned off - -### Version 2.1.0 - August 20, 2012 - -* Fixed #103 - DOM element re-injection after closing -* Fixed #188 - navigation keys inside editable content -* New animation directions (see https://github.com/fancyapps/fancyBox/issues/233#issuecomment-5512453) -* New option "iframe" - it is now possible to separate scrolling for iframe and wrapping element; choose to preload -* New option "swf" - brings back functionality from fancyBox v1 -* Improved media helper - better support for vimeo and youtube; links are now configurable -* Rewritten overlay helper: -* - new option "showEarly" - toggles if should be open before of after content is loaded -* - Facebook-style (https://github.com/fancyapps/fancyBox/issues/24) and therefore uses image for background -* Option "padding" accepts array (e.g., padding: [15, 50, 10, 5]) -* One of dimensions (width or height) can now be set to "auto" (option "autoSize" needs to be "false") -* Updated callbacks: -* - "beforeClose" is now called only once -* - "afterLoad" receives current and previous object as arguments -* Method "$.fancybox.update();" recalculates content width/height -* Updated to work with jQuery v1.8 - -### Version 2.0.6 - April 16, 2012 - -* Fixed #188 - keystrokes in contenteditable -* Fixed #171 - non-images should not be preloaded -* Fixed #158 - 'closeClick: true' breaks gallery navigation -* New "media" helper - detects and displays various media types -* New option "groupAttr" - name of group selector attribute, default is "data-fancybox-group" -* New feature - selector expressions in URLs, see #170 -* Improved 'overlay' helper to use "position: fixed" -* Improved autoSize, fixed wrong height in some cases -* Improved centering and iframe scrolling for iOS -* Updated markup, new element '.fancybox-skin' is now used for styling - -### Version 2.0.5 - February 21, 2012 - -* Fixed #155 - easing for prev/next animations -* Fixed #153 - overriding "keys" options -* Fixed #147 - IE7 problem with #hash links -* Fixed #130 - changing dynamically data-fancybox-group -* Fixed #126 - obey minWidth/minHeight -* Fixed #118 - placement of loading icon and navigation arrows -* Fixed #101 - "index" option not working -* Fixed #94 - "orig" option not working -* Fixed #80 - does not work on IE6 -* Fixed #72 - can't set overlay opacity to 0 -* Fixed #63 - properly set gallery index -* New option "autoCenter" - toggles centering on window resize or scroll, disabled for mobile devices by default -* New option "autoResize" - toggles responsivity, disabled for mobile devices by default -* New option "preload" - number of images to preload -* New feature to target mobile/desktop browsers using CSS, see #108 -* Changed ajax option defaults to "{ dataType: 'html', headers: { 'X-fancyBox': true } }", see #150 and #128 -* Updated loading icon for IE7, IE8 -* Calculates height of the iframe if 'autoSize' is set to 'true' and the iframe is on the same domain as the main page - -### Version 2.0.4 - December 12, 2011 - -* Fixed #47 - fix overriding properties -* New option "position" to thumbnail and button helpers - - -### Version 2.0.3 - November 29, 2011 - -* Fixed #29 - broken elastic transitions - - -### Version 2.0.2 - November 28, 2011 - -* Fixed slideshow -* Fixed scrollbars issue when displayed a very tall image -* New option "nextClick" - navigate to next gallery item when user clicks the content -* New option "modal" - to disable navigation and closing -* Add 'metadata' plugin support -* Add ability to create groups using 'data-fancybox-group' attribute -* Updated manual usage to match earlier releases - - -### Version 2.0.1 - November 23, 2011 - -* Fixed keyboard events inside form elements -* Fixed manual usage - - -### Version 2.0.0 - November 21, 2011 - -First release - completely rewritten, many new features and updated graphics. \ No newline at end of file diff --git a/modules/fancybox/README.md b/modules/fancybox/README.md deleted file mode 100644 index 08a91074..00000000 --- a/modules/fancybox/README.md +++ /dev/null @@ -1,244 +0,0 @@ -fancyBox -======== - -fancyBox is a tool that offers a nice and elegant way to add zooming functionality for images, html content and multi-media on your webpages. - -More information and examples: http://www.fancyapps.com/fancybox/ - -License: http://www.fancyapps.com/fancybox/#license - -Copyright (c) 2012 Janis Skarnelis - janis@fancyapps.com - - -How to use ----------- - -To get started, download the plugin, unzip it and copy files to your website/application directory. -Load files in the section of your HTML document. Make sure you also add the jQuery library. - -```html - - - - - -``` - -Create your links with a `title` (or `data-fancybox-title`) if you want a title to be shown, and add a class: - -```html - -``` - -If you have a set of related items that you would like to group, -additionally include a group name in the `rel` (or `data-fancybox-group`) attribute: - -```html - - -``` - -Initialise the script like this: - -```html - -``` - -May also be passed an optional options object which will extend the default values. Example: - -```html - -``` - -Tip: Automatically group and apply fancyBox to all images: - -```js -$("a[href$='.jpg'],a[href$='.jpeg'],a[href$='.png'],a[href$='.gif']").attr('rel', 'gallery').fancybox(); -``` - -Script uses the `href` attribute of the matched elements to obtain the location of the content and to figure out content type you want to display. -You can specify type directly by adding classname (fancybox.image, fancybox.iframe, etc) or `data-fancybox-type` attribute: - -```html -//Ajax: -Example -//or -Example - -//Iframe: -Example - -//Inline (will display an element with `id="example"`) -Example - -//SWF: -Example - -//Image: -Example -``` - -Note, ajax requests are subject to the [same origin policy](http://en.wikipedia.org/wiki/Same_origin_policy). -If fancyBox will not be able to get content type, it will try to guess based on 'href' and will quit silently if would not succeed. -(this is different from previsous versions where 'ajax' was used as default type or an error message was displayed). - -Advanced --------- - -### Helpers - -Helpers provide a simple mechanism to extend the capabilities of fancyBox. There are two built-in helpers - 'overlay' and 'title'. -You can disable them, set custom options or enable other helpers. Examples: - -```js -//Disable title helper -$(".fancybox").fancybox({ - helpers: { - title: null - } -}); - -//Disable overlay helper -$(".fancybox").fancybox({ - helpers: { - overlay : null - } -}); - -//Change title position and overlay color -$(".fancybox").fancybox({ - helpers: { - title : { - type : 'inside' - }, - overlay : { - css : { - 'background' : 'rgba(255,255,255,0.5)' - } - } - } -}); - -//Enable thumbnail helper and set custom options -$(".fancybox").fancybox({ - helpers: { - thumbs : { - width: 50, - height: 50 - } - } -}); -``` - -### API - -Also available are event driven callback methods. The `this` keyword refers to the current or upcoming object (depends on callback method). Here is how you can change title: - -```js -$(".fancybox").fancybox({ - beforeLoad : function() { - this.title = 'Image ' + (this.index + 1) + ' of ' + this.group.length + (this.title ? ' - ' + this.title : ''); - - /* - "this.element" refers to current element, so you can, for example, use the "alt" attribute of the image to store the title: - this.title = $(this.element).find('img').attr('alt'); - */ - } -}); -``` - -It`s possible to open fancyBox programmatically in various ways: - -```js -//HTML content: -$.fancybox( '

    Lorem Lipsum

    Lorem lipsum

    ', { - title : 'Custom Title' -}); - -//DOM element: -$.fancybox( $("#inline"), { - title : 'Custom Title' -}); - -//Custom object: -$.fancybox({ - href: 'example.jpg', - title : 'Custom Title' -}); - -//Array of objects: -$.fancybox([ - { - href: 'example1.jpg', - title : 'Custom Title 1' - }, - { - href: 'example2.jpg', - title : 'Custom Title 2' - } -], { - padding: 0 -}); -``` - -There are several methods that allow you to interact with and manipulate fancyBox, example: - -```js -//Close fancybox: -$.fancybox.close(); -``` - -There is a simply way to access wrapping elements using JS: - -```js -$.fancybox.wrap -$.fancybox.skin -$.fancybox.outer -$.fancybox.inner -``` - -You can override CSS to customize the look. For example, make navigation arrows always visible, -change width and move them outside of area (use this snippet after including fancybox.css): - -```css -.fancybox-nav span { - visibility: visible; -} - -.fancybox-nav { - width: 80px; -} - -.fancybox-prev { - left: -80px; -} - -.fancybox-next { - right: -80px; -} -``` - -In that case, you might want to increase space around box: - -```js -$(".fancybox").fancybox({ - margin : [20, 60, 20, 60] -}); -``` - -Bug tracker ------------ - -Have a bug? Please create an issue on GitHub at https://github.com/fancyapps/fancyBox/issues diff --git a/modules/fancybox/source/blank.gif b/modules/fancybox/source/blank.gif deleted file mode 100644 index 35d42e80..00000000 Binary files a/modules/fancybox/source/blank.gif and /dev/null differ diff --git a/modules/fancybox/source/fancybox_loading.gif b/modules/fancybox/source/fancybox_loading.gif deleted file mode 100644 index a03a40c0..00000000 Binary files a/modules/fancybox/source/fancybox_loading.gif and /dev/null differ diff --git a/modules/fancybox/source/fancybox_loading@2x.gif b/modules/fancybox/source/fancybox_loading@2x.gif deleted file mode 100644 index 9205aeb0..00000000 Binary files a/modules/fancybox/source/fancybox_loading@2x.gif and /dev/null differ diff --git a/modules/fancybox/source/fancybox_overlay.png b/modules/fancybox/source/fancybox_overlay.png deleted file mode 100644 index a4391396..00000000 Binary files a/modules/fancybox/source/fancybox_overlay.png and /dev/null differ diff --git a/modules/fancybox/source/fancybox_sprite.png b/modules/fancybox/source/fancybox_sprite.png deleted file mode 100644 index fd8d5ca5..00000000 Binary files a/modules/fancybox/source/fancybox_sprite.png and /dev/null differ diff --git a/modules/fancybox/source/fancybox_sprite@2x.png b/modules/fancybox/source/fancybox_sprite@2x.png deleted file mode 100644 index d0e4779f..00000000 Binary files a/modules/fancybox/source/fancybox_sprite@2x.png and /dev/null differ diff --git a/modules/fancybox/source/helpers/fancybox_buttons.png b/modules/fancybox/source/helpers/fancybox_buttons.png deleted file mode 100644 index 07872072..00000000 Binary files a/modules/fancybox/source/helpers/fancybox_buttons.png and /dev/null differ diff --git a/modules/fancybox/source/helpers/jquery.fancybox-buttons.css b/modules/fancybox/source/helpers/jquery.fancybox-buttons.css deleted file mode 100644 index 302d6d5b..00000000 --- a/modules/fancybox/source/helpers/jquery.fancybox-buttons.css +++ /dev/null @@ -1,97 +0,0 @@ -#fancybox-buttons { - position: fixed; - left: 0; - width: 100%; - z-index: 8050; -} - -#fancybox-buttons.top { - top: 10px; -} - -#fancybox-buttons.bottom { - bottom: 10px; -} - -#fancybox-buttons ul { - display: block; - width: 166px; - height: 30px; - margin: 0 auto; - padding: 0; - list-style: none; - border: 1px solid #111; - border-radius: 3px; - -webkit-box-shadow: inset 0 0 0 1px rgba(255,255,255,.05); - -moz-box-shadow: inset 0 0 0 1px rgba(255,255,255,.05); - box-shadow: inset 0 0 0 1px rgba(255,255,255,.05); - background: rgb(50,50,50); - background: -moz-linear-gradient(top, rgb(68,68,68) 0%, rgb(52,52,52) 50%, rgb(41,41,41) 50%, rgb(51,51,51) 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(68,68,68)), color-stop(50%,rgb(52,52,52)), color-stop(50%,rgb(41,41,41)), color-stop(100%,rgb(51,51,51))); - background: -webkit-linear-gradient(top, rgb(68,68,68) 0%,rgb(52,52,52) 50%,rgb(41,41,41) 50%,rgb(51,51,51) 100%); - background: -o-linear-gradient(top, rgb(68,68,68) 0%,rgb(52,52,52) 50%,rgb(41,41,41) 50%,rgb(51,51,51) 100%); - background: -ms-linear-gradient(top, rgb(68,68,68) 0%,rgb(52,52,52) 50%,rgb(41,41,41) 50%,rgb(51,51,51) 100%); - background: linear-gradient(to bottom, rgb(68,68,68) 0%,rgb(52,52,52) 50%,rgb(41,41,41) 50%,rgb(51,51,51) 100%); - filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#444444', endColorstr='#222222',GradientType=0 ); -} - -#fancybox-buttons ul li { - float: left; - margin: 0; - padding: 0; -} - -#fancybox-buttons a { - display: block; - width: 30px; - height: 30px; - text-indent: -9999px; - background-color: transparent; - background-image: url('fancybox_buttons.png'); - background-repeat: no-repeat; - outline: none; - opacity: 0.8; -} - -#fancybox-buttons a:hover { - opacity: 1; -} - -#fancybox-buttons a.btnPrev { - background-position: 5px 0; -} - -#fancybox-buttons a.btnNext { - background-position: -33px 0; - border-right: 1px solid #3e3e3e; -} - -#fancybox-buttons a.btnPlay { - background-position: 0 -30px; -} - -#fancybox-buttons a.btnPlayOn { - background-position: -30px -30px; -} - -#fancybox-buttons a.btnToggle { - background-position: 3px -60px; - border-left: 1px solid #111; - border-right: 1px solid #3e3e3e; - width: 35px -} - -#fancybox-buttons a.btnToggleOn { - background-position: -27px -60px; -} - -#fancybox-buttons a.btnClose { - border-left: 1px solid #111; - width: 35px; - background-position: -56px 0px; -} - -#fancybox-buttons a.btnDisabled { - opacity : 0.4; - cursor: default; -} diff --git a/modules/fancybox/source/helpers/jquery.fancybox-buttons.js b/modules/fancybox/source/helpers/jquery.fancybox-buttons.js deleted file mode 100644 index 352bb5f0..00000000 --- a/modules/fancybox/source/helpers/jquery.fancybox-buttons.js +++ /dev/null @@ -1,122 +0,0 @@ - /*! - * Buttons helper for fancyBox - * version: 1.0.5 (Mon, 15 Oct 2012) - * @requires fancyBox v2.0 or later - * - * Usage: - * $(".fancybox").fancybox({ - * helpers : { - * buttons: { - * position : 'top' - * } - * } - * }); - * - */ -;(function ($) { - //Shortcut for fancyBox object - var F = $.fancybox; - - //Add helper object - F.helpers.buttons = { - defaults : { - skipSingle : false, // disables if gallery contains single image - position : 'top', // 'top' or 'bottom' - tpl : '
    ' - }, - - list : null, - buttons: null, - - beforeLoad: function (opts, obj) { - //Remove self if gallery do not have at least two items - - if (opts.skipSingle && obj.group.length < 2) { - obj.helpers.buttons = false; - obj.closeBtn = true; - - return; - } - - //Increase top margin to give space for buttons - obj.margin[ opts.position === 'bottom' ? 2 : 0 ] += 30; - }, - - onPlayStart: function () { - if (this.buttons) { - this.buttons.play.attr('title', 'Pause slideshow').addClass('btnPlayOn'); - } - }, - - onPlayEnd: function () { - if (this.buttons) { - this.buttons.play.attr('title', 'Start slideshow').removeClass('btnPlayOn'); - } - }, - - afterShow: function (opts, obj) { - var buttons = this.buttons; - - if (!buttons) { - this.list = $(opts.tpl).addClass(opts.position).appendTo('body'); - - buttons = { - prev : this.list.find('.btnPrev').click( F.prev ), - next : this.list.find('.btnNext').click( F.next ), - play : this.list.find('.btnPlay').click( F.play ), - toggle : this.list.find('.btnToggle').click( F.toggle ), - close : this.list.find('.btnClose').click( F.close ) - } - } - - //Prev - if (obj.index > 0 || obj.loop) { - buttons.prev.removeClass('btnDisabled'); - } else { - buttons.prev.addClass('btnDisabled'); - } - - //Next / Play - if (obj.loop || obj.index < obj.group.length - 1) { - buttons.next.removeClass('btnDisabled'); - buttons.play.removeClass('btnDisabled'); - - } else { - buttons.next.addClass('btnDisabled'); - buttons.play.addClass('btnDisabled'); - } - - this.buttons = buttons; - - this.onUpdate(opts, obj); - }, - - onUpdate: function (opts, obj) { - var toggle; - - if (!this.buttons) { - return; - } - - toggle = this.buttons.toggle.removeClass('btnDisabled btnToggleOn'); - - //Size toggle button - if (obj.canShrink) { - toggle.addClass('btnToggleOn'); - - } else if (!obj.canExpand) { - toggle.addClass('btnDisabled'); - } - }, - - beforeClose: function () { - if (this.list) { - this.list.remove(); - } - - this.list = null; - this.buttons = null; - } - }; - -}(jQuery)); diff --git a/modules/fancybox/source/helpers/jquery.fancybox-media.js b/modules/fancybox/source/helpers/jquery.fancybox-media.js deleted file mode 100644 index 99e62127..00000000 --- a/modules/fancybox/source/helpers/jquery.fancybox-media.js +++ /dev/null @@ -1,201 +0,0 @@ -/*! - * Media helper for fancyBox - * version: 1.0.6 (Fri, 14 Jun 2013) - * @requires fancyBox v2.0 or later - * - * Usage: - * $(".fancybox").fancybox({ - * helpers : { - * media: true - * } - * }); - * - * Set custom URL parameters: - * $(".fancybox").fancybox({ - * helpers : { - * media: { - * youtube : { - * params : { - * autoplay : 0 - * } - * } - * } - * } - * }); - * - * Or: - * $(".fancybox").fancybox({, - * helpers : { - * media: true - * }, - * youtube : { - * autoplay: 0 - * } - * }); - * - * Supports: - * - * Youtube - * http://www.youtube.com/watch?v=opj24KnzrWo - * http://www.youtube.com/embed/opj24KnzrWo - * http://youtu.be/opj24KnzrWo - * http://www.youtube-nocookie.com/embed/opj24KnzrWo - * Vimeo - * http://vimeo.com/40648169 - * http://vimeo.com/channels/staffpicks/38843628 - * http://vimeo.com/groups/surrealism/videos/36516384 - * http://player.vimeo.com/video/45074303 - * Metacafe - * http://www.metacafe.com/watch/7635964/dr_seuss_the_lorax_movie_trailer/ - * http://www.metacafe.com/watch/7635964/ - * Dailymotion - * http://www.dailymotion.com/video/xoytqh_dr-seuss-the-lorax-premiere_people - * Twitvid - * http://twitvid.com/QY7MD - * Twitpic - * http://twitpic.com/7p93st - * Instagram - * http://instagr.am/p/IejkuUGxQn/ - * http://instagram.com/p/IejkuUGxQn/ - * Google maps - * http://maps.google.com/maps?q=Eiffel+Tower,+Avenue+Gustave+Eiffel,+Paris,+France&t=h&z=17 - * http://maps.google.com/?ll=48.857995,2.294297&spn=0.007666,0.021136&t=m&z=16 - * http://maps.google.com/?ll=48.859463,2.292626&spn=0.000965,0.002642&t=m&z=19&layer=c&cbll=48.859524,2.292532&panoid=YJ0lq28OOy3VT2IqIuVY0g&cbp=12,151.58,,0,-15.56 - */ -;(function ($) { - "use strict"; - - //Shortcut for fancyBox object - var F = $.fancybox, - format = function( url, rez, params ) { - params = params || ''; - - if ( $.type( params ) === "object" ) { - params = $.param(params, true); - } - - $.each(rez, function(key, value) { - url = url.replace( '$' + key, value || '' ); - }); - - if (params.length) { - url += ( url.indexOf('?') > 0 ? '&' : '?' ) + params; - } - - return url; - }; - - //Add helper object - F.helpers.media = { - defaults : { - youtube : { - matcher : /(youtube\.com|youtu\.be|youtube-nocookie\.com)\/(watch\?v=|v\/|u\/|embed\/?)?(videoseries\?list=(.*)|[\w-]{11}|\?listType=(.*)&list=(.*)).*/i, - params : { - autoplay : 1, - autohide : 1, - fs : 1, - rel : 0, - hd : 1, - wmode : 'opaque', - enablejsapi : 1, - ps: 'docs', - controls: 1 - }, - type : 'iframe', - url : '//www.youtube.com/embed/$3' - }, - vimeo : { - matcher : /(?:vimeo(?:pro)?.com)\/(?:[^\d]+)?(\d+)(?:.*)/, - params : { - autoplay : 1, - hd : 1, - show_title : 1, - show_byline : 1, - show_portrait : 0, - fullscreen : 1 - }, - type : 'iframe', - url : '//player.vimeo.com/video/$1' - }, - metacafe : { - matcher : /metacafe.com\/(?:watch|fplayer)\/([\w\-]{1,10})/, - params : { - autoPlay : 'yes' - }, - type : 'swf', - url : function( rez, params, obj ) { - obj.swf.flashVars = 'playerVars=' + $.param( params, true ); - - return '//www.metacafe.com/fplayer/' + rez[1] + '/.swf'; - } - }, - dailymotion : { - matcher : /dailymotion.com\/video\/(.*)\/?(.*)/, - params : { - additionalInfos : 0, - autoStart : 1 - }, - type : 'swf', - url : '//www.dailymotion.com/swf/video/$1' - }, - twitvid : { - matcher : /twitvid\.com\/([a-zA-Z0-9_\-\?\=]+)/i, - params : { - autoplay : 0 - }, - type : 'iframe', - url : '//www.twitvid.com/embed.php?guid=$1' - }, - twitpic : { - matcher : /twitpic\.com\/(?!(?:place|photos|events)\/)([a-zA-Z0-9\?\=\-]+)/i, - type : 'image', - url : '//twitpic.com/show/full/$1/' - }, - instagram : { - matcher : /(instagr\.am|instagram\.com)\/p\/([a-zA-Z0-9_\-]+)\/?/i, - type : 'image', - url : '//$1/p/$2/media/?size=l' - }, - google_maps : { - matcher : /maps\.google\.([a-z]{2,3}(\.[a-z]{2})?)\/(\?ll=|maps\?)(.*)/i, - type : 'iframe', - url : function( rez ) { - return '//maps.google.' + rez[1] + '/' + rez[3] + '' + rez[4] + '&output=' + (rez[4].indexOf('layer=c') > 0 ? 'svembed' : 'embed'); - } - } - }, - - beforeLoad : function(opts, obj) { - var url = obj.href || '', - type = false, - what, - item, - rez, - params; - - for (what in opts) { - if (opts.hasOwnProperty(what)) { - item = opts[ what ]; - rez = url.match( item.matcher ); - - if (rez) { - type = item.type; - params = $.extend(true, {}, item.params, obj[ what ] || ($.isPlainObject(opts[ what ]) ? opts[ what ].params : null)); - - url = $.type( item.url ) === "function" ? item.url.call( this, rez, params, obj ) : format( item.url, rez, params ); - - break; - } - } - } - - if (type) { - obj.href = url; - obj.type = type; - - obj.autoHeight = false; - } - } - }; - -}(jQuery)); diff --git a/modules/fancybox/source/helpers/jquery.fancybox-thumbs.css b/modules/fancybox/source/helpers/jquery.fancybox-thumbs.css deleted file mode 100644 index 63d29436..00000000 --- a/modules/fancybox/source/helpers/jquery.fancybox-thumbs.css +++ /dev/null @@ -1,55 +0,0 @@ -#fancybox-thumbs { - position: fixed; - left: 0; - width: 100%; - overflow: hidden; - z-index: 8050; -} - -#fancybox-thumbs.bottom { - bottom: 2px; -} - -#fancybox-thumbs.top { - top: 2px; -} - -#fancybox-thumbs ul { - position: relative; - list-style: none; - margin: 0; - padding: 0; -} - -#fancybox-thumbs ul li { - float: left; - padding: 1px; - opacity: 0.5; -} - -#fancybox-thumbs ul li.active { - opacity: 0.75; - padding: 0; - border: 1px solid #fff; -} - -#fancybox-thumbs ul li:hover { - opacity: 1; -} - -#fancybox-thumbs ul li a { - display: block; - position: relative; - overflow: hidden; - border: 1px solid #222; - background: #111; - outline: none; -} - -#fancybox-thumbs ul li img { - display: block; - position: relative; - border: 0; - padding: 0; - max-width: none; -} \ No newline at end of file diff --git a/modules/fancybox/source/helpers/jquery.fancybox-thumbs.js b/modules/fancybox/source/helpers/jquery.fancybox-thumbs.js deleted file mode 100644 index 1aec7df8..00000000 --- a/modules/fancybox/source/helpers/jquery.fancybox-thumbs.js +++ /dev/null @@ -1,165 +0,0 @@ - /*! - * Thumbnail helper for fancyBox - * version: 1.0.7 (Mon, 01 Oct 2012) - * @requires fancyBox v2.0 or later - * - * Usage: - * $(".fancybox").fancybox({ - * helpers : { - * thumbs: { - * width : 50, - * height : 50 - * } - * } - * }); - * - */ -;(function ($) { - //Shortcut for fancyBox object - var F = $.fancybox; - - //Add helper object - F.helpers.thumbs = { - defaults : { - width : 50, // thumbnail width - height : 50, // thumbnail height - position : 'bottom', // 'top' or 'bottom' - source : function ( item ) { // function to obtain the URL of the thumbnail image - var href; - - if (item.element) { - href = $(item.element).find('img').attr('src'); - } - - if (!href && item.type === 'image' && item.href) { - href = item.href; - } - - return href; - } - }, - - wrap : null, - list : null, - width : 0, - - init: function (opts, obj) { - var that = this, - list, - thumbWidth = opts.width, - thumbHeight = opts.height, - thumbSource = opts.source; - - //Build list structure - list = ''; - - for (var n = 0; n < obj.group.length; n++) { - list += '
  • '; - } - - this.wrap = $('
    ').addClass(opts.position).appendTo('body'); - this.list = $('
      ' + list + '
    ').appendTo(this.wrap); - - //Load each thumbnail - $.each(obj.group, function (i) { - var el = obj.group[ i ], - href = thumbSource( el ); - - if (!href) { - return; - } - - $("").on("load", function () { - var width = this.width, - height = this.height, - widthRatio, heightRatio, parent; - - if (!that.list || !width || !height) { - return; - } - - //Calculate thumbnail width/height and center it - widthRatio = width / thumbWidth; - heightRatio = height / thumbHeight; - - parent = that.list.children().eq(i).find('a'); - - if (widthRatio >= 1 && heightRatio >= 1) { - if (widthRatio > heightRatio) { - width = Math.floor(width / heightRatio); - height = thumbHeight; - - } else { - width = thumbWidth; - height = Math.floor(height / widthRatio); - } - } - - $(this).css({ - width : width, - height : height, - top : Math.floor(thumbHeight / 2 - height / 2), - left : Math.floor(thumbWidth / 2 - width / 2) - }); - - parent.width(thumbWidth).height(thumbHeight); - - $(this).hide().appendTo(parent).fadeIn(300); - - }) - .attr('src', href) - .attr('title', el.title); - }); - - //Set initial width - this.width = this.list.children().eq(0).outerWidth(true); - - this.list.width(this.width * (obj.group.length + 1)).css('left', Math.floor($(window).width() * 0.5 - (obj.index * this.width + this.width * 0.5))); - }, - - beforeLoad: function (opts, obj) { - //Remove self if gallery do not have at least two items - if (obj.group.length < 2) { - obj.helpers.thumbs = false; - - return; - } - - //Increase bottom margin to give space for thumbs - obj.margin[ opts.position === 'top' ? 0 : 2 ] += ((opts.height) + 15); - }, - - afterShow: function (opts, obj) { - //Check if exists and create or update list - if (this.list) { - this.onUpdate(opts, obj); - - } else { - this.init(opts, obj); - } - - //Set active element - this.list.children().removeClass('active').eq(obj.index).addClass('active'); - }, - - //Center list - onUpdate: function (opts, obj) { - if (this.list) { - this.list.stop(true).animate({ - 'left': Math.floor($(window).width() * 0.5 - (obj.index * this.width + this.width * 0.5)) - }, 150); - } - }, - - beforeClose: function () { - if (this.wrap) { - this.wrap.remove(); - } - - this.wrap = null; - this.list = null; - this.width = 0; - } - } - -}(jQuery)); diff --git a/modules/fancybox/source/jquery.fancybox.css b/modules/fancybox/source/jquery.fancybox.css deleted file mode 100644 index fb2c6dd2..00000000 --- a/modules/fancybox/source/jquery.fancybox.css +++ /dev/null @@ -1,275 +0,0 @@ -/*! fancyBox v2.1.5 fancyapps.com | fancyapps.com/fancybox/#license */ -.fancybox-wrap, -.fancybox-skin, -.fancybox-outer, -.fancybox-inner, -.fancybox-image, -.fancybox-wrap iframe, -.fancybox-wrap object, -.fancybox-nav, -.fancybox-nav span, -.fancybox-tmp -{ - padding: 0; - margin: 0; - border: 0; - outline: none; - vertical-align: top; -} - -.fancybox-wrap { - position: absolute; - top: 0; - left: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - z-index: 8020; -} - -.fancybox-skin { - position: relative; - background: #f9f9f9; - color: #444; - text-shadow: none; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} - -.fancybox-opened { - z-index: 8030; -} - -.fancybox-opened .fancybox-skin { - -webkit-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); - -moz-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); -} - -.fancybox-outer, .fancybox-inner { - position: relative; -} - -.fancybox-inner { - overflow: hidden; -} - -.fancybox-type-iframe .fancybox-inner { - -webkit-overflow-scrolling: touch; -} - -.fancybox-error { - color: #444; - font: 14px/20px "Helvetica Neue",Helvetica,Arial,sans-serif; - margin: 0; - padding: 15px; - white-space: nowrap; -} - -.fancybox-image, .fancybox-iframe { - display: block; - width: 100%; - height: 100%; -} - -.fancybox-image { - max-width: 100%; - max-height: 100%; -} - -#fancybox-loading, .fancybox-close, .fancybox-prev span, .fancybox-next span { - background-image: url(fancybox_sprite.png); -} - -#fancybox-loading { - position: fixed; - top: 50%; - left: 50%; - margin-top: -22px; - margin-left: -22px; - background-position: 0 -108px; - opacity: 0.8; - cursor: pointer; - z-index: 8060; -} - -#fancybox-loading div { - width: 44px; - height: 44px; - background: url(fancybox_loading.gif) center center no-repeat; -} - -.fancybox-close { - position: absolute; - top: -18px; - right: -18px; - width: 36px; - height: 36px; - cursor: pointer; - z-index: 8040; -} - -.fancybox-nav { - position: absolute; - top: 0; - width: 40%; - height: 100%; - cursor: pointer; - text-decoration: none; - background: transparent url(blank.gif); /* helps IE */ - -webkit-tap-highlight-color: rgba(0,0,0,0); - z-index: 8040; -} - -.fancybox-prev { - left: 0; -} - -.fancybox-next { - right: 0; -} - -.fancybox-nav span { - position: absolute; - top: 50%; - width: 36px; - height: 34px; - margin-top: -18px; - cursor: pointer; - z-index: 8040; - visibility: hidden; -} - -.fancybox-prev span { - left: 10px; - background-position: 0 -36px; -} - -.fancybox-next span { - right: 10px; - background-position: 0 -72px; -} - -.fancybox-nav:hover span { - visibility: visible; -} - -.fancybox-tmp { - position: absolute; - top: -99999px; - left: -99999px; - max-width: 99999px; - max-height: 99999px; - overflow: visible !important; -} - -/* Overlay helper */ - -.fancybox-lock { - overflow: visible !important; - width: auto; -} - -.fancybox-lock body { - overflow: hidden !important; -} - -.fancybox-lock-test { - overflow-y: hidden !important; -} - -.fancybox-overlay { - position: absolute; - top: 0; - left: 0; - overflow: hidden; - display: none; - z-index: 8010; - background: url(fancybox_overlay.png); -} - -.fancybox-overlay-fixed { - position: fixed; - bottom: 0; - right: 0; -} - -.fancybox-lock .fancybox-overlay { - overflow: auto; - overflow-y: scroll; -} - -/* Title helper */ - -.fancybox-title { - visibility: hidden; - font: normal 13px/20px "Helvetica Neue",Helvetica,Arial,sans-serif; - position: relative; - text-shadow: none; - z-index: 8050; -} - -.fancybox-opened .fancybox-title { - visibility: visible; -} - -.fancybox-title-float-wrap { - position: absolute; - bottom: 0; - right: 50%; - margin-bottom: -35px; - z-index: 8050; - text-align: center; -} - -.fancybox-title-float-wrap .child { - display: inline-block; - margin-right: -100%; - padding: 2px 20px; - background: transparent; /* Fallback for web browsers that doesn't support RGBa */ - background: rgba(0, 0, 0, 0.8); - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; - text-shadow: 0 1px 2px #222; - color: #FFF; - font-weight: bold; - line-height: 24px; - white-space: nowrap; -} - -.fancybox-title-outside-wrap { - position: relative; - margin-top: 10px; - color: #fff; -} - -.fancybox-title-inside-wrap { - padding-top: 10px; -} - -.fancybox-title-over-wrap { - position: absolute; - bottom: 0; - left: 0; - color: #fff; - padding: 10px; - background: #000; - background: rgba(0, 0, 0, .8); -} - -/*Retina graphics!*/ -@media only screen and (-webkit-min-device-pixel-ratio: 1.5), - only screen and (min--moz-device-pixel-ratio: 1.5), - only screen and (min-device-pixel-ratio: 1.5){ - - #fancybox-loading, .fancybox-close, .fancybox-prev span, .fancybox-next span { - background-image: url(fancybox_sprite@2x.png); - background-size: 44px 152px; /*The size of the normal image, half the size of the hi-res image*/ - } - - #fancybox-loading div { - background-image: url(fancybox_loading@2x.gif); - background-size: 24px 24px; /*The size of the normal image, half the size of the hi-res image*/ - } -} \ No newline at end of file diff --git a/modules/fancybox/source/jquery.fancybox.js b/modules/fancybox/source/jquery.fancybox.js deleted file mode 100755 index cef01701..00000000 --- a/modules/fancybox/source/jquery.fancybox.js +++ /dev/null @@ -1,2018 +0,0 @@ -/*! - * fancyBox - jQuery Plugin - * version: 2.1.5 (Fri, 14 Jun 2013) - * requires jQuery v1.6 or later - * - * Examples at http://fancyapps.com/fancybox/ - * License: www.fancyapps.com/fancybox/#license - * - * Copyright 2012 Janis Skarnelis - janis@fancyapps.com - * - */ - -;(function (window, document, $, undefined) { - "use strict"; - - var H = $("html"), - W = $(window), - D = $(document), - F = $.fancybox = function () { - F.open.apply( this, arguments ); - }, - IE = navigator.userAgent.match(/msie/i), - didUpdate = null, - isTouch = document.createTouch !== undefined, - - isQuery = function(obj) { - return obj && obj.hasOwnProperty && obj instanceof $; - }, - isString = function(str) { - return str && $.type(str) === "string"; - }, - isPercentage = function(str) { - return isString(str) && str.indexOf('%') > 0; - }, - isScrollable = function(el) { - return (el && !(el.style.overflow && el.style.overflow === 'hidden') && ((el.clientWidth && el.scrollWidth > el.clientWidth) || (el.clientHeight && el.scrollHeight > el.clientHeight))); - }, - getScalar = function(orig, dim) { - var value = parseInt(orig, 10) || 0; - - if (dim && isPercentage(orig)) { - value = F.getViewport()[ dim ] / 100 * value; - } - - return Math.ceil(value); - }, - getValue = function(value, dim) { - return getScalar(value, dim) + 'px'; - }; - - $.extend(F, { - // The current version of fancyBox - version: '2.1.5', - - defaults: { - padding : 15, - margin : 20, - - width : 800, - height : 600, - minWidth : 100, - minHeight : 100, - maxWidth : 9999, - maxHeight : 9999, - pixelRatio: 1, // Set to 2 for retina display support - - autoSize : true, - autoHeight : false, - autoWidth : false, - - autoResize : true, - autoCenter : !isTouch, - fitToView : true, - aspectRatio : false, - topRatio : 0.5, - leftRatio : 0.5, - - scrolling : 'auto', // 'auto', 'yes' or 'no' - wrapCSS : '', - - arrows : true, - closeBtn : true, - closeClick : false, - nextClick : false, - mouseWheel : true, - autoPlay : false, - playSpeed : 3000, - preload : 3, - modal : false, - loop : true, - - ajax : { - dataType : 'html', - headers : { 'X-fancyBox': true } - }, - iframe : { - scrolling : 'auto', - preload : true - }, - swf : { - wmode: 'transparent', - allowfullscreen : 'true', - allowscriptaccess : 'always' - }, - - keys : { - next : { - 13 : 'left', // enter - 34 : 'up', // page down - 39 : 'left', // right arrow - 40 : 'up' // down arrow - }, - prev : { - 8 : 'right', // backspace - 33 : 'down', // page up - 37 : 'right', // left arrow - 38 : 'down' // up arrow - }, - close : [27], // escape key - play : [32], // space - start/stop slideshow - toggle : [70] // letter "f" - toggle fullscreen - }, - - direction : { - next : 'left', - prev : 'right' - }, - - scrollOutside : true, - - // Override some properties - index : 0, - type : null, - href : null, - content : null, - title : null, - - // HTML templates - tpl: { - wrap : '
    ', - image : '', - iframe : '', - error : '

    The requested content cannot be loaded.
    Please try again later.

    ', - closeBtn : '', - next : '', - prev : '', - loading : '
    ' - }, - - // Properties for each animation type - // Opening fancyBox - openEffect : 'fade', // 'elastic', 'fade' or 'none' - openSpeed : 250, - openEasing : 'swing', - openOpacity : true, - openMethod : 'zoomIn', - - // Closing fancyBox - closeEffect : 'fade', // 'elastic', 'fade' or 'none' - closeSpeed : 250, - closeEasing : 'swing', - closeOpacity : true, - closeMethod : 'zoomOut', - - // Changing next gallery item - nextEffect : 'elastic', // 'elastic', 'fade' or 'none' - nextSpeed : 250, - nextEasing : 'swing', - nextMethod : 'changeIn', - - // Changing previous gallery item - prevEffect : 'elastic', // 'elastic', 'fade' or 'none' - prevSpeed : 250, - prevEasing : 'swing', - prevMethod : 'changeOut', - - // Enable default helpers - helpers : { - overlay : true, - title : true - }, - - // Callbacks - onCancel : $.noop, // If canceling - beforeLoad : $.noop, // Before loading - afterLoad : $.noop, // After loading - beforeShow : $.noop, // Before changing in current item - afterShow : $.noop, // After opening - beforeChange : $.noop, // Before changing gallery item - beforeClose : $.noop, // Before closing - afterClose : $.noop // After closing - }, - - //Current state - group : {}, // Selected group - opts : {}, // Group options - previous : null, // Previous element - coming : null, // Element being loaded - current : null, // Currently loaded element - isActive : false, // Is activated - isOpen : false, // Is currently open - isOpened : false, // Have been fully opened at least once - - wrap : null, - skin : null, - outer : null, - inner : null, - - player : { - timer : null, - isActive : false - }, - - // Loaders - ajaxLoad : null, - imgPreload : null, - - // Some collections - transitions : {}, - helpers : {}, - - /* - * Static methods - */ - - open: function (group, opts) { - if (!group) { - return; - } - - if (!$.isPlainObject(opts)) { - opts = {}; - } - - // Close if already active - if (false === F.close(true)) { - return; - } - - // Normalize group - if (!$.isArray(group)) { - group = isQuery(group) ? $(group).get() : [group]; - } - - // Recheck if the type of each element is `object` and set content type (image, ajax, etc) - $.each(group, function(i, element) { - var obj = {}, - href, - title, - content, - type, - rez, - hrefParts, - selector; - - if ($.type(element) === "object") { - // Check if is DOM element - if (element.nodeType) { - element = $(element); - } - - if (isQuery(element)) { - obj = { - href : element.data('fancybox-href') || element.attr('href'), - title : $('
    ').text( element.data('fancybox-title') || element.attr('title') || '' ).html(), - isDom : true, - element : element - }; - - if ($.metadata) { - $.extend(true, obj, element.metadata()); - } - - } else { - obj = element; - } - } - - href = opts.href || obj.href || (isString(element) ? element : null); - title = opts.title !== undefined ? opts.title : obj.title || ''; - - content = opts.content || obj.content; - type = content ? 'html' : (opts.type || obj.type); - - if (!type && obj.isDom) { - type = element.data('fancybox-type'); - - if (!type) { - rez = element.prop('class').match(/fancybox\.(\w+)/); - type = rez ? rez[1] : null; - } - } - - if (isString(href)) { - // Try to guess the content type - if (!type) { - if (F.isImage(href)) { - type = 'image'; - - } else if (F.isSWF(href)) { - type = 'swf'; - - } else if (href.charAt(0) === '#') { - type = 'inline'; - - } else if (isString(element)) { - type = 'html'; - content = element; - } - } - - // Split url into two pieces with source url and content selector, e.g, - // "/mypage.html #my_id" will load "/mypage.html" and display element having id "my_id" - if (type === 'ajax') { - hrefParts = href.split(/\s+/, 2); - href = hrefParts.shift(); - selector = hrefParts.shift(); - } - } - - if (!content) { - if (type === 'inline') { - if (href) { - content = $( isString(href) ? href.replace(/.*(?=#[^\s]+$)/, '') : href ); //strip for ie7 - - } else if (obj.isDom) { - content = element; - } - - } else if (type === 'html') { - content = href; - - } else if (!type && !href && obj.isDom) { - type = 'inline'; - content = element; - } - } - - $.extend(obj, { - href : href, - type : type, - content : content, - title : title, - selector : selector - }); - - group[ i ] = obj; - }); - - // Extend the defaults - F.opts = $.extend(true, {}, F.defaults, opts); - - // All options are merged recursive except keys - if (opts.keys !== undefined) { - F.opts.keys = opts.keys ? $.extend({}, F.defaults.keys, opts.keys) : false; - } - - F.group = group; - - return F._start(F.opts.index); - }, - - // Cancel image loading or abort ajax request - cancel: function () { - var coming = F.coming; - - if (coming && false === F.trigger('onCancel')) { - return; - } - - F.hideLoading(); - - if (!coming) { - return; - } - - if (F.ajaxLoad) { - F.ajaxLoad.abort(); - } - - F.ajaxLoad = null; - - if (F.imgPreload) { - F.imgPreload.onload = F.imgPreload.onerror = null; - } - - if (coming.wrap) { - coming.wrap.stop(true, true).trigger('onReset').remove(); - } - - F.coming = null; - - // If the first item has been canceled, then clear everything - if (!F.current) { - F._afterZoomOut( coming ); - } - }, - - // Start closing animation if is open; remove immediately if opening/closing - close: function (event) { - F.cancel(); - - if (false === F.trigger('beforeClose')) { - return; - } - - F.unbindEvents(); - - if (!F.isActive) { - return; - } - - if (!F.isOpen || event === true) { - $('.fancybox-wrap').stop(true).trigger('onReset').remove(); - - F._afterZoomOut(); - - } else { - F.isOpen = F.isOpened = false; - F.isClosing = true; - - $('.fancybox-item, .fancybox-nav').remove(); - - F.wrap.stop(true, true).removeClass('fancybox-opened'); - - F.transitions[ F.current.closeMethod ](); - } - }, - - // Manage slideshow: - // $.fancybox.play(); - toggle slideshow - // $.fancybox.play( true ); - start - // $.fancybox.play( false ); - stop - play: function ( action ) { - var clear = function () { - clearTimeout(F.player.timer); - }, - set = function () { - clear(); - - if (F.current && F.player.isActive) { - F.player.timer = setTimeout(F.next, F.current.playSpeed); - } - }, - stop = function () { - clear(); - - D.unbind('.player'); - - F.player.isActive = false; - - F.trigger('onPlayEnd'); - }, - start = function () { - if (F.current && (F.current.loop || F.current.index < F.group.length - 1)) { - F.player.isActive = true; - - D.bind({ - 'onCancel.player beforeClose.player' : stop, - 'onUpdate.player' : set, - 'beforeLoad.player' : clear - }); - - set(); - - F.trigger('onPlayStart'); - } - }; - - if (action === true || (!F.player.isActive && action !== false)) { - start(); - } else { - stop(); - } - }, - - // Navigate to next gallery item - next: function ( direction ) { - var current = F.current; - - if (current) { - if (!isString(direction)) { - direction = current.direction.next; - } - - F.jumpto(current.index + 1, direction, 'next'); - } - }, - - // Navigate to previous gallery item - prev: function ( direction ) { - var current = F.current; - - if (current) { - if (!isString(direction)) { - direction = current.direction.prev; - } - - F.jumpto(current.index - 1, direction, 'prev'); - } - }, - - // Navigate to gallery item by index - jumpto: function ( index, direction, router ) { - var current = F.current; - - if (!current) { - return; - } - - index = getScalar(index); - - F.direction = direction || current.direction[ (index >= current.index ? 'next' : 'prev') ]; - F.router = router || 'jumpto'; - - if (current.loop) { - if (index < 0) { - index = current.group.length + (index % current.group.length); - } - - index = index % current.group.length; - } - - if (current.group[ index ] !== undefined) { - F.cancel(); - - F._start(index); - } - }, - - // Center inside viewport and toggle position type to fixed or absolute if needed - reposition: function (e, onlyAbsolute) { - var current = F.current, - wrap = current ? current.wrap : null, - pos; - - if (wrap) { - pos = F._getPosition(onlyAbsolute); - - if (e && e.type === 'scroll') { - delete pos.position; - - wrap.stop(true, true).animate(pos, 200); - - } else { - wrap.css(pos); - - current.pos = $.extend({}, current.dim, pos); - } - } - }, - - update: function (e) { - var type = (e && e.originalEvent && e.originalEvent.type), - anyway = !type || type === 'orientationchange'; - - if (anyway) { - clearTimeout(didUpdate); - - didUpdate = null; - } - - if (!F.isOpen || didUpdate) { - return; - } - - didUpdate = setTimeout(function() { - var current = F.current; - - if (!current || F.isClosing) { - return; - } - - F.wrap.removeClass('fancybox-tmp'); - - if (anyway || type === 'load' || (type === 'resize' && current.autoResize)) { - F._setDimension(); - } - - if (!(type === 'scroll' && current.canShrink)) { - F.reposition(e); - } - - F.trigger('onUpdate'); - - didUpdate = null; - - }, (anyway && !isTouch ? 0 : 300)); - }, - - // Shrink content to fit inside viewport or restore if resized - toggle: function ( action ) { - if (F.isOpen) { - F.current.fitToView = $.type(action) === "boolean" ? action : !F.current.fitToView; - - // Help browser to restore document dimensions - if (isTouch) { - F.wrap.removeAttr('style').addClass('fancybox-tmp'); - - F.trigger('onUpdate'); - } - - F.update(); - } - }, - - hideLoading: function () { - D.unbind('.loading'); - - $('#fancybox-loading').remove(); - }, - - showLoading: function () { - var el, viewport; - - F.hideLoading(); - - el = $(F.opts.tpl.loading).click(F.cancel).appendTo('body'); - - // If user will press the escape-button, the request will be canceled - D.bind('keydown.loading', function(e) { - if ((e.which || e.keyCode) === 27) { - e.preventDefault(); - - F.cancel(); - } - }); - - if (!F.defaults.fixed) { - viewport = F.getViewport(); - - el.css({ - position : 'absolute', - top : (viewport.h * 0.5) + viewport.y, - left : (viewport.w * 0.5) + viewport.x - }); - } - - F.trigger('onLoading'); - }, - - getViewport: function () { - var locked = (F.current && F.current.locked) || false, - rez = { - x: W.scrollLeft(), - y: W.scrollTop() - }; - - if (locked && locked.length) { - rez.w = locked[0].clientWidth; - rez.h = locked[0].clientHeight; - - } else { - // See http://bugs.jquery.com/ticket/6724 - rez.w = isTouch && window.innerWidth ? window.innerWidth : W.width(); - rez.h = isTouch && window.innerHeight ? window.innerHeight : W.height(); - } - - return rez; - }, - - // Unbind the keyboard / clicking actions - unbindEvents: function () { - if (F.wrap && isQuery(F.wrap)) { - F.wrap.unbind('.fb'); - } - - D.unbind('.fb'); - W.unbind('.fb'); - }, - - bindEvents: function () { - var current = F.current, - keys; - - if (!current) { - return; - } - - // Changing document height on iOS devices triggers a 'resize' event, - // that can change document height... repeating infinitely - W.bind('orientationchange.fb' + (isTouch ? '' : ' resize.fb') + (current.autoCenter && !current.locked ? ' scroll.fb' : ''), F.update); - - keys = current.keys; - - if (keys) { - D.bind('keydown.fb', function (e) { - var code = e.which || e.keyCode, - target = e.target || e.srcElement; - - // Skip esc key if loading, because showLoading will cancel preloading - if (code === 27 && F.coming) { - return false; - } - - // Ignore key combinations and key events within form elements - if (!e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && !(target && (target.type || $(target).is('[contenteditable]')))) { - $.each(keys, function(i, val) { - if (current.group.length > 1 && val[ code ] !== undefined) { - F[ i ]( val[ code ] ); - - e.preventDefault(); - return false; - } - - if ($.inArray(code, val) > -1) { - F[ i ] (); - - e.preventDefault(); - return false; - } - }); - } - }); - } - - if ($.fn.mousewheel && current.mouseWheel) { - F.wrap.bind('mousewheel.fb', function (e, delta, deltaX, deltaY) { - var target = e.target || null, - parent = $(target), - canScroll = false; - - while (parent.length) { - if (canScroll || parent.is('.fancybox-skin') || parent.is('.fancybox-wrap')) { - break; - } - - canScroll = isScrollable( parent[0] ); - parent = $(parent).parent(); - } - - if (delta !== 0 && !canScroll) { - if (F.group.length > 1 && !current.canShrink) { - if (deltaY > 0 || deltaX > 0) { - F.prev( deltaY > 0 ? 'down' : 'left' ); - - } else if (deltaY < 0 || deltaX < 0) { - F.next( deltaY < 0 ? 'up' : 'right' ); - } - - e.preventDefault(); - } - } - }); - } - }, - - trigger: function (event, o) { - var ret, obj = o || F.coming || F.current; - - if (obj) { - if ($.isFunction( obj[event] )) { - ret = obj[event].apply(obj, Array.prototype.slice.call(arguments, 1)); - } - - if (ret === false) { - return false; - } - - if (obj.helpers) { - $.each(obj.helpers, function (helper, opts) { - if (opts && F.helpers[helper] && $.isFunction(F.helpers[helper][event])) { - F.helpers[helper][event]($.extend(true, {}, F.helpers[helper].defaults, opts), obj); - } - }); - } - } - - D.trigger(event); - }, - - isImage: function (str) { - return isString(str) && str.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i); - }, - - isSWF: function (str) { - return isString(str) && str.match(/\.(swf)((\?|#).*)?$/i); - }, - - _start: function (index) { - var coming = {}, - obj, - href, - type, - margin, - padding; - - index = getScalar( index ); - obj = F.group[ index ] || null; - - if (!obj) { - return false; - } - - coming = $.extend(true, {}, F.opts, obj); - - // Convert margin and padding properties to array - top, right, bottom, left - margin = coming.margin; - padding = coming.padding; - - if ($.type(margin) === 'number') { - coming.margin = [margin, margin, margin, margin]; - } - - if ($.type(padding) === 'number') { - coming.padding = [padding, padding, padding, padding]; - } - - // 'modal' propery is just a shortcut - if (coming.modal) { - $.extend(true, coming, { - closeBtn : false, - closeClick : false, - nextClick : false, - arrows : false, - mouseWheel : false, - keys : null, - helpers: { - overlay : { - closeClick : false - } - } - }); - } - - // 'autoSize' property is a shortcut, too - if (coming.autoSize) { - coming.autoWidth = coming.autoHeight = true; - } - - if (coming.width === 'auto') { - coming.autoWidth = true; - } - - if (coming.height === 'auto') { - coming.autoHeight = true; - } - - /* - * Add reference to the group, so it`s possible to access from callbacks, example: - * afterLoad : function() { - * this.title = 'Image ' + (this.index + 1) + ' of ' + this.group.length + (this.title ? ' - ' + this.title : ''); - * } - */ - - coming.group = F.group; - coming.index = index; - - // Give a chance for callback or helpers to update coming item (type, title, etc) - F.coming = coming; - - if (false === F.trigger('beforeLoad')) { - F.coming = null; - - return; - } - - type = coming.type; - href = coming.href; - - if (!type) { - F.coming = null; - - //If we can not determine content type then drop silently or display next/prev item if looping through gallery - if (F.current && F.router && F.router !== 'jumpto') { - F.current.index = index; - - return F[ F.router ]( F.direction ); - } - - return false; - } - - F.isActive = true; - - if (type === 'image' || type === 'swf') { - coming.autoHeight = coming.autoWidth = false; - coming.scrolling = 'visible'; - } - - if (type === 'image') { - coming.aspectRatio = true; - } - - if (type === 'iframe' && isTouch) { - coming.scrolling = 'scroll'; - } - - // Build the neccessary markup - coming.wrap = $(coming.tpl.wrap).addClass('fancybox-' + (isTouch ? 'mobile' : 'desktop') + ' fancybox-type-' + type + ' fancybox-tmp ' + coming.wrapCSS).appendTo( coming.parent || 'body' ); - - $.extend(coming, { - skin : $('.fancybox-skin', coming.wrap), - outer : $('.fancybox-outer', coming.wrap), - inner : $('.fancybox-inner', coming.wrap) - }); - - $.each(["Top", "Right", "Bottom", "Left"], function(i, v) { - coming.skin.css('padding' + v, getValue(coming.padding[ i ])); - }); - - F.trigger('onReady'); - - // Check before try to load; 'inline' and 'html' types need content, others - href - if (type === 'inline' || type === 'html') { - if (!coming.content || !coming.content.length) { - return F._error( 'content' ); - } - - } else if (!href) { - return F._error( 'href' ); - } - - if (type === 'image') { - F._loadImage(); - - } else if (type === 'ajax') { - F._loadAjax(); - - } else if (type === 'iframe') { - F._loadIframe(); - - } else { - F._afterLoad(); - } - }, - - _error: function ( type ) { - $.extend(F.coming, { - type : 'html', - autoWidth : true, - autoHeight : true, - minWidth : 0, - minHeight : 0, - scrolling : 'no', - hasError : type, - content : F.coming.tpl.error - }); - - F._afterLoad(); - }, - - _loadImage: function () { - // Reset preload image so it is later possible to check "complete" property - var img = F.imgPreload = new Image(); - - img.onload = function () { - this.onload = this.onerror = null; - - F.coming.width = this.width / F.opts.pixelRatio; - F.coming.height = this.height / F.opts.pixelRatio; - - F._afterLoad(); - }; - - img.onerror = function () { - this.onload = this.onerror = null; - - F._error( 'image' ); - }; - - img.src = F.coming.href; - - if (img.complete !== true) { - F.showLoading(); - } - }, - - _loadAjax: function () { - var coming = F.coming; - - F.showLoading(); - - F.ajaxLoad = $.ajax($.extend({}, coming.ajax, { - url: coming.href, - error: function (jqXHR, textStatus) { - if (F.coming && textStatus !== 'abort') { - F._error( 'ajax', jqXHR ); - - } else { - F.hideLoading(); - } - }, - success: function (data, textStatus) { - if (textStatus === 'success') { - coming.content = data; - - F._afterLoad(); - } - } - })); - }, - - _loadIframe: function() { - var coming = F.coming, - iframe = $(coming.tpl.iframe.replace(/\{rnd\}/g, new Date().getTime())) - .attr('scrolling', isTouch ? 'auto' : coming.iframe.scrolling) - .attr('src', coming.href); - - // This helps IE - $(coming.wrap).bind('onReset', function () { - try { - $(this).find('iframe').hide().attr('src', '//about:blank').end().empty(); - } catch (e) {} - }); - - if (coming.iframe.preload) { - F.showLoading(); - - iframe.one('load', function() { - $(this).data('ready', 1); - - // iOS will lose scrolling if we resize - if (!isTouch) { - $(this).bind('load.fb', F.update); - } - - // Without this trick: - // - iframe won't scroll on iOS devices - // - IE7 sometimes displays empty iframe - $(this).parents('.fancybox-wrap').width('100%').removeClass('fancybox-tmp').show(); - - F._afterLoad(); - }); - } - - coming.content = iframe.appendTo( coming.inner ); - - if (!coming.iframe.preload) { - F._afterLoad(); - } - }, - - _preloadImages: function() { - var group = F.group, - current = F.current, - len = group.length, - cnt = current.preload ? Math.min(current.preload, len - 1) : 0, - item, - i; - - for (i = 1; i <= cnt; i += 1) { - item = group[ (current.index + i ) % len ]; - - if (item.type === 'image' && item.href) { - new Image().src = item.href; - } - } - }, - - _afterLoad: function () { - var coming = F.coming, - previous = F.current, - placeholder = 'fancybox-placeholder', - current, - content, - type, - scrolling, - href, - embed; - - F.hideLoading(); - - if (!coming || F.isActive === false) { - return; - } - - if (false === F.trigger('afterLoad', coming, previous)) { - coming.wrap.stop(true).trigger('onReset').remove(); - - F.coming = null; - - return; - } - - if (previous) { - F.trigger('beforeChange', previous); - - previous.wrap.stop(true).removeClass('fancybox-opened') - .find('.fancybox-item, .fancybox-nav') - .remove(); - } - - F.unbindEvents(); - - current = coming; - content = coming.content; - type = coming.type; - scrolling = coming.scrolling; - - $.extend(F, { - wrap : current.wrap, - skin : current.skin, - outer : current.outer, - inner : current.inner, - current : current, - previous : previous - }); - - href = current.href; - - switch (type) { - case 'inline': - case 'ajax': - case 'html': - if (current.selector) { - content = $('
    ').html(content).find(current.selector); - - } else if (isQuery(content)) { - if (!content.data(placeholder)) { - content.data(placeholder, $('
    ').insertAfter( content ).hide() ); - } - - content = content.show().detach(); - - current.wrap.bind('onReset', function () { - if ($(this).find(content).length) { - content.hide().replaceAll( content.data(placeholder) ).data(placeholder, false); - } - }); - } - break; - - case 'image': - content = current.tpl.image.replace(/\{href\}/g, href); - break; - - case 'swf': - content = ''; - embed = ''; - - $.each(current.swf, function(name, val) { - content += ''; - embed += ' ' + name + '="' + val + '"'; - }); - - content += ''; - break; - } - - if (!(isQuery(content) && content.parent().is(current.inner))) { - current.inner.append( content ); - } - - // Give a chance for helpers or callbacks to update elements - F.trigger('beforeShow'); - - // Set scrolling before calculating dimensions - current.inner.css('overflow', scrolling === 'yes' ? 'scroll' : (scrolling === 'no' ? 'hidden' : scrolling)); - - // Set initial dimensions and start position - F._setDimension(); - - F.reposition(); - - F.isOpen = false; - F.coming = null; - - F.bindEvents(); - - if (!F.isOpened) { - $('.fancybox-wrap').not( current.wrap ).stop(true).trigger('onReset').remove(); - - } else if (previous.prevMethod) { - F.transitions[ previous.prevMethod ](); - } - - F.transitions[ F.isOpened ? current.nextMethod : current.openMethod ](); - - F._preloadImages(); - }, - - _setDimension: function () { - var viewport = F.getViewport(), - steps = 0, - canShrink = false, - canExpand = false, - wrap = F.wrap, - skin = F.skin, - inner = F.inner, - current = F.current, - width = current.width, - height = current.height, - minWidth = current.minWidth, - minHeight = current.minHeight, - maxWidth = current.maxWidth, - maxHeight = current.maxHeight, - scrolling = current.scrolling, - scrollOut = current.scrollOutside ? current.scrollbarWidth : 0, - margin = current.margin, - wMargin = getScalar(margin[1] + margin[3]), - hMargin = getScalar(margin[0] + margin[2]), - wPadding, - hPadding, - wSpace, - hSpace, - origWidth, - origHeight, - origMaxWidth, - origMaxHeight, - ratio, - width_, - height_, - maxWidth_, - maxHeight_, - iframe, - body; - - // Reset dimensions so we could re-check actual size - wrap.add(skin).add(inner).width('auto').height('auto').removeClass('fancybox-tmp'); - - wPadding = getScalar(skin.outerWidth(true) - skin.width()); - hPadding = getScalar(skin.outerHeight(true) - skin.height()); - - // Any space between content and viewport (margin, padding, border, title) - wSpace = wMargin + wPadding; - hSpace = hMargin + hPadding; - - origWidth = isPercentage(width) ? (viewport.w - wSpace) * getScalar(width) / 100 : width; - origHeight = isPercentage(height) ? (viewport.h - hSpace) * getScalar(height) / 100 : height; - - if (current.type === 'iframe') { - iframe = current.content; - - if (current.autoHeight && iframe && iframe.data('ready') === 1) { - try { - if (iframe[0].contentWindow.document.location) { - inner.width( origWidth ).height(9999); - - body = iframe.contents().find('body'); - - if (scrollOut) { - body.css('overflow-x', 'hidden'); - } - - origHeight = body.outerHeight(true); - } - - } catch (e) {} - } - - } else if (current.autoWidth || current.autoHeight) { - inner.addClass( 'fancybox-tmp' ); - - // Set width or height in case we need to calculate only one dimension - if (!current.autoWidth) { - inner.width( origWidth ); - } - - if (!current.autoHeight) { - inner.height( origHeight ); - } - - if (current.autoWidth) { - origWidth = inner.width(); - } - - if (current.autoHeight) { - origHeight = inner.height(); - } - - inner.removeClass( 'fancybox-tmp' ); - } - - width = getScalar( origWidth ); - height = getScalar( origHeight ); - - ratio = origWidth / origHeight; - - // Calculations for the content - minWidth = getScalar(isPercentage(minWidth) ? getScalar(minWidth, 'w') - wSpace : minWidth); - maxWidth = getScalar(isPercentage(maxWidth) ? getScalar(maxWidth, 'w') - wSpace : maxWidth); - - minHeight = getScalar(isPercentage(minHeight) ? getScalar(minHeight, 'h') - hSpace : minHeight); - maxHeight = getScalar(isPercentage(maxHeight) ? getScalar(maxHeight, 'h') - hSpace : maxHeight); - - // These will be used to determine if wrap can fit in the viewport - origMaxWidth = maxWidth; - origMaxHeight = maxHeight; - - if (current.fitToView) { - maxWidth = Math.min(viewport.w - wSpace, maxWidth); - maxHeight = Math.min(viewport.h - hSpace, maxHeight); - } - - maxWidth_ = viewport.w - wMargin; - maxHeight_ = viewport.h - hMargin; - - if (current.aspectRatio) { - if (width > maxWidth) { - width = maxWidth; - height = getScalar(width / ratio); - } - - if (height > maxHeight) { - height = maxHeight; - width = getScalar(height * ratio); - } - - if (width < minWidth) { - width = minWidth; - height = getScalar(width / ratio); - } - - if (height < minHeight) { - height = minHeight; - width = getScalar(height * ratio); - } - - } else { - width = Math.max(minWidth, Math.min(width, maxWidth)); - - if (current.autoHeight && current.type !== 'iframe') { - inner.width( width ); - - height = inner.height(); - } - - height = Math.max(minHeight, Math.min(height, maxHeight)); - } - - // Try to fit inside viewport (including the title) - if (current.fitToView) { - inner.width( width ).height( height ); - - wrap.width( width + wPadding ); - - // Real wrap dimensions - width_ = wrap.width(); - height_ = wrap.height(); - - if (current.aspectRatio) { - while ((width_ > maxWidth_ || height_ > maxHeight_) && width > minWidth && height > minHeight) { - if (steps++ > 19) { - break; - } - - height = Math.max(minHeight, Math.min(maxHeight, height - 10)); - width = getScalar(height * ratio); - - if (width < minWidth) { - width = minWidth; - height = getScalar(width / ratio); - } - - if (width > maxWidth) { - width = maxWidth; - height = getScalar(width / ratio); - } - - inner.width( width ).height( height ); - - wrap.width( width + wPadding ); - - width_ = wrap.width(); - height_ = wrap.height(); - } - - } else { - width = Math.max(minWidth, Math.min(width, width - (width_ - maxWidth_))); - height = Math.max(minHeight, Math.min(height, height - (height_ - maxHeight_))); - } - } - - if (scrollOut && scrolling === 'auto' && height < origHeight && (width + wPadding + scrollOut) < maxWidth_) { - width += scrollOut; - } - - inner.width( width ).height( height ); - - wrap.width( width + wPadding ); - - width_ = wrap.width(); - height_ = wrap.height(); - - canShrink = (width_ > maxWidth_ || height_ > maxHeight_) && width > minWidth && height > minHeight; - canExpand = current.aspectRatio ? (width < origMaxWidth && height < origMaxHeight && width < origWidth && height < origHeight) : ((width < origMaxWidth || height < origMaxHeight) && (width < origWidth || height < origHeight)); - - $.extend(current, { - dim : { - width : getValue( width_ ), - height : getValue( height_ ) - }, - origWidth : origWidth, - origHeight : origHeight, - canShrink : canShrink, - canExpand : canExpand, - wPadding : wPadding, - hPadding : hPadding, - wrapSpace : height_ - skin.outerHeight(true), - skinSpace : skin.height() - height - }); - - if (!iframe && current.autoHeight && height > minHeight && height < maxHeight && !canExpand) { - inner.height('auto'); - } - }, - - _getPosition: function (onlyAbsolute) { - var current = F.current, - viewport = F.getViewport(), - margin = current.margin, - width = F.wrap.width() + margin[1] + margin[3], - height = F.wrap.height() + margin[0] + margin[2], - rez = { - position: 'absolute', - top : margin[0], - left : margin[3] - }; - - if (current.autoCenter && current.fixed && !onlyAbsolute && height <= viewport.h && width <= viewport.w) { - rez.position = 'fixed'; - - } else if (!current.locked) { - rez.top += viewport.y; - rez.left += viewport.x; - } - - rez.top = getValue(Math.max(rez.top, rez.top + ((viewport.h - height) * current.topRatio))); - rez.left = getValue(Math.max(rez.left, rez.left + ((viewport.w - width) * current.leftRatio))); - - return rez; - }, - - _afterZoomIn: function () { - var current = F.current; - - if (!current) { - return; - } - - F.isOpen = F.isOpened = true; - - F.wrap.css('overflow', 'visible').addClass('fancybox-opened'); - - F.update(); - - // Assign a click event - if ( current.closeClick || (current.nextClick && F.group.length > 1) ) { - F.inner.css('cursor', 'pointer').bind('click.fb', function(e) { - if (!$(e.target).is('a') && !$(e.target).parent().is('a')) { - e.preventDefault(); - - F[ current.closeClick ? 'close' : 'next' ](); - } - }); - } - - // Create a close button - if (current.closeBtn) { - $(current.tpl.closeBtn).appendTo(F.skin).bind('click.fb', function(e) { - e.preventDefault(); - - F.close(); - }); - } - - // Create navigation arrows - if (current.arrows && F.group.length > 1) { - if (current.loop || current.index > 0) { - $(current.tpl.prev).appendTo(F.outer).bind('click.fb', F.prev); - } - - if (current.loop || current.index < F.group.length - 1) { - $(current.tpl.next).appendTo(F.outer).bind('click.fb', F.next); - } - } - - F.trigger('afterShow'); - - // Stop the slideshow if this is the last item - if (!current.loop && current.index === current.group.length - 1) { - - F.play( false ); - - } else if (F.opts.autoPlay && !F.player.isActive) { - F.opts.autoPlay = false; - - F.play(true); - } - }, - - _afterZoomOut: function ( obj ) { - obj = obj || F.current; - - $('.fancybox-wrap').trigger('onReset').remove(); - - $.extend(F, { - group : {}, - opts : {}, - router : false, - current : null, - isActive : false, - isOpened : false, - isOpen : false, - isClosing : false, - wrap : null, - skin : null, - outer : null, - inner : null - }); - - F.trigger('afterClose', obj); - } - }); - - /* - * Default transitions - */ - - F.transitions = { - getOrigPosition: function () { - var current = F.current, - element = current.element, - orig = current.orig, - pos = {}, - width = 50, - height = 50, - hPadding = current.hPadding, - wPadding = current.wPadding, - viewport = F.getViewport(); - - if (!orig && current.isDom && element.is(':visible')) { - orig = element.find('img:first'); - - if (!orig.length) { - orig = element; - } - } - - if (isQuery(orig)) { - pos = orig.offset(); - - if (orig.is('img')) { - width = orig.outerWidth(); - height = orig.outerHeight(); - } - - } else { - pos.top = viewport.y + (viewport.h - height) * current.topRatio; - pos.left = viewport.x + (viewport.w - width) * current.leftRatio; - } - - if (F.wrap.css('position') === 'fixed' || current.locked) { - pos.top -= viewport.y; - pos.left -= viewport.x; - } - - pos = { - top : getValue(pos.top - hPadding * current.topRatio), - left : getValue(pos.left - wPadding * current.leftRatio), - width : getValue(width + wPadding), - height : getValue(height + hPadding) - }; - - return pos; - }, - - step: function (now, fx) { - var ratio, - padding, - value, - prop = fx.prop, - current = F.current, - wrapSpace = current.wrapSpace, - skinSpace = current.skinSpace; - - if (prop === 'width' || prop === 'height') { - ratio = fx.end === fx.start ? 1 : (now - fx.start) / (fx.end - fx.start); - - if (F.isClosing) { - ratio = 1 - ratio; - } - - padding = prop === 'width' ? current.wPadding : current.hPadding; - value = now - padding; - - F.skin[ prop ]( getScalar( prop === 'width' ? value : value - (wrapSpace * ratio) ) ); - F.inner[ prop ]( getScalar( prop === 'width' ? value : value - (wrapSpace * ratio) - (skinSpace * ratio) ) ); - } - }, - - zoomIn: function () { - var current = F.current, - startPos = current.pos, - effect = current.openEffect, - elastic = effect === 'elastic', - endPos = $.extend({opacity : 1}, startPos); - - // Remove "position" property that breaks older IE - delete endPos.position; - - if (elastic) { - startPos = this.getOrigPosition(); - - if (current.openOpacity) { - startPos.opacity = 0.1; - } - - } else if (effect === 'fade') { - startPos.opacity = 0.1; - } - - F.wrap.css(startPos).animate(endPos, { - duration : effect === 'none' ? 0 : current.openSpeed, - easing : current.openEasing, - step : elastic ? this.step : null, - complete : F._afterZoomIn - }); - }, - - zoomOut: function () { - var current = F.current, - effect = current.closeEffect, - elastic = effect === 'elastic', - endPos = {opacity : 0.1}; - - if (elastic) { - endPos = this.getOrigPosition(); - - if (current.closeOpacity) { - endPos.opacity = 0.1; - } - } - - F.wrap.animate(endPos, { - duration : effect === 'none' ? 0 : current.closeSpeed, - easing : current.closeEasing, - step : elastic ? this.step : null, - complete : F._afterZoomOut - }); - }, - - changeIn: function () { - var current = F.current, - effect = current.nextEffect, - startPos = current.pos, - endPos = { opacity : 1 }, - direction = F.direction, - distance = 200, - field; - - startPos.opacity = 0.1; - - if (effect === 'elastic') { - field = direction === 'down' || direction === 'up' ? 'top' : 'left'; - - if (direction === 'down' || direction === 'right') { - startPos[ field ] = getValue(getScalar(startPos[ field ]) - distance); - endPos[ field ] = '+=' + distance + 'px'; - - } else { - startPos[ field ] = getValue(getScalar(startPos[ field ]) + distance); - endPos[ field ] = '-=' + distance + 'px'; - } - } - - // Workaround for http://bugs.jquery.com/ticket/12273 - if (effect === 'none') { - F._afterZoomIn(); - - } else { - F.wrap.css(startPos).animate(endPos, { - duration : current.nextSpeed, - easing : current.nextEasing, - complete : F._afterZoomIn - }); - } - }, - - changeOut: function () { - var previous = F.previous, - effect = previous.prevEffect, - endPos = { opacity : 0.1 }, - direction = F.direction, - distance = 200; - - if (effect === 'elastic') { - endPos[ direction === 'down' || direction === 'up' ? 'top' : 'left' ] = ( direction === 'up' || direction === 'left' ? '-' : '+' ) + '=' + distance + 'px'; - } - - previous.wrap.animate(endPos, { - duration : effect === 'none' ? 0 : previous.prevSpeed, - easing : previous.prevEasing, - complete : function () { - $(this).trigger('onReset').remove(); - } - }); - } - }; - - /* - * Overlay helper - */ - - F.helpers.overlay = { - defaults : { - closeClick : true, // if true, fancyBox will be closed when user clicks on the overlay - speedOut : 200, // duration of fadeOut animation - showEarly : true, // indicates if should be opened immediately or wait until the content is ready - css : {}, // custom CSS properties - locked : !isTouch, // if true, the content will be locked into overlay - fixed : true // if false, the overlay CSS position property will not be set to "fixed" - }, - - overlay : null, // current handle - fixed : false, // indicates if the overlay has position "fixed" - el : $('html'), // element that contains "the lock" - - // Public methods - create : function(opts) { - var parent; - - opts = $.extend({}, this.defaults, opts); - - if (this.overlay) { - this.close(); - } - - parent = F.coming ? F.coming.parent : opts.parent; - - this.overlay = $('
    ').appendTo( parent && parent.length ? parent : 'body' ); - this.fixed = false; - - if (opts.fixed && F.defaults.fixed) { - this.overlay.addClass('fancybox-overlay-fixed'); - - this.fixed = true; - } - }, - - open : function(opts) { - var that = this; - - opts = $.extend({}, this.defaults, opts); - - if (this.overlay) { - this.overlay.unbind('.overlay').width('auto').height('auto'); - - } else { - this.create(opts); - } - - if (!this.fixed) { - W.bind('resize.overlay', $.proxy( this.update, this) ); - - this.update(); - } - - if (opts.closeClick) { - this.overlay.bind('click.overlay', function(e) { - if ($(e.target).hasClass('fancybox-overlay')) { - if (F.isActive) { - F.close(); - } else { - that.close(); - } - - return false; - } - }); - } - - this.overlay.css( opts.css ).show(); - }, - - close : function() { - W.unbind('resize.overlay'); - - if (this.el.hasClass('fancybox-lock')) { - $('.fancybox-margin').removeClass('fancybox-margin'); - - this.el.removeClass('fancybox-lock'); - - W.scrollTop( this.scrollV ).scrollLeft( this.scrollH ); - } - - $('.fancybox-overlay').remove().hide(); - - $.extend(this, { - overlay : null, - fixed : false - }); - }, - - // Private, callbacks - - update : function () { - var width = '100%', offsetWidth; - - // Reset width/height so it will not mess - this.overlay.width(width).height('100%'); - - // jQuery does not return reliable result for IE - if (IE) { - offsetWidth = Math.max(document.documentElement.offsetWidth, document.body.offsetWidth); - - if (D.width() > offsetWidth) { - width = D.width(); - } - - } else if (D.width() > W.width()) { - width = D.width(); - } - - this.overlay.width(width).height(D.height()); - }, - - // This is where we can manipulate DOM, because later it would cause iframes to reload - onReady : function (opts, obj) { - var overlay = this.overlay; - - $('.fancybox-overlay').stop(true, true); - - if (!overlay) { - this.create(opts); - } - - if (opts.locked && this.fixed && obj.fixed) { - obj.locked = this.overlay.append( obj.wrap ); - obj.fixed = false; - } - - if (opts.showEarly === true) { - this.beforeShow.apply(this, arguments); - } - }, - - beforeShow : function(opts, obj) { - if (obj.locked && !this.el.hasClass('fancybox-lock')) { - if (this.fixPosition !== false) { - $('*:not(object)').filter(function(){ - return ($(this).css('position') === 'fixed' && !$(this).hasClass("fancybox-overlay") && !$(this).hasClass("fancybox-wrap") ); - }).addClass('fancybox-margin'); - } - - this.el.addClass('fancybox-margin'); - - this.scrollV = W.scrollTop(); - this.scrollH = W.scrollLeft(); - - this.el.addClass('fancybox-lock'); - - W.scrollTop( this.scrollV ).scrollLeft( this.scrollH ); - } - - this.open(opts); - }, - - onUpdate : function() { - if (!this.fixed) { - this.update(); - } - }, - - afterClose: function (opts) { - // Remove overlay if exists and fancyBox is not opening - // (e.g., it is not being open using afterClose callback) - if (this.overlay && !F.coming) { - this.overlay.fadeOut(opts.speedOut, $.proxy( this.close, this )); - } - } - }; - - /* - * Title helper - */ - - F.helpers.title = { - defaults : { - type : 'float', // 'float', 'inside', 'outside' or 'over', - position : 'bottom' // 'top' or 'bottom' - }, - - beforeShow: function (opts) { - var current = F.current, - text = current.title, - type = opts.type, - title, - target; - - if ($.isFunction(text)) { - text = text.call(current.element, current); - } - - if (!isString(text) || $.trim(text) === '') { - return; - } - - title = $('
    ' + text + '
    '); - - switch (type) { - case 'inside': - target = F.skin; - break; - - case 'outside': - target = F.wrap; - break; - - case 'over': - target = F.inner; - break; - - default: // 'float' - target = F.skin; - - title.appendTo('body'); - - if (IE) { - title.width( title.width() ); - } - - title.wrapInner(''); - - //Increase bottom margin so this title will also fit into viewport - F.current.margin[2] += Math.abs( getScalar(title.css('margin-bottom')) ); - break; - } - - title[ (opts.position === 'top' ? 'prependTo' : 'appendTo') ](target); - } - }; - - // jQuery plugin initialization - $.fn.fancybox = function (options) { - var index, - that = $(this), - selector = this.selector || '', - run = function(e) { - var what = $(this).blur(), idx = index, relType, relVal; - - if (!(e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) && !what.is('.fancybox-wrap')) { - relType = options.groupAttr || 'data-fancybox-group'; - relVal = what.attr(relType); - - if (!relVal) { - relType = 'rel'; - relVal = what.get(0)[ relType ]; - } - - if (relVal && relVal !== '' && relVal !== 'nofollow') { - what = selector.length ? $(selector) : that; - what = what.filter('[' + relType + '="' + relVal + '"]'); - idx = what.index(this); - } - - options.index = idx; - - // Stop an event from bubbling if everything is fine - if (F.open(what, options) !== false) { - e.preventDefault(); - } - } - }; - - options = options || {}; - index = options.index || 0; - - if (!selector || options.live === false) { - that.unbind('click.fb-start').bind('click.fb-start', run); - - } else { - D.undelegate(selector, 'click.fb-start').delegate(selector + ":not('.fancybox-item, .fancybox-nav')", 'click.fb-start', run); - } - - this.filter('[data-fancybox-start=1]').trigger('click'); - - return this; - }; - - // Tests that need a body at doc ready - D.ready(function() { - var w1, w2; - - if ( $.scrollbarWidth === undefined ) { - // http://benalman.com/projects/jquery-misc-plugins/#scrollbarwidth - $.scrollbarWidth = function() { - var parent = $('
    ').appendTo('body'), - child = parent.children(), - width = child.innerWidth() - child.height( 99 ).innerWidth(); - - parent.remove(); - - return width; - }; - } - - if ( $.support.fixedPosition === undefined ) { - $.support.fixedPosition = (function() { - var elem = $('
    ').appendTo('body'), - fixed = ( elem[0].offsetTop === 20 || elem[0].offsetTop === 15 ); - - elem.remove(); - - return fixed; - }()); - } - - $.extend(F.defaults, { - scrollbarWidth : $.scrollbarWidth(), - fixed : $.support.fixedPosition, - parent : $('body') - }); - - //Get real width of page scroll-bar - w1 = $(window).width(); - - H.addClass('fancybox-lock-test'); - - w2 = $(window).width(); - - H.removeClass('fancybox-lock-test'); - - $("").appendTo("head"); - }); - -}(window, document, jQuery)); diff --git a/modules/fancybox/source/jquery.fancybox.pack.js b/modules/fancybox/source/jquery.fancybox.pack.js deleted file mode 100755 index 2db12808..00000000 --- a/modules/fancybox/source/jquery.fancybox.pack.js +++ /dev/null @@ -1,46 +0,0 @@ -/*! fancyBox v2.1.5 fancyapps.com | fancyapps.com/fancybox/#license */ -(function(s,H,f,w){var K=f("html"),q=f(s),p=f(H),b=f.fancybox=function(){b.open.apply(this,arguments)},J=navigator.userAgent.match(/msie/i),C=null,t=H.createTouch!==w,u=function(a){return a&&a.hasOwnProperty&&a instanceof f},r=function(a){return a&&"string"===f.type(a)},F=function(a){return r(a)&&0
    ',image:'',iframe:'",error:'

    The requested content cannot be loaded.
    Please try again later.

    ',closeBtn:'',next:'',prev:''},openEffect:"fade",openSpeed:250,openEasing:"swing",openOpacity:!0, -openMethod:"zoomIn",closeEffect:"fade",closeSpeed:250,closeEasing:"swing",closeOpacity:!0,closeMethod:"zoomOut",nextEffect:"elastic",nextSpeed:250,nextEasing:"swing",nextMethod:"changeIn",prevEffect:"elastic",prevSpeed:250,prevEasing:"swing",prevMethod:"changeOut",helpers:{overlay:!0,title:!0},onCancel:f.noop,beforeLoad:f.noop,afterLoad:f.noop,beforeShow:f.noop,afterShow:f.noop,beforeChange:f.noop,beforeClose:f.noop,afterClose:f.noop},group:{},opts:{},previous:null,coming:null,current:null,isActive:!1, -isOpen:!1,isOpened:!1,wrap:null,skin:null,outer:null,inner:null,player:{timer:null,isActive:!1},ajaxLoad:null,imgPreload:null,transitions:{},helpers:{},open:function(a,d){if(a&&(f.isPlainObject(d)||(d={}),!1!==b.close(!0)))return f.isArray(a)||(a=u(a)?f(a).get():[a]),f.each(a,function(e,c){var l={},g,h,k,n,m;"object"===f.type(c)&&(c.nodeType&&(c=f(c)),u(c)?(l={href:c.data("fancybox-href")||c.attr("href"),title:f("
    ").text(c.data("fancybox-title")||c.attr("title")).html(),isDom:!0,element:c}, -f.metadata&&f.extend(!0,l,c.metadata())):l=c);g=d.href||l.href||(r(c)?c:null);h=d.title!==w?d.title:l.title||"";n=(k=d.content||l.content)?"html":d.type||l.type;!n&&l.isDom&&(n=c.data("fancybox-type"),n||(n=(n=c.prop("class").match(/fancybox\.(\w+)/))?n[1]:null));r(g)&&(n||(b.isImage(g)?n="image":b.isSWF(g)?n="swf":"#"===g.charAt(0)?n="inline":r(c)&&(n="html",k=c)),"ajax"===n&&(m=g.split(/\s+/,2),g=m.shift(),m=m.shift()));k||("inline"===n?g?k=f(r(g)?g.replace(/.*(?=#[^\s]+$)/,""):g):l.isDom&&(k=c): -"html"===n?k=g:n||g||!l.isDom||(n="inline",k=c));f.extend(l,{href:g,type:n,content:k,title:h,selector:m});a[e]=l}),b.opts=f.extend(!0,{},b.defaults,d),d.keys!==w&&(b.opts.keys=d.keys?f.extend({},b.defaults.keys,d.keys):!1),b.group=a,b._start(b.opts.index)},cancel:function(){var a=b.coming;a&&!1===b.trigger("onCancel")||(b.hideLoading(),a&&(b.ajaxLoad&&b.ajaxLoad.abort(),b.ajaxLoad=null,b.imgPreload&&(b.imgPreload.onload=b.imgPreload.onerror=null),a.wrap&&a.wrap.stop(!0,!0).trigger("onReset").remove(), -b.coming=null,b.current||b._afterZoomOut(a)))},close:function(a){b.cancel();!1!==b.trigger("beforeClose")&&(b.unbindEvents(),b.isActive&&(b.isOpen&&!0!==a?(b.isOpen=b.isOpened=!1,b.isClosing=!0,f(".fancybox-item, .fancybox-nav").remove(),b.wrap.stop(!0,!0).removeClass("fancybox-opened"),b.transitions[b.current.closeMethod]()):(f(".fancybox-wrap").stop(!0).trigger("onReset").remove(),b._afterZoomOut())))},play:function(a){var d=function(){clearTimeout(b.player.timer)},e=function(){d();b.current&&b.player.isActive&& -(b.player.timer=setTimeout(b.next,b.current.playSpeed))},c=function(){d();p.unbind(".player");b.player.isActive=!1;b.trigger("onPlayEnd")};!0===a||!b.player.isActive&&!1!==a?b.current&&(b.current.loop||b.current.index=c.index?"next":"prev"],b.router=e||"jumpto",c.loop&&(0>a&&(a=c.group.length+a%c.group.length),a%=c.group.length),c.group[a]!==w&&(b.cancel(),b._start(a)))},reposition:function(a,d){var e=b.current,c=e?e.wrap:null,l;c&&(l=b._getPosition(d),a&&"scroll"===a.type?(delete l.position,c.stop(!0,!0).animate(l,200)):(c.css(l),e.pos=f.extend({},e.dim,l)))}, -update:function(a){var d=a&&a.originalEvent&&a.originalEvent.type,e=!d||"orientationchange"===d;e&&(clearTimeout(C),C=null);b.isOpen&&!C&&(C=setTimeout(function(){var c=b.current;c&&!b.isClosing&&(b.wrap.removeClass("fancybox-tmp"),(e||"load"===d||"resize"===d&&c.autoResize)&&b._setDimension(),"scroll"===d&&c.canShrink||b.reposition(a),b.trigger("onUpdate"),C=null)},e&&!t?0:300))},toggle:function(a){b.isOpen&&(b.current.fitToView="boolean"===f.type(a)?a:!b.current.fitToView,t&&(b.wrap.removeAttr("style").addClass("fancybox-tmp"), -b.trigger("onUpdate")),b.update())},hideLoading:function(){p.unbind(".loading");f("#fancybox-loading").remove()},showLoading:function(){var a,d;b.hideLoading();a=f('
    ').click(b.cancel).appendTo("body");p.bind("keydown.loading",function(a){27===(a.which||a.keyCode)&&(a.preventDefault(),b.cancel())});b.defaults.fixed||(d=b.getViewport(),a.css({position:"absolute",top:0.5*d.h+d.y,left:0.5*d.w+d.x}));b.trigger("onLoading")},getViewport:function(){var a=b.current&& -b.current.locked||!1,d={x:q.scrollLeft(),y:q.scrollTop()};a&&a.length?(d.w=a[0].clientWidth,d.h=a[0].clientHeight):(d.w=t&&s.innerWidth?s.innerWidth:q.width(),d.h=t&&s.innerHeight?s.innerHeight:q.height());return d},unbindEvents:function(){b.wrap&&u(b.wrap)&&b.wrap.unbind(".fb");p.unbind(".fb");q.unbind(".fb")},bindEvents:function(){var a=b.current,d;a&&(q.bind("orientationchange.fb"+(t?"":" resize.fb")+(a.autoCenter&&!a.locked?" scroll.fb":""),b.update),(d=a.keys)&&p.bind("keydown.fb",function(e){var c= -e.which||e.keyCode,l=e.target||e.srcElement;if(27===c&&b.coming)return!1;e.ctrlKey||e.altKey||e.shiftKey||e.metaKey||l&&(l.type||f(l).is("[contenteditable]"))||f.each(d,function(d,l){if(1h[0].clientWidth||h[0].clientHeight&&h[0].scrollHeight>h[0].clientHeight),h=f(h).parent();0!==c&&!k&&1g||0>l)&&b.next(0>g?"up":"right"),d.preventDefault())}))},trigger:function(a,d){var e,c=d||b.coming||b.current;if(c){f.isFunction(c[a])&&(e=c[a].apply(c,Array.prototype.slice.call(arguments,1)));if(!1===e)return!1;c.helpers&&f.each(c.helpers,function(d,e){if(e&& -b.helpers[d]&&f.isFunction(b.helpers[d][a]))b.helpers[d][a](f.extend(!0,{},b.helpers[d].defaults,e),c)})}p.trigger(a)},isImage:function(a){return r(a)&&a.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i)},isSWF:function(a){return r(a)&&a.match(/\.(swf)((\?|#).*)?$/i)},_start:function(a){var d={},e,c;a=m(a);e=b.group[a]||null;if(!e)return!1;d=f.extend(!0,{},b.opts,e);e=d.margin;c=d.padding;"number"===f.type(e)&&(d.margin=[e,e,e,e]);"number"===f.type(c)&&(d.padding=[c,c, -c,c]);d.modal&&f.extend(!0,d,{closeBtn:!1,closeClick:!1,nextClick:!1,arrows:!1,mouseWheel:!1,keys:null,helpers:{overlay:{closeClick:!1}}});d.autoSize&&(d.autoWidth=d.autoHeight=!0);"auto"===d.width&&(d.autoWidth=!0);"auto"===d.height&&(d.autoHeight=!0);d.group=b.group;d.index=a;b.coming=d;if(!1===b.trigger("beforeLoad"))b.coming=null;else{c=d.type;e=d.href;if(!c)return b.coming=null,b.current&&b.router&&"jumpto"!==b.router?(b.current.index=a,b[b.router](b.direction)):!1;b.isActive=!0;if("image"=== -c||"swf"===c)d.autoHeight=d.autoWidth=!1,d.scrolling="visible";"image"===c&&(d.aspectRatio=!0);"iframe"===c&&t&&(d.scrolling="scroll");d.wrap=f(d.tpl.wrap).addClass("fancybox-"+(t?"mobile":"desktop")+" fancybox-type-"+c+" fancybox-tmp "+d.wrapCSS).appendTo(d.parent||"body");f.extend(d,{skin:f(".fancybox-skin",d.wrap),outer:f(".fancybox-outer",d.wrap),inner:f(".fancybox-inner",d.wrap)});f.each(["Top","Right","Bottom","Left"],function(a,b){d.skin.css("padding"+b,x(d.padding[a]))});b.trigger("onReady"); -if("inline"===c||"html"===c){if(!d.content||!d.content.length)return b._error("content")}else if(!e)return b._error("href");"image"===c?b._loadImage():"ajax"===c?b._loadAjax():"iframe"===c?b._loadIframe():b._afterLoad()}},_error:function(a){f.extend(b.coming,{type:"html",autoWidth:!0,autoHeight:!0,minWidth:0,minHeight:0,scrolling:"no",hasError:a,content:b.coming.tpl.error});b._afterLoad()},_loadImage:function(){var a=b.imgPreload=new Image;a.onload=function(){this.onload=this.onerror=null;b.coming.width= -this.width/b.opts.pixelRatio;b.coming.height=this.height/b.opts.pixelRatio;b._afterLoad()};a.onerror=function(){this.onload=this.onerror=null;b._error("image")};a.src=b.coming.href;!0!==a.complete&&b.showLoading()},_loadAjax:function(){var a=b.coming;b.showLoading();b.ajaxLoad=f.ajax(f.extend({},a.ajax,{url:a.href,error:function(a,e){b.coming&&"abort"!==e?b._error("ajax",a):b.hideLoading()},success:function(d,e){"success"===e&&(a.content=d,b._afterLoad())}}))},_loadIframe:function(){var a=b.coming, -d=f(a.tpl.iframe.replace(/\{rnd\}/g,(new Date).getTime())).attr("scrolling",t?"auto":a.iframe.scrolling).attr("src",a.href);f(a.wrap).bind("onReset",function(){try{f(this).find("iframe").hide().attr("src","//about:blank").end().empty()}catch(a){}});a.iframe.preload&&(b.showLoading(),d.one("load",function(){f(this).data("ready",1);t||f(this).bind("load.fb",b.update);f(this).parents(".fancybox-wrap").width("100%").removeClass("fancybox-tmp").show();b._afterLoad()}));a.content=d.appendTo(a.inner);a.iframe.preload|| -b._afterLoad()},_preloadImages:function(){var a=b.group,d=b.current,e=a.length,c=d.preload?Math.min(d.preload,e-1):0,f,g;for(g=1;g<=c;g+=1)f=a[(d.index+g)%e],"image"===f.type&&f.href&&((new Image).src=f.href)},_afterLoad:function(){var a=b.coming,d=b.current,e,c,l,g,h;b.hideLoading();if(a&&!1!==b.isActive)if(!1===b.trigger("afterLoad",a,d))a.wrap.stop(!0).trigger("onReset").remove(),b.coming=null;else{d&&(b.trigger("beforeChange",d),d.wrap.stop(!0).removeClass("fancybox-opened").find(".fancybox-item, .fancybox-nav").remove()); -b.unbindEvents();e=a.content;c=a.type;l=a.scrolling;f.extend(b,{wrap:a.wrap,skin:a.skin,outer:a.outer,inner:a.inner,current:a,previous:d});g=a.href;switch(c){case "inline":case "ajax":case "html":a.selector?e=f("
    ").html(e).find(a.selector):u(e)&&(e.data("fancybox-placeholder")||e.data("fancybox-placeholder",f('
    ').insertAfter(e).hide()),e=e.show().detach(),a.wrap.bind("onReset",function(){f(this).find(e).length&&e.hide().replaceAll(e.data("fancybox-placeholder")).data("fancybox-placeholder", -!1)}));break;case "image":e=a.tpl.image.replace(/\{href\}/g,g);break;case "swf":e='',h="",f.each(a.swf,function(a,b){e+='';h+=" "+a+'="'+b+'"'}),e+='"}u(e)&&e.parent().is(a.inner)||a.inner.append(e);b.trigger("beforeShow"); -a.inner.css("overflow","yes"===l?"scroll":"no"===l?"hidden":l);b._setDimension();b.reposition();b.isOpen=!1;b.coming=null;b.bindEvents();if(!b.isOpened)f(".fancybox-wrap").not(a.wrap).stop(!0).trigger("onReset").remove();else if(d.prevMethod)b.transitions[d.prevMethod]();b.transitions[b.isOpened?a.nextMethod:a.openMethod]();b._preloadImages()}},_setDimension:function(){var a=b.getViewport(),d=0,e=!1,c=!1,e=b.wrap,l=b.skin,g=b.inner,h=b.current,c=h.width,k=h.height,n=h.minWidth,v=h.minHeight,p=h.maxWidth, -q=h.maxHeight,t=h.scrolling,r=h.scrollOutside?h.scrollbarWidth:0,y=h.margin,z=m(y[1]+y[3]),s=m(y[0]+y[2]),w,A,u,D,B,G,C,E,I;e.add(l).add(g).width("auto").height("auto").removeClass("fancybox-tmp");y=m(l.outerWidth(!0)-l.width());w=m(l.outerHeight(!0)-l.height());A=z+y;u=s+w;D=F(c)?(a.w-A)*m(c)/100:c;B=F(k)?(a.h-u)*m(k)/100:k;if("iframe"===h.type){if(I=h.content,h.autoHeight&&1===I.data("ready"))try{I[0].contentWindow.document.location&&(g.width(D).height(9999),G=I.contents().find("body"),r&&G.css("overflow-x", -"hidden"),B=G.outerHeight(!0))}catch(H){}}else if(h.autoWidth||h.autoHeight)g.addClass("fancybox-tmp"),h.autoWidth||g.width(D),h.autoHeight||g.height(B),h.autoWidth&&(D=g.width()),h.autoHeight&&(B=g.height()),g.removeClass("fancybox-tmp");c=m(D);k=m(B);E=D/B;n=m(F(n)?m(n,"w")-A:n);p=m(F(p)?m(p,"w")-A:p);v=m(F(v)?m(v,"h")-u:v);q=m(F(q)?m(q,"h")-u:q);G=p;C=q;h.fitToView&&(p=Math.min(a.w-A,p),q=Math.min(a.h-u,q));A=a.w-z;s=a.h-s;h.aspectRatio?(c>p&&(c=p,k=m(c/E)),k>q&&(k=q,c=m(k*E)),cA||z>s)&&c>n&&k>v&&!(19p&&(c=p,k=m(c/E)),g.width(c).height(k),e.width(c+y),a=e.width(),z=e.height();else c=Math.max(n,Math.min(c,c-(a-A))),k=Math.max(v,Math.min(k,k-(z-s)));r&&"auto"===t&&kA||z>s)&&c>n&&k>v;c=h.aspectRatio?cv&&k
    ').appendTo(d&&d.lenth?d:"body");this.fixed=!1;a.fixed&&b.defaults.fixed&&(this.overlay.addClass("fancybox-overlay-fixed"),this.fixed=!0)},open:function(a){var d=this;a=f.extend({},this.defaults,a);this.overlay?this.overlay.unbind(".overlay").width("auto").height("auto"):this.create(a);this.fixed||(q.bind("resize.overlay",f.proxy(this.update,this)),this.update());a.closeClick&&this.overlay.bind("click.overlay", -function(a){if(f(a.target).hasClass("fancybox-overlay"))return b.isActive?b.close():d.close(),!1});this.overlay.css(a.css).show()},close:function(){q.unbind("resize.overlay");this.el.hasClass("fancybox-lock")&&(f(".fancybox-margin").removeClass("fancybox-margin"),this.el.removeClass("fancybox-lock"),q.scrollTop(this.scrollV).scrollLeft(this.scrollH));f(".fancybox-overlay").remove().hide();f.extend(this,{overlay:null,fixed:!1})},update:function(){var a="100%",b;this.overlay.width(a).height("100%"); -J?(b=Math.max(H.documentElement.offsetWidth,H.body.offsetWidth),p.width()>b&&(a=p.width())):p.width()>q.width()&&(a=p.width());this.overlay.width(a).height(p.height())},onReady:function(a,b){var e=this.overlay;f(".fancybox-overlay").stop(!0,!0);e||this.create(a);a.locked&&this.fixed&&b.fixed&&(b.locked=this.overlay.append(b.wrap),b.fixed=!1);!0===a.showEarly&&this.beforeShow.apply(this,arguments)},beforeShow:function(a,b){b.locked&&!this.el.hasClass("fancybox-lock")&&(!1!==this.fixPosition&&f("*").filter(function(){return"fixed"=== -f(this).css("position")&&!f(this).hasClass("fancybox-overlay")&&!f(this).hasClass("fancybox-wrap")}).addClass("fancybox-margin"),this.el.addClass("fancybox-margin"),this.scrollV=q.scrollTop(),this.scrollH=q.scrollLeft(),this.el.addClass("fancybox-lock"),q.scrollTop(this.scrollV).scrollLeft(this.scrollH));this.open(a)},onUpdate:function(){this.fixed||this.update()},afterClose:function(a){this.overlay&&!b.coming&&this.overlay.fadeOut(a.speedOut,f.proxy(this.close,this))}};b.helpers.title={defaults:{type:"float", -position:"bottom"},beforeShow:function(a){var d=b.current,e=d.title,c=a.type;f.isFunction(e)&&(e=e.call(d.element,d));if(r(e)&&""!==f.trim(e)){d=f('
    '+e+"
    ");switch(c){case "inside":c=b.skin;break;case "outside":c=b.wrap;break;case "over":c=b.inner;break;default:c=b.skin,d.appendTo("body"),J&&d.width(d.width()),d.wrapInner(''),b.current.margin[2]+=Math.abs(m(d.css("margin-bottom")))}d["top"===a.position?"prependTo": -"appendTo"](c)}}};f.fn.fancybox=function(a){var d,e=f(this),c=this.selector||"",l=function(g){var h=f(this).blur(),k=d,l,m;g.ctrlKey||g.altKey||g.shiftKey||g.metaKey||h.is(".fancybox-wrap")||(l=a.groupAttr||"data-fancybox-group",m=h.attr(l),m||(l="rel",m=h.get(0)[l]),m&&""!==m&&"nofollow"!==m&&(h=c.length?f(c):e,h=h.filter("["+l+'="'+m+'"]'),k=h.index(this)),a.index=k,!1!==b.open(h,a)&&g.preventDefault())};a=a||{};d=a.index||0;c&&!1!==a.live?p.undelegate(c,"click.fb-start").delegate(c+":not('.fancybox-item, .fancybox-nav')", -"click.fb-start",l):e.unbind("click.fb-start").bind("click.fb-start",l);this.filter("[data-fancybox-start=1]").trigger("click");return this};p.ready(function(){var a,d;f.scrollbarWidth===w&&(f.scrollbarWidth=function(){var a=f('
    ').appendTo("body"),b=a.children(),b=b.innerWidth()-b.height(99).innerWidth();a.remove();return b});f.support.fixedPosition===w&&(f.support.fixedPosition=function(){var a=f('
    ').appendTo("body"), -b=20===a[0].offsetTop||15===a[0].offsetTop;a.remove();return b}());f.extend(b.defaults,{scrollbarWidth:f.scrollbarWidth(),fixed:f.support.fixedPosition,parent:f("body")});a=f(s).width();K.addClass("fancybox-lock-test");d=f(s).width();K.removeClass("fancybox-lock-test");f("").appendTo("head")})})(window,document,jQuery); \ No newline at end of file diff --git a/modules/jquery/.bower.json b/modules/jquery/.bower.json deleted file mode 100644 index 69272f92..00000000 --- a/modules/jquery/.bower.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "jquery", - "main": "dist/jquery.js", - "license": "MIT", - "ignore": [ - "package.json" - ], - "keywords": [ - "jquery", - "javascript", - "browser", - "library" - ], - "homepage": "https://github.com/jquery/jquery-dist", - "version": "3.3.1", - "_release": "3.3.1", - "_resolution": { - "type": "version", - "tag": "3.3.1", - "commit": "9e8ec3d10fad04748176144f108d7355662ae75e" - }, - "_source": "https://github.com/jquery/jquery-dist.git", - "_target": "3.3.1", - "_originalSource": "jquery" -} \ No newline at end of file diff --git a/modules/jquery/AUTHORS.txt b/modules/jquery/AUTHORS.txt deleted file mode 100644 index 4bb5da15..00000000 --- a/modules/jquery/AUTHORS.txt +++ /dev/null @@ -1,313 +0,0 @@ -Authors ordered by first contribution. - -John Resig -Gilles van den Hoven -Michael Geary -Stefan Petre -Yehuda Katz -Corey Jewett -Klaus Hartl -Franck Marcia -Jörn Zaefferer -Paul Bakaus -Brandon Aaron -Mike Alsup -Dave Methvin -Ed Engelhardt -Sean Catchpole -Paul Mclanahan -David Serduke -Richard D. Worth -Scott González -Ariel Flesler -Jon Evans -TJ Holowaychuk -Michael Bensoussan -Robert Katić -Louis-Rémi Babé -Earle Castledine -Damian Janowski -Rich Dougherty -Kim Dalsgaard -Andrea Giammarchi -Mark Gibson -Karl Swedberg -Justin Meyer -Ben Alman -James Padolsey -David Petersen -Batiste Bieler -Alexander Farkas -Rick Waldron -Filipe Fortes -Neeraj Singh -Paul Irish -Iraê Carvalho -Matt Curry -Michael Monteleone -Noah Sloan -Tom Viner -Douglas Neiner -Adam J. Sontag -Dave Reed -Ralph Whitbeck -Carl Fürstenberg -Jacob Wright -J. Ryan Stinnett -unknown -temp01 -Heungsub Lee -Colin Snover -Ryan W Tenney -Pinhook -Ron Otten -Jephte Clain -Anton Matzneller -Alex Sexton -Dan Heberden -Henri Wiechers -Russell Holbrook -Julian Aubourg -Gianni Alessandro Chiappetta -Scott Jehl -James Burke -Jonas Pfenniger -Xavi Ramirez -Jared Grippe -Sylvester Keil -Brandon Sterne -Mathias Bynens -Timmy Willison <4timmywil@gmail.com> -Corey Frang -Digitalxero -Anton Kovalyov -David Murdoch -Josh Varner -Charles McNulty -Jordan Boesch -Jess Thrysoee -Michael Murray -Lee Carpenter -Alexis Abril -Rob Morgan -John Firebaugh -Sam Bisbee -Gilmore Davidson -Brian Brennan -Xavier Montillet -Daniel Pihlstrom -Sahab Yazdani -avaly -Scott Hughes -Mike Sherov -Greg Hazel -Schalk Neethling -Denis Knauf -Timo Tijhof -Steen Nielsen -Anton Ryzhov -Shi Chuan -Berker Peksag -Toby Brain -Matt Mueller -Justin -Daniel Herman -Oleg Gaidarenko -Richard Gibson -Rafaël Blais Masson -cmc3cn <59194618@qq.com> -Joe Presbrey -Sindre Sorhus -Arne de Bree -Vladislav Zarakovsky -Andrew E Monat -Oskari -Joao Henrique de Andrade Bruni -tsinha -Matt Farmer -Trey Hunner -Jason Moon -Jeffery To -Kris Borchers -Vladimir Zhuravlev -Jacob Thornton -Chad Killingsworth -Nowres Rafid -David Benjamin -Uri Gilad -Chris Faulkner -Elijah Manor -Daniel Chatfield -Nikita Govorov -Wesley Walser -Mike Pennisi -Markus Staab -Dave Riddle -Callum Macrae -Benjamin Truyman -James Huston -Erick Ruiz de Chávez -David Bonner -Akintayo Akinwunmi -MORGAN -Ismail Khair -Carl Danley -Mike Petrovich -Greg Lavallee -Daniel Gálvez -Sai Lung Wong -Tom H Fuertes -Roland Eckl -Jay Merrifield -Allen J Schmidt Jr -Jonathan Sampson -Marcel Greter -Matthias Jäggli -David Fox -Yiming He -Devin Cooper -Paul Ramos -Rod Vagg -Bennett Sorbo -Sebastian Burkhard -Zachary Adam Kaplan -nanto_vi -nanto -Danil Somsikov -Ryunosuke SATO -Jean Boussier -Adam Coulombe -Andrew Plummer -Mark Raddatz -Isaac Z. Schlueter -Karl Sieburg -Pascal Borreli -Nguyen Phuc Lam -Dmitry Gusev -Michał Gołębiowski-Owczarek -Li Xudong -Steven Benner -Tom H Fuertes -Renato Oliveira dos Santos -ros3cin -Jason Bedard -Kyle Robinson Young -Chris Talkington -Eddie Monge -Terry Jones -Jason Merino -Jeremy Dunck -Chris Price -Guy Bedford -Amey Sakhadeo -Mike Sidorov -Anthony Ryan -Dominik D. Geyer -George Kats -Lihan Li -Ronny Springer -Chris Antaki -Marian Sollmann -njhamann -Ilya Kantor -David Hong -John Paul -Jakob Stoeck -Christopher Jones -Forbes Lindesay -S. Andrew Sheppard -Leonardo Balter -Roman Reiß -Benjy Cui -Rodrigo Rosenfeld Rosas -John Hoven -Philip Jägenstedt -Christian Kosmowski -Liang Peng -TJ VanToll -Senya Pugach -Aurelio De Rosa -Nazar Mokrynskyi -Amit Merchant -Jason Bedard -Arthur Verschaeve -Dan Hart -Bin Xin -David Corbacho -Veaceslav Grimalschi -Daniel Husar -Frederic Hemberger -Ben Toews -Aditya Raghavan -Victor Homyakov -Shivaji Varma -Nicolas HENRY -Anne-Gaelle Colom -George Mauer -Leonardo Braga -Stephen Edgar -Thomas Tortorini -Winston Howes -Jon Hester -Alexander O'Mara -Bastian Buchholz -Arthur Stolyar -Calvin Metcalf -Mu Haibao -Richard McDaniel -Chris Rebert -Gabriel Schulhof -Gilad Peleg -Martin Naumann -Marek Lewandowski -Bruno Pérel -Reed Loden -Daniel Nill -Yongwoo Jeon -Sean Henderson -Richard Kraaijenhagen -Connor Atherton -Gary Ye -Christian Grete -Liza Ramo -Julian Alexander Murillo -Joelle Fleurantin -Jae Sung Park -Jun Sun -Josh Soref -Henry Wong -Jon Dufresne -Martijn W. van der Lee -Devin Wilson -Steve Mao -Zack Hall -Bernhard M. Wiedemann -Todor Prikumov -Jha Naman -William Robinet -Alexander Lisianoi -Vitaliy Terziev -Joe Trumbull -Alexander K -Damian Senn -Ralin Chimev -Felipe Sateler -Christophe Tafani-Dereeper -Manoj Kumar -David Broder-Rodgers -Alex Louden -Alex Padilla -南漂一卒 -karan-96 -Boom Lee -Andreas Solleder -CDAGaming -Pierre Spring -Shashanka Nataraj -Erik Lax -Matan Kotler-Berkowitz <205matan@gmail.com> -Jordan Beland -Henry Zhu -Saptak Sengupta -Nilton Cesar -basil.belokon diff --git a/modules/jquery/LICENSE.txt b/modules/jquery/LICENSE.txt deleted file mode 100644 index e4e5e00e..00000000 --- a/modules/jquery/LICENSE.txt +++ /dev/null @@ -1,36 +0,0 @@ -Copyright JS Foundation and other contributors, https://js.foundation/ - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/jquery/jquery - -The following license applies to all parts of this software except as -documented below: - -==== - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== - -All files located in the node_modules and external directories are -externally maintained libraries used by this software which have their -own licenses; we recommend you read them, as their terms may differ from -the terms above. diff --git a/modules/jquery/README.md b/modules/jquery/README.md deleted file mode 100644 index 83e7b937..00000000 --- a/modules/jquery/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# jQuery - -> jQuery is a fast, small, and feature-rich JavaScript library. - -For information on how to get started and how to use jQuery, please see [jQuery's documentation](http://api.jquery.com/). -For source files and issues, please visit the [jQuery repo](https://github.com/jquery/jquery). - -If upgrading, please see the [blog post for 3.3.1](https://blog.jquery.com/2017/03/20/jquery-3.3.1-now-available/). This includes notable differences from the previous version and a more readable changelog. - -## Including jQuery - -Below are some of the most common ways to include jQuery. - -### Browser - -#### Script tag - -```html - -``` - -#### Babel - -[Babel](http://babeljs.io/) is a next generation JavaScript compiler. One of the features is the ability to use ES6/ES2015 modules now, even though browsers do not yet support this feature natively. - -```js -import $ from "jquery"; -``` - -#### Browserify/Webpack - -There are several ways to use [Browserify](http://browserify.org/) and [Webpack](https://webpack.github.io/). For more information on using these tools, please refer to the corresponding project's documention. In the script, including jQuery will usually look like this... - -```js -var $ = require("jquery"); -``` - -#### AMD (Asynchronous Module Definition) - -AMD is a module format built for the browser. For more information, we recommend [require.js' documentation](http://requirejs.org/docs/whyamd.html). - -```js -define(["jquery"], function($) { - -}); -``` - -### Node - -To include jQuery in [Node](nodejs.org), first install with npm. - -```sh -npm install jquery -``` - -For jQuery to work in Node, a window with a document is required. Since no such window exists natively in Node, one can be mocked by tools such as [jsdom](https://github.com/tmpvar/jsdom). This can be useful for testing purposes. - -```js -require("jsdom").env("", function(err, window) { - if (err) { - console.error(err); - return; - } - - var $ = require("jquery")(window); -}); -``` diff --git a/modules/jquery/bower.json b/modules/jquery/bower.json deleted file mode 100644 index 95798d5a..00000000 --- a/modules/jquery/bower.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "jquery", - "main": "dist/jquery.js", - "license": "MIT", - "ignore": [ - "package.json" - ], - "keywords": [ - "jquery", - "javascript", - "browser", - "library" - ] -} \ No newline at end of file diff --git a/modules/jquery/dist/jquery.min.js b/modules/jquery/dist/jquery.min.js deleted file mode 100644 index 4d9b3a25..00000000 --- a/modules/jquery/dist/jquery.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("