Compare commits

..

No commits in common. "master" and "v14.6.0" have entirely different histories.

281 changed files with 7545 additions and 13217 deletions

8
.babelrc Normal file
View file

@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"module:babel-plugin-macros",
]
}

View file

@ -1,9 +0,0 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"module:babel-plugin-macros",
"@babel/plugin-transform-optional-chaining"
]
}

View file

@ -1,4 +1,5 @@
last 2 Chrome versions last 2 Chrome versions
last 2 Safari versions last 2 Safari versions
Firefox ESR Firefox ESR
maintained node versions
not dead not dead

View file

@ -1,31 +1,25 @@
export default { 'use strict';
'F2 - Rename File': renameCurrent,
'L - Lint': run('npm run lint'), module.exports = {
'F - Fix Lint': run('npm run fix:lint'), 'F2 - Rename file': async ({DOM}) => {
'T - Test': run('npm run test'), await DOM.renameCurrent();
'C - Coverage': run('npm run coverage'), },
'D - Build Dev': run('npm run build:client:dev'), 'D - Build Dev': async ({CloudCmd}) => {
'P - Build Prod': run('npm run build:client'), await CloudCmd.TerminalRun.show({
command: 'npm run build:client:dev',
autoClose: false,
closeMessage: 'Press any button to close Terminal',
});
CloudCmd.refresh();
},
'P - Build Prod': async ({CloudCmd}) => {
await CloudCmd.TerminalRun.show({
command: 'npm run build:client',
autoClose: true,
});
CloudCmd.refresh();
},
}; };
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,
});
};
}

View file

@ -19,5 +19,6 @@ cssnano.config.js
bin/release.js bin/release.js
client client
legacy
server_ server_

View file

@ -9,7 +9,7 @@ root = true
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = false trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4

34
.eslintrc.js Normal file
View file

@ -0,0 +1,34 @@
'use strict';
module.exports = {
extends: [
'plugin:putout/recommended',
],
plugins: [
'putout',
'node',
],
overrides: [{
files: ['bin/release.js'],
rules: {
'no-console': 'off',
'node/shebang': 'off',
},
extends: [
'plugin:node/recommended',
],
}, {
files: ['bin/cloudcmd.js'],
rules: {
'no-console': 'off',
},
extends: [
'plugin:node/recommended',
],
}, {
files: ['{client,common}/**/*.js'],
env: {
browser: true,
},
}],
};

3
.github/FUNDING.yml vendored
View file

@ -1,3 +0,0 @@
github: coderaiser
open_collective: cloudcmd
ko_fi: coderaiser

12
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,12 @@
<!--
Thank you for reporting an issue. Please fill in the template below. If unsure
about something, just do as best as you're able.
-->
* **Version** (`cloudcmd -v`):
* **Node Version** `node -v`:
* **OS** (`uname -a` on Linux):
* **Browser name/version**:
* **Used Command Line Parameters**:
* **Changed Config**:

View file

@ -1,45 +0,0 @@
---
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.

View file

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Stack Overflow
url: https://stackoverflow.com/search?q=cloudcmd
about: Please ask and answer questions here.

View file

@ -1,21 +0,0 @@
---
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.

View file

@ -1,24 +0,0 @@
***
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**

View file

@ -4,5 +4,6 @@ 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") - [ ] commit message named according to [Contributing Guide](https://github.com/coderaiser/cloudcmd/blob/master/CONTRIBUTING.md "Contributting Guide")
- [ ] `npm run fix:lint` is OK - [ ] `npm run codestyle` is OK
- [ ] `npm test` is OK - [ ] `npm test` is OK

View file

@ -1,60 +0,0 @@
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

View file

@ -1,88 +0,0 @@
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

View file

@ -1,53 +1,20 @@
name: Node CI name: Node CI
on:
- push on: [push]
permissions:
contents: write
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
NAME: cloudcmd
strategy:
matrix:
node-version:
- 22.x
- 24.x
- 26.x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@master
- uses: oven-sh/setup-bun@v2 - name: Use Node.js 12.x
with: uses: actions/setup-node@v1
bun-version: latest with:
- name: Use Node.js ${{ matrix.node-version }} version: 12.x
uses: actions/setup-node@v6 - name: npm install, build, and test
with: run: |
node-version: ${{ matrix.node-version }} npm install
- name: Install Redrun npm run build
run: bun i redrun madrun -g --no-save npm test
- 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 }}

21
.gitignore vendored
View file

@ -1,17 +1,18 @@
*.swp
*.log
*.lock
.nyc_output
.DS_Store
.idea
package-lock.json package-lock.json
npm-debug.log* yarn.lock
yarn-error.log
node_modules node_modules
npm-debug.log*
coverage coverage
modules/execon modules/execon
modules/emitify modules/emitify
.nyc_output
*.swp
.putoutcache
dist dist
dist-dev dist-dev

View file

@ -1,62 +1,178 @@
import {run, cutEnv} from 'madrun'; 'use strict';
import {defineEnv} from 'supertape/env';
const testEnv = defineEnv({ const {
timeout: 7000, run,
css: true, parallel,
}); predefined,
} = require('madrun');
const buildEnv = { const {version} = require('./package');
NODE_ENV: 'production',
};
export default { const names = [
'bin/cloudcmd.js',
'client',
'common',
'server',
'test',
'bin/release.js',
'webpack.config.js',
'cssnano.config.js',
'.webpack',
'.eslintrc.js',
'.madrun.js',
'{client,server,common}/**/*.spec.js',
];
const {putout} = predefined;
const env = 'THREAD_IT_COUNT=0';
const dockerName = 'coderaiser/cloudcmd';
module.exports = {
'start': () => 'node bin/cloudcmd.js', 'start': () => 'node bin/cloudcmd.js',
'start:dev': async () => await run('start', null, { 'start:dev': () => `NODE_ENV=development ${run('start')}`,
NODE_ENV: 'development',
}),
'build:start': () => run(['build:client', 'start']), 'build:start': () => run(['build:client', 'start']),
'build:start:dev': () => run([ 'build:start:dev': () => run(['build:client:dev', 'start:dev']),
'build:client:dev', 'lint:all': () => run(['lint', 'lint:css', 'spell']),
'start:dev', 'lint:base': () => putout({
]), names,
'lint:all': () => run('lint:progress'), }),
'lint': () => 'redlint fix; putout .', 'lint:css': () => 'stylelint css/*.css',
'lint:progress': () => run('lint', '-f progress'), 'spell': () => 'yaspeller .',
'watch:lint': () => 'nodemon -w client -w server -w test -w common -w .webpack -x "putout -s"', 'fix:lint': () => run(['lint', 'lint:css'], '--fix'),
'fresh:lint': () => run('lint', '--fresh'), 'lint': () => run('lint:progress', '--cache'),
'lint:fresh': () => run('lint', '--fresh'), 'lint:progress': () => run('lint:base', '-f progress'),
'fix:lint': async () => `putout --fix . && redlint fix`, 'lint:stream': () => run('lint:base', '-f stream'),
'lint:stream': () => run('lint', '-f stream'),
'test': () => [testEnv, `tape '{test}/**/*.js' '{bin,client,static,common,server}/**/*.spec.js' -f fail`], 'test:base': () => {
'test:e2e': () => `tape 'test-e2e/**/*.js'`, const cmd = 'tape';
const names = `'test/**/*.js' '{client,static,common,server}/**/*.spec.js'`;
return `${cmd} ${names}`;
},
'test': () => `${env} ${run('test:base')}`,
'test:client': () => `tape 'test/client/**/*.js'`, 'test:client': () => `tape 'test/client/**/*.js'`,
'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`, 'test:server': () => `tape 'test/**/*.js' 'server/**/*.spec.js' 'common/**/*.spec.js'`,
'wisdom': async () => await run(['lint:all', 'build', 'test'], null, { 'wisdom': () => run(['lint:all', 'build', 'test']),
CI: 1,
}),
'wisdom:type': () => 'bin/release.js', 'wisdom:type': () => 'bin/release.js',
'coverage': async () => [testEnv, `c8 ${await cutEnv('test')}`], 'docker:pull': () => 'docker pull node',
'coverage:report': () => 'c8 report --reporter=lcov', 'docker:pull:alpine': () => 'docker pull mhart/alpine-node',
'report': () => 'c8 report --reporter=lcov', 'docker:pull:arm32': () => 'docker pull arm32v7/node:slim',
'6to5': () => [buildEnv, 'rspack build --config rspack.config.js'], 'docker:pull:arm64': () => 'docker pull arm64v8/node:slim',
'docker:push': () => dockerPush('x64', version),
'docker:push:latest': () => dockerPush('x64'),
'docker:push:alpine': () => dockerPush('alpine', version),
'docker:push:alpine:latest': () => dockerPush('alpine'),
'docker:push:arm32': () => dockerPush('arm32', version),
'docker:push:arm32:latest': () => dockerPush('arm32'),
'docker:push:arm64': () => dockerPush('arm64', version),
'docker:push:arm64:latest': () => dockerPush('arm64'),
'docker:build': () => dockerBuild('docker/Dockerfile', 'x64', version),
'docker:build:alpine': () => dockerBuild('docker/Dockerfile.alpine', 'alpine', version),
'docker:build:arm32': () => dockerBuild('docker/arm/Dockerfile.arm32v7', 'arm32', version),
'docker:build:arm64': () => dockerBuild('docker/arm/Dockerfile.arm64v8', 'arm64', version),
'docker:manifest:create': () => {
const images = [
`${dockerName}:latest`,
`${dockerName}:latest-x64`,
// `${dockerName}:latest-arm32`,
// `${dockerName}:latest-arm64`,
].join(' ');
return `docker manifest create ${images}`;
},
'docker:manifest:push': () => `docker manifest push ${dockerName}:latest`,
'docker': () => run(['docker:x64', 'docker:alpine', 'docker:manifest:*']),
'docker-ci': () => run(['build', 'docker-login', 'docker']),
'docker-login': () => 'docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD',
'docker:x64': () => run([
'docker:pull',
'docker:build',
'docker:tag',
'docker:push',
'docker:push:latest',
]),
'docker:alpine': () => run([
'docker:pull:alpine',
'docker:build:alpine',
'docker:tag:alpine',
'docker:push:alpine',
'docker:push:alpine:latest',
]),
/*
'docker:arm32': () => run([
'docker:pull:arm32',
'docker:build:arm32',
'docker:tag:arm32',
'docker:push:arm32',
'docker:push:arm32:latest',
]),
'docker:arm64': () => run([
'docker:pull:arm64',
'docker:build:arm64',
'docker:tag:arm64',
'docker:push:arm64',
'docker:push:arm64:latest',
]),
*/
'docker:manifest': () => run([
'docker:manifest:create',
'docker:manifest:push',
]),
'docker:tag': () => dockerTag('x64', version),
'docker:tag:alpine': () => dockerTag('alpine', version),
'docker:tag:arm32': () => dockerTag('arm32', version),
'docker:tag:arm64': () => dockerTag('arm64', version),
'docker:rm:version': () => dockerRmi('x64', version),
'docker:rm:latest': () => dockerRmi('x64'),
'docker:rm:alpine': () => dockerRmi('alpine', version),
'docker:rm:latest-alpine': () => dockerRmi('alpine'),
'docker:rm:arm32': () => dockerRmi('arm32', version),
'docker:rm:latest-arm32': () => dockerRmi('arm32'),
'docker:rm:arm64': () => dockerRmi('arm64', version),
'docker:rm:latest-arm64': () => dockerRmi('arm64'),
'docker:rm-old': () => `${parallel('docker:rm:*')} || true`,
'coverage': () => `${env} nyc ${run('test:base')}`,
'report': () => 'nyc report --reporter=text-lcov | coveralls',
'6to5': () => 'webpack --progress',
'6to5:client': () => run('6to5', '--mode production'), '6to5:client': () => run('6to5', '--mode production'),
'6to5:client:dev': async () => await run('6to5', '--mode development', { '6to5:client:dev': () => `NODE_ENV=development ${run('6to5', '--mode development')}`,
NODE_ENV: 'development',
}),
'pre6to5:client': () => 'rimraf dist', 'pre6to5:client': () => 'rimraf dist',
'pre6to5:client:dev': () => 'rimraf dist-dev', 'pre6to5:client:dev': () => 'rimraf dist-dev',
'watch:client': () => run('6to5:client', '--watch'), 'watch:client': () => run('6to5:client', '--watch'),
'watch:client:dev': () => run('6to5:client:dev', '--watch'), 'watch:client:dev': () => run('6to5:client:dev', '--watch'),
'watch:server': () => 'nodemon bin/cloudcmd.js', 'watch:server': () => 'nodemon bin/cloudcmd.js',
'watch:test': async () => [testEnv, `nodemon -w client -w server -w test -w common -x ${await cutEnv('test')}`], 'watch:lint': () => `nodemon -w client -w server -w webpack.config.js -x ${run('lint')}`,
'watch:test:client': async () => `nodemon -w client -w test/client -x ${await run('test:client')}`, 'watch:test': () => `nodemon -w client -w server -w test -w common -x ${run('test')}`,
'watch:test:server': async () => `nodemon -w client -w test/client -x ${await run('test:server')}`, 'watch:test:client': () => `nodemon -w client -w test/client -x ${run('test:client')}`,
'watch:coverage': async () => [testEnv, `nodemon -w server -w test -w common -x ${await cutEnv('coverage')}`], 'watch:test:server': () => `nodemon -w client -w test/client -x ${run('test:server')}`,
'watch:fix:lint': async () => `nodemon -w client -w server -w test -w common -x '${await run('fix:lint')}'`, 'watch:coverage': () => `nodemon -w server -w test -w common -x ${run('coverage')}`,
'build': async () => run('6to5:*'), 'build': () => run('6to5:*'),
'build:dev': async () => run('build:client:dev'),
'build:client': () => run('6to5:client'), 'build:client': () => run('6to5:client'),
'build:client:dev': () => run('6to5:client:dev'), 'build:client:dev': () => run('6to5:client:dev'),
'heroku-postbuild': () => run('6to5:client'), 'heroku-postbuild': () => run('6to5:client'),
}; };
function dockerPush(type, version = 'latest') {
return `docker push coderaiser/cloudcmd:${version}-${type}`;
}
function dockerBuild(file, type, version) {
return `docker build -f ${file} -t coderaiser/cloudcmd:${version}-${type} .`;
}
function dockerTag(type, version) {
return `docker tag coderaiser/cloudcmd:${version}-${type} coderaiser/cloudcmd:latest-${type}`;
}
function dockerRmi(type, version = 'latest') {
return `docker rmi -f coderaiser/cloudcmd:${version}-${type}`;
}

View file

@ -1,29 +1,30 @@
*.spec.*
*.config.*
*.fixture.js*
*.ai
*.cdr
*.eps
*.log
*.lock
.* .*
*.spec.js
*.fixture.js*
manifest.yml manifest.yml
docker-compose.yml
now.json
app.json
bower.json
deno.json
bin/release.*
img/logo/cloudcmd-hq.png
webpack.config.js
docker docker
test* docker-compose.yml
test
fixture fixture
fixture-*
coverage coverage
css css
html html
yarn-error.log
yarn.lock
now.json
cssnano.config.js
app.json
bower.json
manifest.yml
bin/release.js
client client
webpack.config.js
*.ai
*.cdr
*.eps

View file

@ -1,15 +0,0 @@
{
"checkCoverage": false,
"all": false,
"exclude": [
"**/*.spec.js",
"**/*.*.js",
"**/*.config.*",
"**/fixture",
"**/test/**"
],
"branches": 100,
"lines": 100,
"functions": 100,
"statements": 100
}

View file

@ -1,60 +1,27 @@
{ {
"plugins": ["cloudcmd"],
"ignore": [
"*.md",
"app.json",
"fontello.json",
"html",
"fixture*"
],
"rules": {
"package-json/add-type": "off"
},
"match": { "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": { "server": {
"nodejs/remove-process-exit": "on" "remove-process-exit": true
}, },
"server/{server,exit}.js": { "server/(server|exit).js": {
"nodejs/remove-process-exit": "off" "remove-process-exit": false,
"remove-console": false
}, },
"server/{server,exit,terminal,distribute/log}.{js,mjs}": { "server/(terminal|distribute/log).js": {
"remove-console": "off" "remove-console": false
}, },
"client/{client,cloudcmd,load-module}.{js,mjs}": { "client/(client|cloudcmd|load-module).js": {
"remove-console": "off" "remove-console": false
}, },
"client": { "client/modules/config/index.js": {
"nodejs": "off" "apply-shorthand-properties": [{
}, "ignore": [
"client/sw": { "ONE_MINUTE"
"remove-console": "off" ]
}]
}, },
"test/common/cloudfunc.js": { "test/common/cloudfunc.js": {
"remove-console": "off" "remove-console": false
},
"storage.js": {
"promises/remove-useless-async": "off"
},
"docker.yml": {
"github/set-node-versions": "off"
},
"vim.js": {
"merge-duplicate-functions": "off"
},
"common": {
"nodejs/declare": "off"
} }
} }
} }

View file

@ -1,38 +0,0 @@
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(),
],
},
};

View file

@ -1,36 +0,0 @@
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,
};
}

View file

@ -1,173 +0,0 @@
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}`;
}

17
.stylelintrc.yml Normal file
View file

@ -0,0 +1,17 @@
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
max-empty-lines: 2

52
.travis.yml Normal file
View file

@ -0,0 +1,52 @@
language: node_js
node_js:
- 14
- 12
- 10
cache:
npm: false
matrix:
allow_failures:
- os: windows
os:
- linux
- osx
- windows
# https://docs.travis-ci.com/user/customizing-the-build/#git-end-of-line-conversion-control
# need for windows
git:
autocrlf: input
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
before_deploy:
- echo '{"experimental":"enabled"}' | sudo tee /etc/docker/daemon.json
- mkdir -p $HOME/.docker
- echo '{"experimental":"enabled"}' | tee $HOME/.docker/config.json
- sudo service docker start
deploy:
provider: script
script: npm run docker-ci
skip_cleanup: true
on:
node: 12
condition: $TRAVIS_OS_NAME = linux
tags: true
all_branches: false

View file

@ -1,2 +0,0 @@
[files]
extend-exclude = ["ChangeLog"]

81
.webpack/css.js Normal file
View file

@ -0,0 +1,81 @@
'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 OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const extractCSS = (a) => new ExtractTextPlugin(`${a}.css`);
const extractMain = extractCSS('[name]');
const cssNames = [
'nojs',
'view',
'config',
'terminal',
'user-menu',
...getCSSList('columns'),
];
const cssPlugins = cssNames.map(extractCSS);
const clean = (a) => a.filter(Boolean);
const plugins = clean([
...cssPlugins,
extractMain,
!isDev && new OptimizeCssAssetsPlugin(),
]);
const rules = [{
test: /\.css$/,
exclude: /css\/(nojs|view|config|terminal|user-menu|columns.*)\.css/,
use: extractMain.extract([
'css-loader',
]),
},
...cssPlugins.map(extract), {
test: /\.(png|gif|svg|woff|woff2|eot|ttf)$/,
use: {
loader: 'url-loader',
options: {
limit: 100000,
},
},
}];
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;
return {
test: RegExp(`css/${filename}`),
use: extractPlugin.extract([
'css-loader',
]),
};
}

43
.webpack/html.js Normal file
View file

@ -0,0 +1,43 @@
'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,
};
}

134
.webpack/js.js Normal file
View file

@ -0,0 +1,134 @@
'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 {NODE_ENV} = env;
const isDev = 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 noParse = (a) => /\.spec\.js$/.test(a);
const options = {
babelrc: false,
plugins: [
'module:babel-plugin-macros',
],
};
const rules = clean([
!isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
isDev && {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options,
}]);
const plugins = [
new EnvironmentPlugin({
NODE_ENV,
}),
new ServiceWorkerWebpackPlugin({
entry: join(__dirname, '..', 'client', 'sw', 'sw.js'),
excludes: ['*'],
}),
];
const splitChunks = {
name: 'cloudcmd.common',
chunks: 'all',
};
module.exports = {
resolve: {
symlinks: false,
},
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/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`,
},
output: {
filename: '[name].js',
path: isDev ? distDev : dist,
pathinfo: isDev,
devtoolModuleFilenameTemplate,
publicPath: '/dist/',
},
externals: [
externals,
],
module: {
rules,
noParse,
},
plugins,
performance: {
maxEntrypointSize: 600000,
maxAssetSize: 600000,
},
};
function externals(context, request, fn) {
if (!isDev)
return fn();
const list = [];
if (list.includes(request))
return fn(null, request);
fn();
}
function devtoolModuleFilenameTemplate(info) {
const resource = info.absoluteResourcePath.replace(rootDir + sep, '');
return `file://cloudcmd/${resource}`;
}

View file

@ -22,14 +22,12 @@
"Iptables", "Iptables",
"JitSu", "JitSu",
"Node", "Node",
"IO",
"Olena", "Olena",
"TarZak", "TarZak",
"Termux", "Termux",
"Zalitok", "Zalitok",
"WebSocket", "WebSocket",
"auth", "auth",
"autostart",
"binded", "binded",
"cd", "cd",
"cloudcmd", "cloudcmd",
@ -46,7 +44,6 @@
"gz", "gz",
"io", "io",
"js", "js",
"linux",
"maintainers", "maintainers",
"markdown", "markdown",
"microservice", "microservice",
@ -56,8 +53,6 @@
"nginx", "nginx",
"npm", "npm",
"or io", "or io",
"patreon",
"rc",
"refactor", "refactor",
"sexualized", "sexualized",
"sslPort", "sslPort",
@ -66,7 +61,6 @@
"v0", "v0",
"v1", "v1",
"v2", "v2",
"yml", "yml"
"systemd"
] ]
} }

View file

@ -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: Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language * Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences * Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism * Gracefully accepting constructive criticism
- Focusing on what is best for the community * Focusing on what is best for the community
- Showing empathy towards other community members * Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances * The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks * Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission * 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 * Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## Our Responsibilities

View file

@ -1,30 +1,27 @@
## Commit Commit
---------------
Format of the commit message: **type(scope) subject** Format of the commit message: **type(scope) subject**
**Type**: **Type**:
- feature(scope) subject
- feature: scope: subject - fix(scope) subject
- fix: scope: subject - docs(scope) subject
- docs: scope: subject - refactor(scope) subject
- refactor: scope: subject - test(scope) subject
- test: scope: subject - chore(scope) subject
- chore: scope: subject
**Scope**: **Scope**:
Scope could be anything specifying place of the commit change. Scope could be anything specifying place of the commit change.
For example util, console, view, edit, style etc... For example util, console, view, edit, style etc...
**Subject text**: **Subject text**:
- use imperative, present tense: “change” not “changed” nor “changes” - use imperative, present tense: “change” not “changed” nor “changes”
- don't capitalize first letter - don't capitalize first letter
- no dot (.) at the end - no dot (.) at the end
**Message body**: **Message body**:
- just as in <subject> use imperative, present tense: “change” not “changed” nor “changes” - just as in <subject> use imperative, present tense: “change” not “changed” nor “changes”
- includes motivation for the change and contrasts with previous behavior - includes motivation for the change and contrasts with previous behavior
**Examples**: **Examples**:
- [fix(style) .name{width}: 37% -> 35%](https://github.com/coderaiser/cloudcmd/commit/94b0642e3990c17b3a0ee3efeb75f343e1e7c050)
- [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: console: dispatch: focus -> mouseup](https://github.com/coderaiser/cloudcmd/commit/f41ec5058d1411e86a881f8e8077e0572e0409ec)

1953
ChangeLog

File diff suppressed because it is too large Load diff

1010
HELP.md

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
(The MIT License) (The MIT License)
Copyright (c) 2012-2025 Coderaiser <mnemonic.enemy@gmail.com> Copyright (c) 2012-2019 Coderaiser <mnemonic.enemy@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -1,22 +1,25 @@
# Cloud Commander v19.19.1 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![Deploy on InstaPods][DeployInstaPodsIMG]][DeployInstaPodsURL] # Cloud Commander v14.6.0 [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Codacy][CodacyIMG]][CodacyURL] [![Gitter][GitterIMGURL]][GitterURL] [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcoderaiser%2Fcloudcmd?ref=badge_shield)
### [Main][MainURL] [Blog][BlogURL] [Support][SupportURL] [Demo][DemoURL] ### [Main][MainURL] [Blog][BlogURL] Live([Heroku][HerokuURL])
[MainURL]: https://cloudcmd.io "Main" [NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png
[BlogURL]: https://blog.cloudcmd.io "Blog" [MainURL]: http://cloudcmd.io "Main"
[SupportURL]: https://patreon.com/coderaiser "Patreon" [BlogURL]: http://blog.cloudcmd.io "Blog"
[DemoURL]: https://cloudcmd-zdp6.onrender.com [HerokuURL]: https://cloudcmd.herokuapp.com/ "Heroku"
[NPM_INFO_IMG]: https://nodei.co/npm/cloudcmd.png [BuildStatusURL]: https://travis-ci.org/coderaiser/cloudcmd "Build Status"
[BuildStatusURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml "Build Status" [BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/cloudcmd.svg?style=flat-squere&longCache=true
[BuildStatusIMGURL]: https://github.com/coderaiser/cloudcmd/actions/workflows/nodejs.yml/badge.svg
[CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd [BuildAppveyorURL]: https://ci.appveyor.com/project/coderaiser/cloudcmd
[CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36 [BuildAppveyorIMGURL]: https://ci.appveyor.com/api/projects/status/tse6sc8dxrqxqehi?svg=true
[GitterURL]: https://gitter.im/cloudcmd/hello
[GitterIMGURL]: https://img.shields.io/gitter/room/coderaiser/cloudcmd.js.svg [CodacyURL]: https://www.codacy.com/app/coderaiser/cloudcmd
[DeployURL]: https://heroku.com/deploy?template=https://github.com/coderaiser/cloudcmd "Deploy" [CodacyIMG]: https://api.codacy.com/project/badge/Grade/ddda78be780549ce8754f8d47a8c0e36
[DeployIMG]: https://www.herokucdn.com/deploy/button.png
[DeployInstaPodsIMG]: https://img.shields.io/badge/deploy%20on-InstaPods-blue [GitterURL]: https://gitter.im/cloudcmd/hello
[DeployInstaPodsURL]: https://app.instapods.com/dashboard/pods/create?app=cloudcmd&ref=cloudcmd [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** a file manager for the web with console and editor. **Cloud Commander** a file manager for the web with console and editor.
@ -27,7 +30,6 @@
``` ```
npm i cloudcmd -g npm i cloudcmd -g
``` ```
## Start ## Start
For starting just type in console: For starting just type in console:
@ -45,8 +47,8 @@ Open url `http://localhost:8000` in browser.
You will see something similar to this. You will see something similar to this.
![View](https://cloudcmd.io/img/screen/view.png "View") ![View](https://cloudcmd.io/img/screen/view.png "View")
## Deploy
## Deploy
`Cloud Commander` could be easily deployed to [Heroku][DeployURL]. `Cloud Commander` could be easily deployed to [Heroku][DeployURL].
[![Deploy][DeployIMG]][DeployURL] [![Deploy][DeployIMG]][DeployURL]
@ -70,19 +72,17 @@ npm i cloudcmd express socket.io -S
And create `index.js`: And create `index.js`:
```js ```js
import http from 'node:http'; const http = require('http');
import {cloudcmd} from 'cloudcmd'; const cloudcmd = require('cloudcmd');
import {Server} from 'socket.io'; const io = require('socket.io');
import express from 'express'; const app = require('express')();
const app = express();
const port = 1337; const port = 1337;
const prefix = '/'; const prefix = '/';
const server = http.createServer(app); const server = http.createServer(app);
const socket = new Server(server, { const socket = io.listen(server, {
path: `${prefix}socket.io`, path: `${prefix}/socket.io`
}); });
const config = { const config = {
@ -93,8 +93,8 @@ const filePicker = {
data: { data: {
FilePicker: { FilePicker: {
key: 'key', key: 'key',
}, }
}, }
}; };
// override option from json/modules.json // override option from json/modules.json
@ -109,11 +109,11 @@ const {
const configManager = createConfigManager({ const configManager = createConfigManager({
configPath, configPath,
}); }),
app.use(prefix, cloudcmd({ app.use(prefix, cloudcmd({
socket, // used by Config, Edit (optional) and Console (required) socket, // used by Config, Edit (optional) and Console (required)
config, // config data (optional) config, // config data (optional)
modules, // optional modules, // optional
configManager, // optional configManager, // optional
})); }));
@ -121,21 +121,12 @@ app.use(prefix, cloudcmd({
server.listen(port); 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: `Cloud Commander` could be used as a [docker container](https://hub.docker.com/r/coderaiser/cloudcmd/ "Docker container") this way:
```sh ```sh
docker run -it --rm -v ~:/root -v /:/mnt/fs -w=/root -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`, Config would be read from home directory, hosts root file system would be mount to `/mnt/fs`,
@ -161,15 +152,11 @@ When you create this file run:
docker-compose up docker-compose up
``` ```
## Documentation Get involved
---------------
More documentation you can find on https://cloudcmd.io/.
## Get involved
There is a lot ways to be involved in `Cloud Commander` development: 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 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 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"); - if you know languages you can help with [site translations](https://github.com/coderaiser/cloudcmd/wiki "Cloud Commander community wiki");

View file

@ -11,7 +11,6 @@
"folder", "folder",
"orthodox" "orthodox"
], ],
"stack": "heroku-18",
"env": { "env": {
"NPM_CONFIG_PRODUCTION": { "NPM_CONFIG_PRODUCTION": {
"description": "Keep false to install devDependencies and build frontend", "description": "Keep false to install devDependencies and build frontend",
@ -63,6 +62,11 @@
"value": "", "value": "",
"required": false "required": false
}, },
"CLOUDCMD_CONFIG_DIALOG": {
"description": "show config dialog",
"value": "false",
"required": false
},
"CLOUDCMD_ONE_FILE_PANEL": { "CLOUDCMD_ONE_FILE_PANEL": {
"description": "show one file panel", "description": "show one file panel",
"value": "false", "value": "false",

View file

@ -1,48 +1,37 @@
#!/usr/bin/env node #!/usr/bin/env node
import process from 'node:process'; 'use strict';
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',
};
process.on('unhandledRejection', exit); const Info = require('../package');
const DIR_SERVER = '../server/';
const isUndefined = (a) => typeof a === 'undefined'; const {promisify} = require('util');
const tryToCatch = require('try-to-catch');
const choose = (a, b) => { const exit = require(DIR_SERVER + 'exit');
if (isUndefined(a)) const {
return b; createConfig,
configPath,
return a; } = require(DIR_SERVER + 'config');
};
const config = createConfig({ const config = createConfig({
configPath, configPath,
}); });
const maybeRoot = (a) => { const env = require(DIR_SERVER + 'env');
if (a === '.') const prefixer = require(DIR_SERVER + '/prefixer');
return process.cwd();
const choose = (a, b) => {
if (a === undefined)
return b;
return a; return a;
}; };
const yargsOptions = { process.on('unhandledRejection', exit);
configuration: {
'strip-aliased': true, const {argv} = process;
'strip-dashed': true, const args = require('minimist')(argv.slice(2), {
},
coerce: {
root: maybeRoot,
},
string: [ string: [
'name', 'name',
'port', 'port',
@ -57,8 +46,6 @@ const yargsOptions = {
'terminal-path', 'terminal-path',
'terminal-command', 'terminal-command',
'columns', 'columns',
'menu',
'theme',
'import-url', 'import-url',
'import-token', 'import-token',
'export-token', 'export-token',
@ -73,7 +60,6 @@ const yargsOptions = {
'open', 'open',
'config-dialog', 'config-dialog',
'config-auth', 'config-auth',
'config-port',
'console', 'console',
'sync-console-path', 'sync-console-path',
'contact', 'contact',
@ -83,7 +69,6 @@ const yargsOptions = {
'confirm-copy', 'confirm-copy',
'confirm-move', 'confirm-move',
'show-config', 'show-config',
'show-dot-files',
'show-file-name', 'show-file-name',
'vim', 'vim',
'keys-panel', 'keys-panel',
@ -92,70 +77,65 @@ const yargsOptions = {
'import', 'import',
'import-listen', 'import-listen',
'log', 'log',
'zip',
'dropbox', 'dropbox',
], ],
default: { default: {
'server': true, 'server' : true,
'name': choose(env.parse('name'), config('name')), 'name' : choose(env('name'), config('name')),
'auth': choose(env.bool('auth'), config('auth')), 'auth' : choose(env.bool('auth'), config('auth')),
'port': config('port'), 'port' : config('port'),
'online': config('online'), 'online' : config('online'),
'open': choose(env.bool('open'), config('open')), 'open' : choose(env.bool('open'), config('open')),
'editor': env.parse('editor') || config('editor'), 'editor' : env('editor') || config('editor'),
'menu': env.parse('menu') || config('menu'), 'packer' : config('packer') || 'tar',
'packer': config('packer') || 'tar', 'zip' : config('zip'),
'zip': config('zip'), 'username' : env('username') || config('username'),
'username': env.parse('username') || config('username'), 'root' : choose(env('root'), config('root')),
'root': choose(env.parse('root'), config('root')), 'prefix' : choose(env('cloudcmd_prefix'), config('prefix')),
'prefix': choose(env.parse('prefix'), config('prefix')), 'console' : choose(env.bool('console'), config('console')),
'console': choose(env.bool('console'), config('console')), 'contact' : choose(env.bool('contact'), config('contact')),
'contact': choose(env.bool('contact'), config('contact')), 'terminal' : choose(env.bool('terminal'), config('terminal')),
'terminal': choose(env.bool('terminal'), config('terminal')), 'columns' : env('columns') || config('columns') || '',
'columns': env.parse('columns') || config('columns') || '', 'vim' : choose(env.bool('vim'), config('vim')),
'theme': env.parse('theme') || config('theme') || '', 'log' : config('log'),
'vim': choose(env.bool('vim'), config('vim')),
'log': choose(env.bool('log'), config('log')),
'import-url': env.parse('import_url') || config('importUrl'), 'import-url': env('import_url') || config('importUrl'),
'import-listen': choose(env.bool('import_listen'), config('importListen')), 'import-listen': choose(env.bool('import_listen'), config('importListen')),
'import': choose(env.bool('import'), config('import')), 'import' : choose(env.bool('import'), config('import')),
'export': choose(env.bool('export'), config('export')), 'export' : choose(env.bool('export'), config('export')),
'prefix-socket': config('prefixSocket'), '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')), 'show-file-name': choose(env.bool('show_file_name'), config('showFileName')),
'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')), 'sync-console-path': choose(env.bool('sync_console_path'), config('syncConsolePath')),
'config-dialog': choose(env.bool('config_dialog'), config('configDialog')), 'config-dialog': choose(env.bool('config_dialog'), config('configDialog')),
'config-auth': choose(env.bool('config_auth'), config('configAuth')), 'config-auth': choose(env.bool('config_auth'), config('configAuth')),
'config-port': choose(env.bool('config_port'), config('configPort')), 'terminal-path': env('terminal_path') || config('terminalPath'),
'terminal-path': env.parse('terminal_path') || config('terminalPath'), 'terminal-command': env('terminal_command') || config('terminalCommand'),
'terminal-command': env.parse('terminal_command') || config('terminalCommand'),
'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')), 'terminal-auto-restart': choose(env.bool('terminal_auto_restart'), config('terminalAutoRestart')),
'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')), 'one-file-panel': choose(env.bool('one_file_panel'), config('oneFilePanel')),
'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')), 'confirm-copy': choose(env.bool('confirm_copy'), config('confirmCopy')),
'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')), 'confirm-move': choose(env.bool('confirm_move'), config('confirmMove')),
'keys-panel': env.bool('keys_panel') || config('keysPanel'), 'keys-panel': env.bool('keys_panel') || config('keysPanel'),
'import-token': env.parse('import_token') || config('importToken'), 'import-token': env('import_token') || config('importToken'),
'export-token': env.parse('export_token') || config('exportToken'), 'export-token': env('export_token') || config('exportToken'),
'dropbox': config('dropbox'), 'dropbox': config('dropbox'),
'dropbox-token': config('dropboxToken') || '', 'dropbox-token': config('dropboxToken'),
}, },
alias: { alias: {
version: 'v', v: 'version',
help: 'h', h: 'help',
password: 'p', p: 'password',
online: 'o', o: 'online',
username: 'u', u: 'username',
save: 's', s: 'save',
auth: 'a', a: 'auth',
config: 'c', c: 'config',
}, },
}; 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) if (args.version)
version(); version();
@ -165,21 +145,8 @@ else
main(); main();
async function main() { async function main() {
const {validateArgs} = await import('@putout/cli-validate-args');
const error = await validateArgs(args, [
...yargsOptions.boolean,
...yargsOptions.string,
]);
if (error)
return exit(error);
if (args.repl) if (args.repl)
await repl(); repl();
validate.columns(args.columns);
validate.theme(args.theme);
port(args.port); port(args.port);
@ -189,41 +156,37 @@ async function main() {
config('open', args.open); config('open', args.open);
config('username', args.username); config('username', args.username);
config('console', args.console); config('console', args.console);
config('syncConsolePath', args.syncConsolePath); config('syncConsolePath', args['sync-console-path']);
config('showDotFiles', args.showDotFiles); config('showFileName', args['show-file-name']);
config('showFileName', args.showFileName);
config('contact', args.contact); config('contact', args.contact);
config('terminal', args.terminal); config('terminal', args.terminal);
config('terminalPath', args.terminalPath); config('terminalPath', args['terminal-path']);
config('terminalCommand', args.terminalCommand); config('terminalCommand', args['terminal-command']);
config('terminalAutoRestart', args.terminalAutoRestart); config('terminalAutoRestart', args['terminal-auto-restart']);
config('editor', args.editor); config('editor', args.editor);
config('menu', args.menu);
config('prefix', prefixer(args.prefix)); config('prefix', prefixer(args.prefix));
config('prefixSocket', prefixer(args.prefixSocket)); config('prefixSocket', prefixer(args['prefix-socket']));
config('root', args.root || '/'); config('root', args.root || '/');
config('vim', args.vim); config('vim', args.vim);
config('theme', args.theme);
config('columns', args.columns); config('columns', args.columns);
config('log', args.log); config('log', args.log);
config('confirmCopy', args.confirmCopy); config('confirmCopy', args['confirm-copy']);
config('confirmMove', args.confirmMove); config('confirmMove', args['confirm-move']);
config('oneFilePanel', args.oneFilePanel); config('oneFilePanel', args['one-file-panel']);
config('configDialog', args.configDialog); config('configDialog', args['config-dialog']);
config('configAuth', args.configAuth); config('configAuth', args['config-auth']);
config('configPort', args.configPort); config('keysPanel', args['keys-panel']);
config('keysPanel', args.keysPanel);
config('export', args.export); config('export', args.export);
config('exportToken', args.exportToken); config('exportToken', args['export-token']);
config('import', args.import); config('import', args.import);
config('importToken', args.importToken); config('importToken', args['import-token']);
config('importListen', args.importListen); config('importListen', args['import-listen']);
config('importUrl', args.importUrl); config('importUrl', args['import-url']);
config('dropbox', args.dropbox); config('dropbox', args['dropbox']);
config('dropboxToken', args.dropboxToken); config('dropboxToken', args['dropbox-token'] || '');
await readConfig(args.config); readConfig(args.config);
const options = { const options = {
root: config('root'), root: config('root'),
@ -232,44 +195,42 @@ async function main() {
prefix: config('prefix'), prefix: config('prefix'),
prefixSocket: config('prefixSocket'), prefixSocket: config('prefixSocket'),
columns: config('columns'), columns: config('columns'),
theme: config('theme'),
menu: config('menu'),
}; };
const password = env.parse('password') || args.password; const password = env('password') || args.password;
if (password) if (password)
config('password', await getPassword(password)); config('password', getPassword(password));
validateRoot(options.root, config); validateRoot(options.root, config);
if (args.showConfig) if (args['show-config'])
await showConfig(); showConfig();
const {distributeImport} = await import('../server/distribute/import.js'); const distribute = require('../server/distribute');
const importConfig = promisify(distributeImport); const importConfig = promisify(distribute.import);
await start(options, config); await importConfig(config);
if (args.save) if (args.save)
config.write(); config.write();
start(options, config);
await tryToCatch(checkUpdate); await tryToCatch(checkUpdate);
await importConfig(config);
} }
function validateRoot(root, config) { function validateRoot(root, config) {
const validate = require(DIR_SERVER + 'validate');
validate.root(root, config); validate.root(root, config);
if (root === '/') if (root === '/')
return; return;
if (config('log')) console.log(`root: ${root}`);
console.log(`root: ${root}`);
} }
async function getPassword(password) { function getPassword(password) {
const {default: criton} = await import('criton'); const criton = require('criton');
return criton(password, config('algo')); return criton(password, config('algo'));
} }
@ -277,11 +238,13 @@ function version() {
console.log('v' + Info.version); console.log('v' + Info.version);
} }
async function start(options, config) { function start(options, config) {
const SERVER = DIR_SERVER + 'server';
if (!args.server) if (!args.server)
return; return;
const {default: server} = await import('../server/server.js'); const server = require(SERVER);
server(options, config); server(options, config);
} }
@ -294,38 +257,38 @@ function port(arg) {
exit('cloudcmd --port: should be a number'); exit('cloudcmd --port: should be a number');
} }
async function showConfig() { function showConfig() {
const {showConfig} = await import('../server/show-config.js'); const show = require('../server/show-config');
const data = showConfig(config('*')); const data = show(config('*'));
console.log(data); console.log(data);
} }
async function readConfig(name) { function readConfig(name) {
if (!name) if (!name)
return; return;
const {default: forEachKey} = await import('for-each-key'); const fs = require('fs');
const tryCatch = require('try-catch');
const jju = require('jju');
const forEachKey = require('for-each-key');
const data = await import(name, { const readjsonSync = (name) => jju.parse(fs.readFileSync(name, 'utf8'), {
with: { mode: 'json',
type: 'json',
},
}); });
const [error, data] = tryCatch(readjsonSync, name);
if (error)
return exit(error.message);
forEachKey(config, data); forEachKey(config, data);
} }
async function help() { function help() {
const {default: bin} = await import('../json/help.json', { const bin = require('../json/help');
with: { const forEachKey = require('for-each-key');
type: 'json', const currify = require('currify');
},
});
const {default: forEachKey} = await import('for-each-key');
const {default: currify} = await import('currify');
const usage = 'Usage: cloudcmd [options]'; const usage = 'Usage: cloudcmd [options]';
const url = Info.homepage; const url = Info.homepage;
const log = currify((a, b, c) => console.log(a, b, c)); const log = currify((a, b, c) => console.log(a, b, c));
@ -336,27 +299,28 @@ async function help() {
console.log('\nGeneral help using Cloud Commander: <%s>', url); console.log('\nGeneral help using Cloud Commander: <%s>', url);
} }
async function repl() { function repl() {
console.log('REPL mode enabled (telnet localhost 1337)'); console.log('REPL mode enabled (telnet localhost 1337)');
await import('../server/repl.js'); require(DIR_SERVER + 'repl');
} }
async function checkUpdate() { async function checkUpdate() {
const {default: load} = await import('package-json'); const load = require('package-json');
const {version} = await load(Info.name, 'latest');
await showUpdateInfo(version); const {version} = await load(Info.name, 'latest');
showUpdateInfo(version);
} }
async function showUpdateInfo(version) { function showUpdateInfo(version) {
if (version === Info.version) if (version === Info.version)
return; return;
const {default: chalk} = await import('chalk'); const chalk = require('chalk');
const latestVersion = chalk.green.bold(`v${version}`); const latestVersion = chalk.green.bold('v' + version);
const latest = `update available: ${latestVersion}`; const latest = `update available: ${latestVersion}`;
const current = chalk.dim(`(current: v${Info.version})`); const current = chalk.dim(`(current: v${Info.version})`);
console.log('%s %s', latest, current); console.log('%s %s', latest, current);
} }

View file

@ -1,26 +0,0 @@
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();
});

View file

@ -1,72 +1,67 @@
#!/usr/bin/env node #!/usr/bin/env node
import {promisify} from 'node:util'; 'use strict';
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 place = promisify(_place); const DIR = '../';
const Info = require(DIR + 'package');
await main(); const minor = require('minor');
const place = require('place');
const rendy = require('rendy');
const shortdate = require('shortdate');
async function main() { const ERROR = Error('ERROR: version is missing. release --patch|--minor|--major');
const history = '## Version history\n\n';
main((error) => {
if (error)
console.error(error.message);
});
function main(callback) {
const history = 'Version history\n---------------\n';
const link = '//github.com/coderaiser/cloudcmd/releases/tag/'; const link = '//github.com/coderaiser/cloudcmd/releases/tag/';
const template = '- *{{ date }}*, ' +
const template = '- ' + '**[v{{ version }}]' +
'*{{ date }}*, ' + '(' + link + 'v{{ version }})**\n';
'**[v{{ version }}]' +
'(' +
link +
'v{{ version }})**\n';
const {version} = Info; const {version} = Info;
const [error, versionNew] = await tryToCatch(cl); cl((error, versionNew) => {
if (error)
return callback(error);
if (error) replaceVersion('README.md', version, versionNew, callback);
return console.error(error); replaceVersion('HELP.md', version, versionNew, () => {
const historyNew = history + rendy(template, {
date : shortdate(),
version : versionNew,
});
await replaceVersion('README.md', version, versionNew); replaceVersion('HELP.md', history, historyNew, callback);
await replaceVersion('HELP.md', version, versionNew);
const historyNew = history +
rendy(template, {
date: shortdate(),
version: versionNew,
}); });
});
await replaceVersion('HELP.md', history, historyNew);
} }
async function replaceVersion(name, version, versionNew) { function replaceVersion(name, version, versionNew, callback) {
const [error] = await tryToCatch(place, name, version, versionNew); place(name, version, versionNew, (error) => {
if (error)
return callback(error);
if (error) callback(null, 'done: ' + name);
return console.error(error); });
console.log(`done: ${name}`);
} }
async function cl() { function cl(callback) {
const {argv} = process; const {argv} = process;
const length = argv.length - 1; const length = argv.length - 1;
const last = process.argv[length]; const last = process.argv[length];
const regExp = /^--(major|minor|patch)$/; const regExp = /^--(major|minor|patch)$/;
const [, match] = last.match(regExp) || []; const [, match] = last.match(regExp) || [];
console.log(last);
if (!regExp.test(last)) if (!regExp.test(last))
throw Error('ERROR: version is missing. release --patch|--minor|--major'); return callback(ERROR);
return getVersionNew(last, match); callback(null, getVersionNew(last, match));
} }
function getVersionNew(last, match) { function getVersionNew(last, match) {
@ -75,3 +70,4 @@ function getVersionNew(last, match) {
return last.substr(3); return last.substr(3);
} }

View file

@ -1,59 +1,74 @@
import process from 'node:process'; 'use strict';
import Emitify from 'emitify';
import inherits from 'inherits'; /* global DOM */
import {rendy} from 'rendy';
import load from 'load.js'; const Emitify = require('emitify');
import {tryToCatch} from 'try-to-catch'; const inherits = require('inherits');
import {addSlashToEnd} from 'format-io'; const rendy = require('rendy');
import pascalCase from 'just-pascal-case'; const load = require('load.js');
import currify from 'currify'; const tryToCatch = require('try-to-catch');
import {
apiURL, const pascalCase = require('just-pascal-case');
formatMsg, const isDev = process.env.NODE_ENV === 'development';
buildFromJSON,
} from '#common/cloudfunc'; const Images = require('./dom/images');
import * as Images from '#dom/images'; const {unregisterSW} = require('./sw/register');
import {unregisterSW} from './sw/register.js'; const getJsonFromFileTable = require('./get-json-from-file-table');
import {getJsonFromFileTable} from './get-json-from-file-table.js';
import {Key} from './key/index.js'; const currify = require('currify');
import {loadModule} from './load-module.js';
const noJS = (a) => a.replace(/.js$/, ''); const noJS = (a) => a.replace(/.js$/, '');
const isDev = process.env.NODE_ENV === 'development'; const {
apiURL,
formatMsg,
buildFromJSON,
} = require('../common/cloudfunc');
const loadModule = require('./load-module');
inherits(CloudCmdProto, Emitify); inherits(CloudCmdProto, Emitify);
export const createCloudCmd = ({DOM, Listeners}) => { module.exports = new CloudCmdProto(DOM);
return new CloudCmdProto({
DOM,
Listeners,
});
};
load.addErrorListener((e, src) => { load.addErrorListener((e, src) => {
const msg = `file ${src} could not be loaded`; const msg = `file ${src} could not be loaded`;
Images.show.error(msg); Images.show.error(msg);
}); });
function CloudCmdProto({DOM, Listeners}) { function CloudCmdProto(DOM) {
let Listeners;
const log = (...a) => {
if (!isDev )
return;
console.log(...a);
};
Emitify.call(this); Emitify.call(this);
const CloudCmd = this; const CloudCmd = this;
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const {Storage, Files} = DOM; const {
Storage,
Files,
} = DOM;
this.log = () => { this.log = log;
if (!isDev)
return;
};
this.prefix = ''; this.prefix = '';
this.prefixSocket = ''; this.prefixSocket = '';
this.prefixURL = ''; this.prefixURL = '';
this.DIRCLIENT = '/dist/';
this.DIRCLIENT_MODULES = this.DIRCLIENT + 'modules/';
this.MIN_ONE_PANEL_WIDTH = DOM.getCSSVar('min-one-panel-width'); this.MIN_ONE_PANEL_WIDTH = 1155;
this.HOST = location.origin || location.protocol + '//' + location.host; this.HOST = location.origin || location.protocol + '//' + location.host;
const TITLE = 'Cloud Commander';
this.TITLE = TITLE;
this.sort = { this.sort = {
left: 'name', left: 'name',
right: 'name', right: 'name',
@ -64,16 +79,28 @@ function CloudCmdProto({DOM, Listeners}) {
right: 'asc', right: 'asc',
}; };
this.changeDir = async (path, overrides = {}) => { /**
* Функция привязываеться ко всем ссылкам и
* загружает содержимое каталогов
*
* @param params - {
* paramLink - ссылка
* needRefresh - необходимость обязательной загрузки данных с сервера
* panel
* }
* @param callback
*/
this.loadDir = async (params) => {
const p = params;
const refresh = p.isRefresh;
const { const {
isRefresh,
panel, panel,
history = true, history,
noCurrent, noCurrent,
currentName, currentName,
} = overrides; } = p;
const refresh = isRefresh;
let panelChanged; let panelChanged;
if (!noCurrent && panel && panel !== Info.panel) { if (!noCurrent && panel && panel !== Info.panel) {
@ -89,12 +116,11 @@ function CloudCmdProto({DOM, Listeners}) {
Images.show.load(imgPosition, panel); Images.show.load(imgPosition, panel);
/* загружаем содержимое каталога */ /* загружаем содержимое каталога */
await ajaxLoad(addSlashToEnd(path), { await ajaxLoad(p.path, {
refresh, refresh,
history, history,
noCurrent, noCurrent,
currentName, currentName,
showDotFiles: CloudCmd.config('showDotFiles'),
}, panel); }, panel);
}; };
@ -107,8 +133,6 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.prefix = prefix; CloudCmd.prefix = prefix;
CloudCmd.prefixURL = `${prefix}${apiURL}`; CloudCmd.prefixURL = `${prefix}${apiURL}`;
CloudCmd.prefixSocket = config.prefixSocket; CloudCmd.prefixSocket = config.prefixSocket;
CloudCmd.DIR_DIST = `${prefix}/dist`;
CloudCmd.DIR_MODULES = `${this.DIR_DIST}/modules`;
CloudCmd.config = (key) => config[key]; CloudCmd.config = (key) => config[key];
CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a)); CloudCmd.config.if = currify((key, fn, a) => config[key] && fn(a));
@ -117,6 +141,7 @@ function CloudCmdProto({DOM, Listeners}) {
* should be called from config.js only * should be called from config.js only
* after successful update on server * after successful update on server
*/ */
if (key === 'password') if (key === 'password')
return; return;
@ -127,14 +152,22 @@ function CloudCmdProto({DOM, Listeners}) {
CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity; CloudCmd.MIN_ONE_PANEL_WIDTH = Infinity;
if (!document.body.scrollIntoViewIfNeeded) if (!document.body.scrollIntoViewIfNeeded)
await load.js(`${CloudCmd.DIR_MODULES}/polyfill.js`); await load.js(prefix + CloudCmd.DIRCLIENT_MODULES + 'polyfill.js');
await initModules(); await initModules();
await baseInit(); await baseInit();
await loadStyle();
CloudCmd.route(location.hash); CloudCmd.route(location.hash);
}; };
async function loadStyle() {
const {prefix} = CloudCmd;
const name = prefix + '/dist/cloudcmd.common.css';
await load.css(name);
}
this.route = (path) => { this.route = (path) => {
const query = path.split('/'); const query = path.split('/');
@ -144,13 +177,12 @@ function CloudCmdProto({DOM, Listeners}) {
const [kebabModule] = query; const [kebabModule] = query;
const module = noJS(pascalCase(kebabModule.slice(1))); const module = noJS(pascalCase(kebabModule.slice(1)));
const [, file] = query; const file = query[1];
const current = DOM.getCurrentByName(file); const current = DOM.getCurrentByName(file);
if (file && !current) { if (file && !current) {
const msg = formatMsg('set current file', file, 'error'); const msg = formatMsg('set current file', file, 'error');
CloudCmd.log(msg); CloudCmd.log(msg);
return; return;
} }
@ -172,7 +204,7 @@ function CloudCmdProto({DOM, Listeners}) {
}; };
const initModules = async () => { const initModules = async () => {
CloudCmd.Key = Key; CloudCmd.Key = new CloudCmd.Key();
CloudCmd.Key.bind(); CloudCmd.Key.bind();
const [, modules] = await tryToCatch(Files.get, 'modules'); const [, modules] = await tryToCatch(Files.get, 'modules');
@ -222,7 +254,7 @@ function CloudCmdProto({DOM, Listeners}) {
}); });
const dirPath = DOM.getCurrentDirPath(); const dirPath = DOM.getCurrentDirPath();
Listeners = CloudCmd.Listeners;
Listeners.init(); Listeners.init();
const panels = getPanels(); const panels = getPanels();
@ -236,7 +268,7 @@ function CloudCmdProto({DOM, Listeners}) {
const data = await Storage.get(dirPath); const data = await Storage.get(dirPath);
if (!data) if (!data)
await Storage.setJson(dirPath, getJsonFromFileTable()); await Storage.set(dirPath, getJsonFromFileTable());
} }
function getPanels() { function getPanels() {
@ -259,15 +291,19 @@ function CloudCmdProto({DOM, Listeners}) {
}; };
this.refresh = async (options = {}) => { this.refresh = async (options = {}) => {
const {panel = Info.panel, currentName} = options; const {
panel = Info.panel,
currentName,
} = options;
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const isRefresh = true; const isRefresh = true;
const history = false; const history = false;
const noCurrent = options?.noCurrent; const noCurrent = options ? options.noCurrent : false;
await CloudCmd.changeDir(path, { await CloudCmd.loadDir({
path,
isRefresh, isRefresh,
history, history,
panel, panel,
@ -283,18 +319,22 @@ function CloudCmdProto({DOM, Listeners}) {
* @param options * @param options
* { refresh, history } - необходимость обновить данные о каталоге * { refresh, history } - необходимость обновить данные о каталоге
* @param panel * @param panel
* @param callback
* *
*/ */
async function ajaxLoad(path, options = {}, panel) { async function ajaxLoad(path, options = {}, panel) {
const {RESTful} = DOM; const {RESTful} = DOM;
CloudCmd.log(`reading dir: "${path}";`); CloudCmd.log('reading dir: "' + path + '";');
const dirStorage = CloudCmd.config('dirStorage'); const dirStorage = CloudCmd.config('dirStorage');
const json = dirStorage && await Storage.getJson(path); const json = dirStorage && await Storage.get(path);
const name = options.currentName || Info.name; const name = options.currentName || Info.name;
const {noCurrent, refresh} = options; const {
noCurrent,
refresh,
} = options;
if (!refresh && json) if (!refresh && json)
return await createFileTable(json, panel, options); return await createFileTable(json, panel, options);
@ -311,8 +351,7 @@ function CloudCmdProto({DOM, Listeners}) {
const [, newObj] = await RESTful.read(path + query, 'json'); const [, newObj] = await RESTful.read(path + query, 'json');
if (!newObj) if (!newObj)
// that's OK, error handled by RESTful return; // that's OK, error handled by RESTful
return;
options.sort = sort; options.sort = sort;
options.order = order; options.order = order;
@ -325,35 +364,28 @@ function CloudCmdProto({DOM, Listeners}) {
if (!CloudCmd.config('dirStorage')) if (!CloudCmd.config('dirStorage'))
return; return;
Storage.setJson(path, newObj); Storage.set(path, newObj);
} }
/** /**
* Функция строит файловую таблицу * Функция строит файловую таблицу
* @param data - данные о файлах * @param json - данные о файлах
* @param panelParam * @param panelParam
* @param options - history, noCurrent, showDotFiles * @param history
* @param callback
*/ */
async function createFileTable(data, panelParam, options) { async function createFileTable(data, panelParam, options) {
const { const {
history, history,
noCurrent, noCurrent,
showDotFiles,
} = options; } = options;
const names = [ const names = ['file', 'path', 'link', 'pathLink'];
'file',
'path',
'link',
'pathLink',
];
const [error, [ const [
file, error,
path, [file, path, link, pathLink],
link, ] = await tryToCatch(Files.get, names);
pathLink,
]] = await tryToCatch(Files.get, names);
if (error) if (error)
return DOM.Dialog.alert(error.responseText); return DOM.Dialog.alert(error.responseText);
@ -361,7 +393,10 @@ function CloudCmdProto({DOM, Listeners}) {
const panel = panelParam || DOM.getPanel(); const panel = panelParam || DOM.getPanel();
const {prefix} = CloudCmd; const {prefix} = CloudCmd;
const {dir, name} = Info; const {
dir,
name,
} = Info;
const {childNodes} = panel; const {childNodes} = panel;
let i = childNodes.length; let i = childNodes.length;
@ -370,13 +405,12 @@ function CloudCmdProto({DOM, Listeners}) {
panel.removeChild(panel.lastChild); panel.removeChild(panel.lastChild);
panel.innerHTML = buildFromJSON({ panel.innerHTML = buildFromJSON({
sort: options.sort, sort : options.sort,
order: options.order, order : options.order,
data, data,
id: panel.id, id : panel.id,
prefix, prefix,
showDotFiles, template : {
template: {
file, file,
path, path,
pathLink, pathLink,
@ -416,7 +450,9 @@ function CloudCmdProto({DOM, Listeners}) {
const path = parentDirPath; const path = parentDirPath;
await CloudCmd.changeDir(path); await CloudCmd.loadDir({
path,
});
const current = DOM.getCurrentByName(dir); const current = DOM.getCurrentByName(dir);
const [first] = DOM.getFiles(panel); const [first] = DOM.getFiles(panel);
@ -426,3 +462,4 @@ function CloudCmdProto({DOM, Listeners}) {
}); });
}; };
} }

View file

@ -1,44 +1,42 @@
import '../css/main.css'; 'use strict';
import process from 'node:process';
import wraptile from 'wraptile'; require('../css/main.css');
import load from 'load.js'; require('../css/nojs.css');
import * as Util from '#common/util'; require('../css/columns/name-size-date.css');
import * as CloudFunc from '#common/cloudfunc'; require('../css/columns/name-size.css');
import DOM from '#dom';
import {registerSW, listenSW} from './sw/register.js'; const wraptile = require('wraptile');
import {initSortPanel, sortPanel} from './sort.js'; const load = require('load.js');
import {createCloudCmd} from './client.js';
import * as Listeners from './listeners/index.js';
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
export default init; const {
registerSW,
listenSW,
} = require('./sw/register');
globalThis.CloudCmd = init; // prevent additional loading of emitify
window.Emitify = require('emitify');
async function init(config) { module.exports = window.CloudCmd = (config) => {
globalThis.CloudCmd = createCloudCmd({ window.Util = require('../common/util');
DOM, window.CloudFunc = require('../common/cloudfunc');
Listeners,
});
globalThis.DOM = DOM;
globalThis.Util = Util;
globalThis.CloudFunc = CloudFunc;
await register(config); const DOM = require('./dom');
window.DOM = DOM;
window.CloudCmd = require('./client');
register(config);
require('./listeners');
require('./key');
require('./sort');
initSortPanel();
globalThis.CloudCmd.sortPanel = sortPanel;
const prefix = getPrefix(config.prefix); const prefix = getPrefix(config.prefix);
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) { function getPrefix(prefix) {
if (!prefix) if (!prefix)
@ -54,7 +52,7 @@ const onUpdateFound = wraptile(async (config) => {
if (isDev) if (isDev)
return; return;
const {DOM} = globalThis; const {DOM} = window;
const prefix = getPrefix(config.prefix); const prefix = getPrefix(config.prefix);
await load.js(`${prefix}/dist/cloudcmd.common.js`); await load.js(`${prefix}/dist/cloudcmd.common.js`);
@ -63,7 +61,7 @@ const onUpdateFound = wraptile(async (config) => {
console.log('cloudcmd: sw: updated'); console.log('cloudcmd: sw: updated');
DOM.Events.removeAll(); DOM.Events.removeAll();
globalThis.CloudCmd(config); window.CloudCmd(config);
}); });
async function register(config) { async function register(config) {
@ -72,3 +70,4 @@ async function register(config) {
listenSW(sw, 'updatefound', onUpdateFound(config)); listenSW(sw, 'updatefound', onUpdateFound(config));
} }

View file

@ -1,124 +1,137 @@
/* global CloudCmd*/ 'use strict';
import * as Storage from '#dom/storage';
import tryToPromiseAll from '../../common/try-to-promise-all.js';
const CLASS = 'cut-file'; /* global CloudCmd */
const COPY = 'copy';
const CUT = 'cut';
function showMessage(msg) { const tryToPromiseAll = require('../../common/try-to-promise-all');
globalThis.DOM.Dialog.alert(msg); const Storage = require('./storage');
} const DOM = require('./');
function getNames() { module.exports = new BufferProto();
const {DOM} = globalThis;
const files = DOM.getActiveFiles();
return DOM.getFilenames(files); function BufferProto() {
} const Info = DOM.CurrentInfo;
const CLASS = 'cut-file';
const COPY = 'copy';
const CUT = 'cut';
const Buffer = {
cut : callIfEnabled.bind(null, cut),
copy : callIfEnabled.bind(null, copy),
clear : callIfEnabled.bind(null, clear),
paste : callIfEnabled.bind(null, paste),
};
function addCutClass() { function showMessage(msg) {
const {DOM} = globalThis; DOM.Dialog.alert(msg);
const files = DOM.getActiveFiles();
for (const element of files) {
element.classList.add(CLASS);
} }
}
function rmCutClass() { function getNames() {
const {DOM} = globalThis; const files = DOM.getActiveFiles();
const files = DOM.getByClassAll(CLASS); const names = DOM.getFilenames(files);
for (const element of files) { return names;
element.classList.remove(CLASS);
} }
function addCutClass() {
const files = DOM.getActiveFiles();
for (const element of files) {
element.classList.add(CLASS);
}
}
function rmCutClass() {
const files = DOM.getByClassAll(CLASS);
for (const element of files) {
element.classList.remove(CLASS);
}
}
function callIfEnabled(callback) {
const is = CloudCmd.config('buffer');
if (is)
return callback();
showMessage('Buffer disabled in config!');
}
async function readBuffer() {
const [e, cp, ct] = await tryToPromiseAll([
Storage.get(COPY),
Storage.get(CUT),
]);
return [
e,
cp,
ct,
];
}
async function copy() {
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
await Storage.remove(CUT);
await Storage.set(COPY, {
from,
names,
});
}
async function cut() {
const names = getNames();
const from = Info.dirPath;
await clear();
if (!names.length)
return;
addCutClass();
await Storage.set(CUT, {
from,
names,
});
}
async function clear() {
await Storage.remove(COPY);
await Storage.remove(CUT);
rmCutClass();
}
async function paste() {
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();
}
return Buffer;
} }
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();
});

View file

@ -1,83 +0,0 @@
/* 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);
}

View file

@ -1,16 +1,28 @@
'use strict';
/* global DOM */ /* global DOM */
/* global CloudCmd */ /* global CloudCmd */
import createElement from '@cloudcmd/create-element';
import {getTitle, FS} from '#common/cloudfunc'; const btoa = require('../../common/btoa');
import {encode, decode} from '#common/entity'; const createElement = require('@cloudcmd/create-element');
const {
encode,
decode,
} = require('../../common/entity');
const {
getTitle,
FS,
} = require('../../common/cloudfunc');
let Title; let Title;
const CURRENT_FILE = 'current-file'; const CURRENT_FILE = 'current-file';
const encodeNBSP = (a) => a?.replace('\xa0', '&nbsp;'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const decodeNBSP = (a) => a?.replace('&nbsp;', '\xa0'); const SPACE = ' ';
export const _CURRENT_FILE = CURRENT_FILE; module.exports._CURRENT_FILE = CURRENT_FILE;
/** /**
* set name from current (or param) file * set name from current (or param) file
@ -18,7 +30,7 @@ export const _CURRENT_FILE = CURRENT_FILE;
* @param name * @param name
* @param current * @param current
*/ */
export const setCurrentName = (name, current) => { module.exports.setCurrentName = (name, current) => {
const Info = DOM.CurrentInfo; const Info = DOM.CurrentInfo;
const {link} = Info; const {link} = Info;
const {prefix} = CloudCmd; const {prefix} = CloudCmd;
@ -29,7 +41,7 @@ export const setCurrentName = (name, current) => {
link.href = dir + encoded; link.href = dir + encoded;
link.innerHTML = encoded; link.innerHTML = encoded;
current.setAttribute('data-name', createNameAttribute(name)); current.setAttribute('data-name', 'js-file-' + btoa(encodeURI(name)));
CloudCmd.emit('current-file', current); CloudCmd.emit('current-file', current);
return link; return link;
@ -40,48 +52,28 @@ export const setCurrentName = (name, current) => {
* *
* @param currentFile * @param currentFile
*/ */
export const getCurrentName = (currentFile) => { module.exports.getCurrentName = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
if (!current) if (!current)
return ''; return '';
return parseNameAttribute(current.getAttribute('data-name')); const link = DOM.getCurrentLink(current);
if (!link)
return '';
return decode(link.title)
.replace(NBSP_REG, SPACE);
}; };
/** /**
* Generate a `data-name` attribute for the given filename * get current direcotory path
* @param name The string name to encode
*/ */
const createNameAttribute = (name) => { module.exports.getCurrentDirPath = (panel = DOM.getPanel()) => {
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); const path = DOM.getByDataName('js-path', panel);
return path.textContent; return path.textContent
.replace(NBSP_REG, SPACE);
}; };
/** /**
@ -89,31 +81,36 @@ export const getCurrentDirPath = (panel = DOM.getPanel()) => {
* *
* @param currentFile - current file by default * @param currentFile - current file by default
*/ */
export const getCurrentPath = (currentFile) => { module.exports.getCurrentPath = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const [element] = DOM.getByTag('a', current); const [element] = DOM.getByTag('a', current);
const {prefix} = CloudCmd; const {prefix} = CloudCmd;
return parseHrefAttribute(prefix, element.getAttribute('href')); const path = element
.getAttribute('href')
.replace(RegExp('^' + prefix + FS), '')
.replace(NBSP_REG, SPACE);
return decode(path);
}; };
/** /**
* get current directory name * get current direcotory name
*/ */
export const getCurrentDirName = () => { module.exports.getCurrentDirName = () => {
const href = DOM const href = DOM.getCurrentDirPath()
.getCurrentDirPath()
.replace(/\/$/, ''); .replace(/\/$/, '');
const substr = href.substr(href, href.lastIndexOf('/')); const substr = href.substr(href, href.lastIndexOf('/'));
const ret = href.replace(substr + '/', '') || '/';
return href.replace(`${substr}/`, '') || '/'; return ret;
}; };
/** /**
* get current directory path * get current direcotory path
*/ */
export const getParentDirPath = (panel) => { module.exports.getParentDirPath = (panel) => {
const path = DOM.getCurrentDirPath(panel); const path = DOM.getCurrentDirPath(panel);
const dirName = DOM.getCurrentDirName() + '/'; const dirName = DOM.getCurrentDirName() + '/';
const index = path.lastIndexOf(dirName); const index = path.lastIndexOf(dirName);
@ -125,9 +122,9 @@ export const getParentDirPath = (panel) => {
}; };
/** /**
* get not current directory path * get not current direcotory path
*/ */
export const getNotCurrentDirPath = () => { module.exports.getNotCurrentDirPath = () => {
const panel = DOM.getPanel({ const panel = DOM.getPanel({
active: false, active: false,
}); });
@ -140,20 +137,20 @@ export const getNotCurrentDirPath = () => {
* *
* @currentFile * @currentFile
*/ */
export const getCurrentFile = () => { module.exports.getCurrentFile = () => {
return DOM.getByClass(CURRENT_FILE); return DOM.getByClass(CURRENT_FILE);
}; };
/** /**
* get current file by name * get current file by name
*/ */
export const getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => { module.exports.getCurrentByName = (name, panel = DOM.CurrentInfo.panel) => {
const dataName = 'js-file-' + btoa(encodeURI(encodeNBSP(name))); const dataName = 'js-file-' + btoa(encodeURI(name));
return DOM.getByDataName(dataName, panel); return DOM.getByDataName(dataName, panel);
}; };
/** /**
* private function that unset currentFile * private function thet unset currentfile
* *
* @currentFile * @currentFile
*/ */
@ -169,7 +166,7 @@ function unsetCurrentFile(currentFile) {
/** /**
* unified way to set current file * unified way to set current file
*/ */
export const setCurrentFile = (currentFile, options) => { module.exports.setCurrentFile = (currentFile, options) => {
const o = options; const o = options;
const currentFileWas = DOM.getCurrentFile(); const currentFileWas = DOM.getCurrentFile();
@ -198,7 +195,7 @@ export const setCurrentFile = (currentFile, options) => {
* but it should be false * but it should be false
* to prevent default behavior * to prevent default behavior
*/ */
if (!o || o.history) { if (!o || o.history !== false) {
const historyPath = path === '/' ? path : FS + path; const historyPath = path === '/' ? path : FS + path;
DOM.setHistory(historyPath, null, historyPath); DOM.setHistory(historyPath, null, historyPath);
} }
@ -206,7 +203,6 @@ export const setCurrentFile = (currentFile, options) => {
/* scrolling to current file */ /* scrolling to current file */
const CENTER = true; const CENTER = true;
DOM.scrollIntoViewIfNeeded(currentFile, CENTER); DOM.scrollIntoViewIfNeeded(currentFile, CENTER);
CloudCmd.emit('current-file', currentFile); CloudCmd.emit('current-file', currentFile);
@ -216,7 +212,7 @@ export const setCurrentFile = (currentFile, options) => {
return DOM; return DOM;
}; };
export const setCurrentByName = (name) => { this.setCurrentByName = (name) => {
const current = DOM.getCurrentByName(name); const current = DOM.getCurrentByName(name);
return DOM.setCurrentFile(current); return DOM.setCurrentFile(current);
}; };
@ -227,7 +223,7 @@ export const setCurrentByName = (name) => {
* @param layer - element * @param layer - element
* @param - position {x, y} * @param - position {x, y}
*/ */
export const getCurrentByPosition = ({x, y}) => { module.exports.getCurrentByPosition = ({x, y}) => {
const element = document.elementFromPoint(x, y); const element = document.elementFromPoint(x, y);
const getEl = (el) => { const getEl = (el) => {
@ -259,7 +255,7 @@ export const getCurrentByPosition = ({x, y}) => {
* *
* @param currentFile * @param currentFile
*/ */
export const isCurrentFile = (currentFile) => { module.exports.isCurrentFile = (currentFile) => {
if (!currentFile) if (!currentFile)
return false; return false;
@ -271,7 +267,8 @@ export const isCurrentFile = (currentFile) => {
* *
* @param name * @param name
*/ */
export const setTitle = (name) => {
module.exports.setTitle = (name) => {
if (!Title) if (!Title)
Title = DOM.getByTag('title')[0] || createElement('title', { Title = DOM.getByTag('title')[0] || createElement('title', {
innerHTML: name, innerHTML: name,
@ -288,24 +285,20 @@ export const setTitle = (name) => {
* *
* @param currentFile * @param currentFile
*/ */
export const isCurrentIsDir = (currentFile) => { module.exports.isCurrentIsDir = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const path = DOM.getCurrentPath(current);
const fileType = DOM.getCurrentType(current); const fileType = DOM.getCurrentType(current);
const isZip = path.endsWith('.zip'); return /^directory(-link)?/.test(fileType);
const isDir = /^directory(-link)?/.test(fileType);
return isDir || isZip;
}; };
export const getCurrentType = (currentFile) => { module.exports.getCurrentType = (currentFile) => {
const current = currentFile || DOM.getCurrentFile(); const current = currentFile || DOM.getCurrentFile();
const el = DOM.getByDataName('js-type', current); const el = DOM.getByDataName('js-type', current);
const type = el.className const type = el.className
.split(' ') .split(' ')
.pop(); .pop();
return type; return type;
}; };

View file

@ -1,72 +1,69 @@
import {test, stub} from 'supertape'; 'use strict';
import {create} from 'auto-globals';
import wraptile from 'wraptile';
import * as currentFile from './current-file.js';
const test = require('supertape');
const {create} = require('auto-globals');
const stub = require('@cloudcmd/stub');
const id = (a) => a; const id = (a) => a;
const wraptile = require('wraptile');
const returns = wraptile(id); const returns = wraptile(id);
const currentFile = require('./current-file');
const {_CURRENT_FILE} = currentFile; const {_CURRENT_FILE} = currentFile;
test('current-file: setCurrentName: setAttribute', (t) => { test('current-file: setCurrentName: setAttribute', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const {setAttribute} = current; const {setAttribute} = current;
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-aGVsbG8='), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [
'data-name',
'js-file-aGVsbG8=',
], 'should call setAttribute');
t.end(); t.end();
}); });
test('current-file: setCurrentName: setAttribute: cyrillic', (t) => { test('current-file: setCurrentName: setAttribute: cyrillic', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const {setAttribute} = current; const {setAttribute} = current;
currentFile.setCurrentName('ай', current); currentFile.setCurrentName('ай', current);
globalThis.DOM = DOM; t.ok(setAttribute.calledWith('data-name', 'js-file-JUQwJUIwJUQwJUI5'), 'should call setAttribute');
globalThis.CloudCmd = CloudCmd;
t.calledWith(setAttribute, [ global.DOM = DOM;
'data-name', global.CloudCmd = CloudCmd;
'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(); t.end();
}); });
test('current-file: emit', (t) => { test('current-file: emit', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const emit = stub(); const emit = stub();
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd({ global.CloudCmd = getCloudCmd({
emit, emit,
}); });
@ -74,41 +71,47 @@ test('current-file: emit', (t) => {
currentFile.setCurrentName('hello', current); currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM; t.ok(emit.calledWith('current-file', current), 'should call emit');
globalThis.CloudCmd = CloudCmd;
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.calledWith(emit, ['current-file', current], 'should call emit');
t.end(); t.end();
}); });
test('current-file: setCurrentName: return', (t) => { test('current-file: setCurrentName: return', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const link = {}; const link = {};
globalThis.DOM = getDOM({ global.DOM = getDOM({
link, link,
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const result = currentFile.setCurrentName('hello', current); const result = currentFile.setCurrentName('hello', current);
globalThis.DOM = DOM;
globalThis.CloudCmd = CloudCmd;
t.equal(result, link, 'should return link'); t.equal(result, link, 'should return link');
global.DOM = DOM;
global.CloudCmd = CloudCmd;
t.end(); t.end();
}); });
test('current-file: getParentDirPath: result', (t) => { test('current-file: getParentDirPath: result', (t) => {
const {DOM} = globalThis; const {DOM} = global;
const getCurrentDirPath = returns('/D/Films/+++favorite films/'); const getCurrentDirPath = returns('/D/Films/+++favorite films/');
const getCurrentDirName = returns('+++favorite films'); const getCurrentDirName = returns('+++favorite films');
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
}); });
@ -116,55 +119,65 @@ test('current-file: getParentDirPath: result', (t) => {
const result = currentFile.getParentDirPath(); const result = currentFile.getParentDirPath();
const expected = '/D/Films/'; const expected = '/D/Films/';
globalThis.DOM = DOM; global.DOM = DOM;
t.equal(result, expected, 'should return parent dir path'); t.equal(result, expected, 'should return parent dir path');
t.end(); t.end();
}); });
test('current-file: isCurrentFile: no', (t) => { test('current-file: isCurrentFile: no', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const result = currentFile.isCurrentFile(); const result = currentFile.isCurrentFile();
const expect = false;
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.notOk(result); t.equal(result, expect, 'should equal');
t.end(); t.end();
}); });
test('current-file: isCurrentFile', (t) => { test('current-file: isCurrentFile', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
const isContainClass = stub(); const isContainClass = stub();
globalThis.DOM = getDOM({ global.DOM = getDOM({
isContainClass, isContainClass,
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = {}; const current = {};
currentFile.isCurrentFile(current); currentFile.isCurrentFile(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.calledWith(isContainClass, [current, _CURRENT_FILE], 'should call isContainClass'); t.ok(isContainClass.calledWith(current, _CURRENT_FILE), 'should call isContainClass');
t.end(); t.end();
}); });
test('current-file: getCurrentType', (t) => { test('current-file: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const {getByDataName} = globalThis.DOM; const {getByDataName} = global.DOM;
getByDataName.returns({ getByDataName.returns({
className: 'mini-icon directory', className: 'mini-icon directory',
@ -174,129 +187,122 @@ test('current-file: getCurrentType', (t) => {
currentFile.getCurrentType(current); currentFile.getCurrentType(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.calledWith(getByDataName, ['js-type', current]); t.ok(getByDataName.calledWith('js-type', current));
t.end(); t.end();
}); });
test('current-file: isCurrentIsDir: getCurrentType', (t) => { test('current-file: isCurrentIsDir: getCurrentType', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM(); global.DOM = getDOM();
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const {getCurrentType} = globalThis.DOM; const {getCurrentType} = global.DOM;
const current = create(); const current = create();
currentFile.isCurrentIsDir(current); currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.calledWith(getCurrentType, [current]); t.ok(getCurrentType.calledWith(current));
t.end(); t.end();
}); });
test('current-file: isCurrentIsDir: directory', (t) => { test('current-file: isCurrentIsDir: directory', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentType: stub().returns('directory'), getCurrentType: stub().returns('directory'),
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const result = currentFile.isCurrentIsDir(current); const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.ok(result); t.ok(result);
t.end(); t.end();
}); });
test('current-file: isCurrentIsDir: directory-link', (t) => { test('current-file: isCurrentIsDir: directory-link', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentType: stub().returns('directory-link'), getCurrentType: stub().returns('directory-link'),
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const result = currentFile.isCurrentIsDir(current); const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.ok(result); t.ok(result);
t.end(); t.end();
}); });
test('current-file: isCurrentIsDir: file', (t) => { test('current-file: isCurrentIsDir: file', (t) => {
const {DOM, CloudCmd} = globalThis; const {
DOM,
CloudCmd,
} = global;
globalThis.DOM = getDOM({ global.DOM = getDOM({
getCurrentType: stub().returns('file'), getCurrentType: stub().returns('file'),
}); });
globalThis.CloudCmd = getCloudCmd(); global.CloudCmd = getCloudCmd();
const current = create(); const current = create();
const result = currentFile.isCurrentIsDir(current); const result = currentFile.isCurrentIsDir(current);
globalThis.DOM = DOM; global.DOM = DOM;
globalThis.CloudCmd = CloudCmd; global.CloudCmd = CloudCmd;
t.notOk(result); t.notOk(result);
t.end(); t.end();
}); });
const getCloudCmd = ({emit} = {}) => ({ function getCloudCmd({emit} = {}) {
prefix: '', return {
emit: emit || stub(), 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&nbsp;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;
function getDOM({
link = {},
getCurrentDirPath = stub(),
getCurrentDirName = stub(),
getByDataName = stub(),
isContainClass = stub(),
getCurrentType = stub(),
} = {}) {
return { return {
getCurrentDirPath, getCurrentDirPath,
getCurrentDirName, getCurrentDirName,
getCurrentPath,
getByDataName, getByDataName,
isContainClass, isContainClass,
getCurrentType, getCurrentType,
@ -306,3 +312,4 @@ function getDOM(overrides = {}) {
}, },
}; };
} }

View file

@ -1,18 +1,27 @@
import {tryToCatch} from 'try-to-catch'; 'use strict';
import * as smalltalk from 'smalltalk';
const tryToCatch = require('try-to-catch');
const {
alert,
prompt,
confirm,
progress,
} = require('smalltalk');
const title = 'Cloud Commander'; const title = 'Cloud Commander';
export const alert = (...a) => smalltalk.alert(title, ...a, { module.exports.alert = (...a) => alert(title, ...a, {
cancel: false, cancel: false,
}); });
export const prompt = (...a) => tryToCatch(smalltalk.prompt, title, ...a); module.exports.prompt = (...a) => tryToCatch(prompt, title, ...a);
export const confirm = (...a) => tryToCatch(smalltalk.confirm, title, ...a); module.exports.confirm = (...a) => tryToCatch(confirm, title, ...a);
export const progress = (...a) => smalltalk.progress(title, ...a); module.exports.progress = (...a) => progress(title, ...a);
alert.noFiles = () => { module.exports.alert.noFiles = () => {
return smalltalk.alert(title, 'No files selected!', { return alert(title, 'No files selected!', {
cancel: false, cancel: false,
}); });
}; };

View file

@ -1,19 +1,27 @@
/* global DOM, CloudCmd */ 'use strict';
import philip from 'philip';
import * as Dialog from '#dom/dialog';
import {FS} from '#common/cloudfunc';
import * as Images from '#dom/images';
export const uploadDirectory = (items) => { /* global CloudCmd */
const philip = require('philip');
const Images = require('./images');
const {FS} = require('../../common/cloudfunc');
const DOM = require('.');
const Dialog = require('./dialog');
const {getCurrentDirPath: getPathWhenRootEmpty} = DOM;
module.exports = (items) => {
if (items.length) if (items.length)
Images.show('top'); Images.show('top');
const entries = Array const entries = Array.from(items).map((item) => {
.from(items) return item.webkitGetAsEntry();
.map((item) => item.webkitGetAsEntry()); });
const dirPath = DOM.getCurrentDirPath(); const dirPath = getPathWhenRootEmpty();
const path = dirPath.replace(/\/$/, ''); const path = dirPath
.replace(/\/$/, '');
const progress = Dialog.progress('Uploading...'); const progress = Dialog.progress('Uploading...');
@ -57,8 +65,15 @@ export const uploadDirectory = (items) => {
uploader.on('end', CloudCmd.refresh); uploader.on('end', CloudCmd.refresh);
}; };
const percent = (i, n, per = 100) => Math.round(i * per / n); function percent(i, n, per = 100) {
return Math.round(i * per / n);
}
const uploadFile = (url, data) => DOM.load.put(url, data); function uploadFile(url, data) {
return DOM.load.put(url, data);
}
function uploadDir(url) {
return DOM.load.put(url + '?dir');
}
const uploadDir = (url) => DOM.load.put(`${url}?dir`);

View file

@ -1,15 +1,8 @@
import currify from 'currify'; 'use strict';
const DOM = { const currify = require('currify');
show,
hide, const DOM = module.exports;
getByClass,
getByClassAll,
getByDataName,
getById,
getByTag,
isContainClass,
};
/** /**
* check class of element * check class of element
@ -17,7 +10,7 @@ const DOM = {
* @param element * @param element
* @param className * @param className
*/ */
export function isContainClass(element, className) { const isContainClass = (element, className) => {
if (!element) if (!element)
throw Error('element could not be empty!'); throw Error('element could not be empty!');
@ -25,68 +18,67 @@ export function isContainClass(element, className) {
throw Error('className could not be empty!'); throw Error('className could not be empty!');
if (Array.isArray(className)) if (Array.isArray(className))
return className.some(currify( return className.some(currify(isContainClass, element));
isContainClass,
element,
));
const {classList} = element; const {classList} = element;
return classList.contains(className); return classList.contains(className);
} };
module.exports.isContainClass = isContainClass;
/** /**
* Function search element by tag * Function search element by tag
* @param tag - className * @param tag - className
* @param element - element * @param element - element
*/ */
export function getByTag(tag, element = document) { module.exports.getByTag = (tag, element = document) => {
return element.getElementsByTagName(tag); return element.getElementsByTagName(tag);
} };
/** /**
* Function search element by id * Function search element by id
* @param id * @param Id - id
* @param element
*/ */
export function getById(id, element = document) { module.exports.getById = (id, element = document) => {
return element.querySelector(`#${id}`); return element.querySelector('#' + id);
} };
/** /**
* Function search first element by class name * Function search first element by class name
* @param className - className * @param className - className
* @param element - element * @param element - element
*/ */
export function getByClass(className, element = document) { module.exports.getByClass = (className, element = document) => {
return DOM.getByClassAll(className, element)[0]; return DOM.getByClassAll(className, element)[0];
} };
export function getByDataName(attribute, element = document) { module.exports.getByDataName = (attribute, element = document) => {
const selector = '[' + 'data-name="' + attribute + '"]'; const selector = '[' + 'data-name="' + attribute + '"]';
return element.querySelector(selector); return element.querySelector(selector);
} };
/** /**
* Function search element by class name * Function search element by class name
* @param className * @param pClass - className
* @param element * @param element - element
*/ */
export function getByClassAll(className, element) { module.exports.getByClassAll = (className, element) => {
return (element || document).getElementsByClassName(className); return (element || document).getElementsByClassName(className);
} };
/** /**
* add class=hidden to element * add class=hidden to element
* *
* @param element * @param element
*/ */
export function hide(element) { module.exports.hide = (element) => {
element.classList.add('hidden'); element.classList.add('hidden');
return DOM; return DOM;
} };
export function show(element) { module.exports.show = (element) => {
element.classList.remove('hidden'); element.classList.remove('hidden');
return DOM; return DOM;
} };

View file

@ -1,27 +1,19 @@
import {test, stub} from 'supertape'; 'use strict';
import {create} from 'auto-globals';
import {tryCatch} from 'try-catch'; const test = require('supertape');
import { const {create} = require('auto-globals');
isContainClass, const tryCatch = require('try-catch');
getByTag,
getById, const {isContainClass} = require('./dom-tree');
getByClass,
getByDataName,
getByClassAll,
hide,
show,
} from './dom-tree.js';
test('dom: isContainClass: no element', (t) => { test('dom: isContainClass: no element', (t) => {
const [e] = tryCatch(isContainClass); const [e] = tryCatch(isContainClass);
t.equal(e.message, 'element could not be empty!', 'should throw when no element'); t.equal(e.message, 'element could not be empty!', 'should throw when no element');
t.end(); t.end();
}); });
test('dom: isContainClass: no className', (t) => { test('dom: isContainClass: no className', (t) => {
const [e] = tryCatch(isContainClass, {}); const [e] = tryCatch(isContainClass, {});
t.equal(e.message, 'className could not be empty!', 'should throw when no element'); t.equal(e.message, 'className could not be empty!', 'should throw when no element');
t.end(); t.end();
}); });
@ -33,7 +25,7 @@ test('dom: isContainClass: contains', (t) => {
const className = 'hello'; const className = 'hello';
isContainClass(el, className); isContainClass(el, className);
t.calledWith(contains, [className], 'should call contains'); t.ok(contains.calledWith(className), 'should call contains');
t.end(); t.end();
}); });
@ -42,111 +34,13 @@ test('dom: isContainClass: contains: array', (t) => {
const {contains} = el.classList; const {contains} = el.classList;
const className = 'hello'; const className = 'hello';
isContainClass(el, ['world', className, 'hello']); isContainClass(el, [
'world',
className,
'hello',
]);
t.calledWith(contains, [className], 'should call contains'); t.ok(contains.calledWith(className), 'should call contains');
t.end(); 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();
});

View file

@ -1,6 +1,8 @@
'use strict';
let list = []; let list = [];
export const add = (el, name, fn) => { module.exports.add = (el, name, fn) => {
list.push([ list.push([
el, el,
name, name,
@ -8,8 +10,11 @@ export const add = (el, name, fn) => {
]); ]);
}; };
export const clear = () => { module.exports.clear = () => {
list = []; list = [];
}; };
export const get = () => list; module.exports.get = () => {
return list;
};

View file

@ -1,5 +1,7 @@
import {test} from 'supertape'; 'use strict';
import * as eventStore from './event-store.js';
const test = require('supertape');
const eventStore = require('./event-store');
test('event-store: get', (t) => { test('event-store: get', (t) => {
const el = {}; const el = {};
@ -8,16 +10,11 @@ test('event-store: get', (t) => {
eventStore.add(el, name, fn); eventStore.add(el, name, fn);
const result = eventStore.get(); const result = eventStore.get();
const expected = [ const expected = [
[ [el, name, fn],
el,
name,
fn,
],
]; ];
t.deepEqual(result, expected); t.deepEqual(expected, result, 'should equal');
t.end(); t.end();
}); });
@ -32,6 +29,6 @@ test('event-store: clear', (t) => {
const result = eventStore.get(); const result = eventStore.get();
const expected = []; const expected = [];
t.deepEqual(result, expected); t.deepEqual(expected, result, 'should equal');
t.end(); t.end();
}); });

View file

@ -1,203 +1,250 @@
import itype from 'itype'; 'use strict';
import * as EventStore from './event-store.js';
/** const itype = require('itype');
* safe add event listener const EventStore = require('./event-store');
*
* @param type
* @param element - document by default
* @param listener
*/
export const add = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => { module.exports = new EventsProto();
const [name, fn, options] = args;
element.addEventListener(name, fn, options); function EventsProto() {
EventStore.add(element, name, fn); const Events = this;
});
return Events; const getEventOptions = (eventName) => {
}; if (eventName !== 'touchstart')
return false;
/** return {
* safe add event listener passive: true,
* };
* @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) { function parseArgs(eventName, element, listener, callback) {
listener = element; let isFunc;
element = null; 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':
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;
}
} }
add(type, element, once); /**
* safe add event listener
*
* @param type
* @param element {document by default}
* @param listener
*/
this.add = (type, element, listener) => {
checkType(type);
return Events; parseArgs(type, element, listener, (element, args) => {
}; const [name, fn, options] = args;
/** element.addEventListener(name, fn, options);
* safe remove event listener EventStore.add(element, name, fn);
* });
* @param type
* @param listener
* @param element - document by default
*/
export const remove = (type, element, listener) => {
checkType(type);
parseArgs(type, element, listener, (element, args) => { return Events;
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; * safe add event listener
const args = [ *
eventName, * @param type
element, * @param listener
listener, * @param element {document by default}
callback, */
]; this.addOnce = (type, element, listener) => {
const once = (event) => {
Events.remove(type, element, once);
listener(event);
};
const EVENT_NAME = 1; if (!listener) {
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; listener = element;
element = null; element = null;
} }
if (!element) this.add(type, element, once);
element = window;
callback(element, [ return Events;
eventName, };
listener,
getEventOptions(eventName),
]);
break;
case 'array': /**
* safe remove event listener
*
* @param type
* @param listener
* @param element {document by default}
*/
this.remove = (type, element, listener) => {
checkType(type);
for (const name of eventName) { parseArgs(type, element, listener, (element, args) => {
parseArgs(name, element, listener, callback); element.removeEventListener(...args);
} });
break; return Events;
};
case 'object': /**
* remove all added event listeners
*
* @param listener
*/
this.removeAll = () => {
const events = EventStore.get();
for (const name of Object.keys(eventName)) { for (const [el, name, fn] of events)
const eventListener = eventName[name]; el.removeEventListener(name, fn);
parseArgs(name, element, eventListener, callback); EventStore.clear();
} };
break; /**
* 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!');
} }
} }
const Events = {
add,
addClick,
addContextMenu,
addKey,
addLoad,
addOnce,
remove,
removeAll,
rmClick,
rmKey,
};

View file

@ -1,29 +1,37 @@
/* global CloudCmd */ /* global CloudCmd */
import itype from 'itype';
import {promisify} from 'es6-promisify'; 'use strict';
import * as load from '#dom/load';
import * as RESTful from '#dom/rest'; const itype = require('itype');
const currify = require('currify');
const {promisify} = require('es6-promisify');
const load = require('./load');
const RESTful = require('./rest');
const Promises = {}; const Promises = {};
const FILES_JSON = 'config|modules'; const FILES_JSON = 'config|modules';
const FILES_HTML = 'file|path|link|pathLink|media'; const FILES_HTML = 'file|path|link|pathLink|media';
const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload'; const FILES_HTML_ROOT = 'view/media-tmpl|config-tmpl|upload';
const DIR_HTML = '/tmpl/'; const DIR_HTML = '/tmpl/';
const DIR_HTML_FS = `${DIR_HTML}fs/`; const DIR_HTML_FS = DIR_HTML + 'fs/';
const DIR_JSON = '/json/'; const DIR_JSON = '/json/';
const timeout = getTimeoutOnce(2000); const timeout = getTimeoutOnce(2000);
export const get = getFile; const get = currify(getFile);
const unaryMap = (array, fn) => array.map((a) => fn(a));
function getFile(name) { module.exports.get = get;
async function getFile(name) {
const type = itype(name); const type = itype(name);
check(name); check(name);
if (type === 'string') if (type === 'string')
return getModule(name); return await getModule(name);
if (type === 'array') if (type === 'array')
return Promise.all(name.map(getFile)); return Promise.all(unaryMap(name, get));
} }
function check(name) { function check(name) {
@ -31,9 +39,9 @@ function check(name) {
throw Error('name could not be empty!'); throw Error('name could not be empty!');
} }
function getModule(name) { async function getModule(name) {
const regExpHTML = RegExp(FILES_HTML + '|' + FILES_HTML_ROOT); const regExpHTML = new RegExp(FILES_HTML + '|' + FILES_HTML_ROOT);
const regExpJSON = RegExp(FILES_JSON); const regExpJSON = new RegExp(FILES_JSON);
const isHTML = regExpHTML.test(name); const isHTML = regExpHTML.test(name);
const isJSON = regExpJSON.test(name); const isJSON = regExpJSON.test(name);
@ -45,13 +53,12 @@ function getModule(name) {
return getConfig(); return getConfig();
const path = getPath(name, isHTML, isJSON); const path = getPath(name, isHTML, isJSON);
return getSystemFile(path); return getSystemFile(path);
} }
function getPath(name, isHTML, isJSON) { function getPath(name, isHTML, isJSON) {
let path; let path;
const regExp = RegExp(FILES_HTML_ROOT); const regExp = new RegExp(FILES_HTML_ROOT);
const isRoot = regExp.test(name); const isRoot = regExp.test(name);
if (isHTML) { if (isHTML) {
@ -69,8 +76,8 @@ function getPath(name, isHTML, isJSON) {
} }
function showError(name) { function showError(name) {
const str = `Wrong file name: ${name}`; const str = 'Wrong file name: ' + name;
const error = Error(str); const error = new Error(str);
throw error; throw error;
} }
@ -134,3 +141,4 @@ function getTimeoutOnce(time) {
}, time); }, time);
}; };
} }

View file

@ -1,13 +1,20 @@
/* global DOM */ /* global DOM */
import createElement from '@cloudcmd/create-element';
'use strict';
const createElement = require('@cloudcmd/create-element');
const Images = module.exports;
const LOADING = 'loading'; const LOADING = 'loading';
const HIDDEN = 'hidden'; const HIDDEN = 'hidden';
const ERROR = 'error'; const ERROR = 'error';
const getLoadingType = () => isSVG() ? '-svg' : '-gif'; function getLoadingType() {
return isSVG() ? '-svg' : '-gif';
}
export const get = getElement; module.exports.get = getElement;
/** /**
* check SVG SMIL animation support * check SVG SMIL animation support
@ -36,7 +43,7 @@ function getElement() {
} }
/* Функция создаёт картинку загрузки */ /* Функция создаёт картинку загрузки */
export const loading = () => { module.exports.loading = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const {classList} = element;
const loadingImage = LOADING + getLoadingType(); const loadingImage = LOADING + getLoadingType();
@ -48,7 +55,7 @@ export const loading = () => {
}; };
/* Функция создаёт картинку ошибки загрузки */ /* Функция создаёт картинку ошибки загрузки */
export const error = () => { module.exports.error = () => {
const element = getElement(); const element = getElement();
const {classList} = element; const {classList} = element;
const loadingImage = LOADING + getLoadingType(); const loadingImage = LOADING + getLoadingType();
@ -59,21 +66,15 @@ export const error = () => {
return element; return element;
}; };
show.load = show; module.exports.show = show;
show.error = (text) => { module.exports.show.load = show;
const image = Images.error(); module.exports.show.error = error;
DOM.show(image);
image.title = text;
return image;
};
/** /**
* Function shows loading spinner * Function shows loading spinner
* position = {top: true}; * position = {top: true};
*/ */
export function show(position, panel) { function show(position, panel) {
const image = Images.loading(); const image = Images.loading();
const parent = image.parentElement; const parent = image.parentElement;
const refreshButton = DOM.getRefreshButton(panel); const refreshButton = DOM.getRefreshButton(panel);
@ -99,25 +100,34 @@ export function show(position, panel) {
return image; return image;
} }
function error(text) {
const image = Images.error();
DOM.show(image);
image.title = text;
return image;
}
/** /**
* hide load image * hide load image
*/ */
export function hide() { module.exports.hide = () => {
const element = Images.get(); const element = Images.get();
DOM.hide(element); DOM.hide(element);
return Images; return Images;
} };
export const setProgress = (value, title) => { module.exports.setProgress = (value, title) => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
if (!element) if (!element)
return Images; return Images;
element.setAttribute(DATA, `${value}%`); element.setAttribute(DATA, value + '%');
if (title) if (title)
element.title = title; element.title = title;
@ -125,7 +135,7 @@ export const setProgress = (value, title) => {
return Images; return Images;
}; };
export const clearProgress = () => { module.exports.clearProgress = () => {
const DATA = 'data-progress'; const DATA = 'data-progress';
const element = Images.get(); const element = Images.get();
@ -138,12 +148,3 @@ export const clearProgress = () => {
return Images; return Images;
}; };
const Images = {
clearProgress,
setProgress,
show,
hide,
get,
error,
loading,
};

File diff suppressed because it is too large Load diff

View file

@ -1,52 +0,0 @@
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();
});

216
client/dom/io.js Normal file
View file

@ -0,0 +1,216 @@
'use strict';
/* global CloudCmd*/
const itype = require('itype');
const {promisify} = require('es6-promisify');
const {FS} = require('../../common/cloudfunc');
const Images = require('./images');
const load = require('./load');
const imgPosition = {
top: true,
};
module.exports._replaceHash = replaceHash;
function replaceHash(url) {
/*
* if we send ajax request -
* no need in hash so we escape #
*/
return url.replace(/#/g, '%23');
}
module.exports.delete = promisify((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,
},
});
});
module.exports.patch = promisify((url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PATCH',
url: FS + url,
data,
callback,
imgPosition,
});
});
module.exports.write = promisify((url, data, callback) => {
const isFunc = itype.function(data);
if (!callback && isFunc) {
callback = data;
data = null;
}
sendRequest({
method: 'PUT',
url: FS + url,
data,
callback,
imgPosition,
});
});
module.exports.read = promisify((url, dataType, callback) => {
const notLog = !url.includes('?');
const isFunc = itype.function(dataType);
if (!callback && isFunc) {
callback = dataType;
dataType = 'text';
}
sendRequest({
method: 'GET',
url: FS + url,
callback,
notLog,
dataType,
});
});
module.exports.cp = promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/cp',
data,
callback,
imgPosition,
});
});
module.exports.pack = promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/pack',
data,
callback,
});
});
module.exports.extract = promisify((data, callback) => {
sendRequest({
method : 'PUT',
url : '/extract',
data,
callback,
});
});
module.exports.mv = promisify((data, callback) => {
sendRequest({
method : 'PUT',
url : '/mv',
data,
callback,
imgPosition,
});
});
module.exports.Config = {
read: promisify((callback) => {
sendRequest({
method: 'GET',
url: '/config',
callback,
imgPosition,
notLog: true,
});
}),
write: promisify((data, callback) => {
sendRequest({
method: 'PATCH',
url: '/config',
data,
callback,
imgPosition,
});
}),
};
module.exports.Markdown = {
read: promisify((url, callback) => {
sendRequest({
method: 'GET',
url: '/markdown' + url,
callback,
imgPosition,
notLog: true,
});
}),
render: promisify((data, callback) => {
sendRequest({
method: 'PUT',
url: '/markdown',
data,
callback,
imgPosition,
notLog: true,
});
}),
};
function sendRequest(params) {
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;
p.callback(Error(text));
},
success: (data) => {
Images.hide();
if (!p.notLog)
CloudCmd.log(data);
p.callback(null, data);
},
});
}

View file

@ -1,11 +1,15 @@
import {test} from 'supertape'; 'use strict';
import {_replaceHash} from './send-request.js';
const test = require('supertape');
const io = require('./io');
test('cloudcmd: client: io: replaceHash', (t) => { test('cloudcmd: client: io: replaceHash', (t) => {
const {_replaceHash} = io;
const url = '/hello/####world'; const url = '/hello/####world';
const result = _replaceHash(url); const result = _replaceHash(url);
const expected = '/hello/%23%23%23%23world'; const expected = '/hello/%23%23%23%23world';
t.equal(result, expected); t.equal(result, expected, 'should equal');
t.end(); t.end();
}); });

View file

@ -1,166 +0,0 @@
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,
});
},
};

View file

@ -1,61 +0,0 @@
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();
});

View file

@ -1,48 +0,0 @@
/* 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');
}

View file

@ -1,58 +1,60 @@
/* global CloudCmd */ 'use strict';
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';
export const loadRemote = callbackify(async (name, options) => { /* global CloudCmd */
const rendy = require('rendy');
const itype = require('itype');
const load = require('load.js');
const tryToCatch = require('try-to-catch');
const {findObjByNameInArr} = require('../../common/util');
const Files = require('./files');
module.exports = (name, options, callback = options) => {
const {prefix, config} = CloudCmd; const {prefix, config} = CloudCmd;
const o = options; const o = options;
if (o.name && window[o.name]) if (o.name && window[o.name])
return; return callback();
const modules = await Files.get('modules'); Files.get('modules').then(async (modules) => {
const online = config('online') && navigator.onLine;
const module = findObjByNameInArr(modules.remote, name);
const online = config('online') && navigator.onLine; const isArray = itype.array(module.local);
const module = findObjByNameInArr(modules.remote, name); const {version} = module;
const isArray = itype.array(module.local); let remoteTmpls;
const {version} = module; let local;
let remoteTmpls; if (isArray) {
let local; remoteTmpls = module.remote;
local = module.local;
} else {
remoteTmpls = [module.remote];
local = [module.local];
}
if (isArray) { const localURL = local.map((url) => {
remoteTmpls = module.remote; return prefix + url;
({local} = module); });
} else {
remoteTmpls = [module.remote];
local = [module.local];
}
const localURL = []; const remoteURL = remoteTmpls.map((tmpl) => {
return rendy(tmpl, {
version,
});
});
for (const url of local) { if (online) {
localURL.push(prefix + url); const [e] = await tryToCatch(load.parallel, remoteURL);
}
const remoteURL = []; if (!e)
return callback();
}
for (const tmpl of remoteTmpls) { const [e] = await tryToCatch(load.parallel, localURL);
remoteURL.push(rendy(tmpl, { callback(e);
version, });
})); };
}
if (online) {
const [e] = await tryToCatch(load.parallel, remoteURL);
if (!e)
return;
}
await load.parallel(localURL);
});

View file

@ -1,8 +1,12 @@
import itype from 'itype'; 'use strict';
import jonny from 'jonny';
import Emitify from 'emitify'; const itype = require('itype');
import exec from 'execon'; const jonny = require('jonny');
import * as Images from '#dom/images'; const Emitify = require('emitify');
const exec = require('execon');
const Images = require('./images');
module.exports.getIdBySrc = getIdBySrc;
/** /**
* Function gets id by src * Function gets id by src
@ -10,18 +14,17 @@ import * as Images from '#dom/images';
* *
* Example: http://domain.com/1.js -> 1_js * Example: http://domain.com/1.js -> 1_js
*/ */
export function getIdBySrc(src) { function getIdBySrc(src) {
const isStr = itype.string(src); const isStr = itype.string(src);
if (!isStr) if (!isStr)
return; return;
if (src.includes(':')) if (~src.indexOf(':'))
src += '-join'; src += '-join';
const num = src.lastIndexOf('/') + 1; const num = src.lastIndexOf('/') + 1;
const sub = src.substr(src, num); const sub = src.substr(src, num);
const id = src const id = src
.replace(sub, '') .replace(sub, '')
.replace(/\./g, '-'); .replace(/\./g, '-');
@ -34,15 +37,15 @@ export function getIdBySrc(src) {
* *
* @param params * @param params
*/ */
export const ajax = (params) => { module.exports.ajax = (params) => {
const p = params; const p = params;
const isObject = itype.object(p.data); const isObject = itype.object(p.data);
const isArray = itype.array(p.data); const isArray = itype.array(p.data);
const isArrayBuf = itype(p.data) === 'arraybuffer'; const isArrayBuf = itype(p.data) === 'arraybuffer';
const type = p.type || p.method || 'GET'; const type = p.type || p.method || 'GET';
const {
const {headers = {}} = p; headers = {},
} = p;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open(type, p.url, true); xhr.open(type, p.url, true);
@ -60,7 +63,7 @@ export const ajax = (params) => {
if (!isArrayBuf && isObject || isArray) if (!isArrayBuf && isObject || isArray)
data = jonny.stringify(p.data); data = jonny.stringify(p.data);
else else
({data} = p); data = p.data;
xhr.onreadystatechange = (event) => { xhr.onreadystatechange = (event) => {
const xhr = event.target; const xhr = event.target;
@ -78,7 +81,7 @@ export const ajax = (params) => {
return exec(p.error, xhr); return exec(p.error, xhr);
const notText = p.dataType !== 'text'; const notText = p.dataType !== 'text';
const isContain = type.includes(TYPE_JSON); const isContain = ~type.indexOf(TYPE_JSON);
let data = xhr.response; let data = xhr.response;
@ -91,11 +94,12 @@ export const ajax = (params) => {
xhr.send(data); xhr.send(data);
}; };
export const put = (url, body) => { module.exports.put = (url, body) => {
const emitter = Emitify(); const emitter = Emitify();
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
url = encodeURI(url).replace(/#/g, '#'); url = encodeURI(url)
.replace('#', '%23');
xhr.open('put', url, true); xhr.open('put', url, true);
@ -116,10 +120,8 @@ export const put = (url, body) => {
if (!over) if (!over)
return; return;
if (xhr.status === OK) { if (xhr.status === OK)
emitter.emit('progress', 100);
return emitter.emit('end'); return emitter.emit('end');
}
const error = Error(xhr.responseText); const error = Error(xhr.responseText);
emitter.emit('error', error); emitter.emit('error', error);
@ -129,3 +131,4 @@ export const put = (url, body) => {
return emitter; return emitter;
}; };

View file

@ -1,27 +1,23 @@
'use strict';
/* global CloudCmd */ /* 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 capitalize = require('just-capitalize');
const {
refresh = CloudCmd.refresh,
Dialog = _Dialog,
currentFile = _currentFile,
} = overrides;
const { const Dialog = require('../dialog');
isCurrentFile, const Storage = require('../storage');
getCurrentName, const RESTful = require('../rest');
getCurrentFile, const {
getCurrentByName, isCurrentFile,
getCurrentType, getCurrentName,
getCurrentDirPath, getCurrentFile,
setCurrentName, getCurrentByName,
} = currentFile; getCurrentType,
getCurrentDirPath,
setCurrentName,
} = require('../current-file');
module.exports = async (current) => {
if (!isCurrentFile(current)) if (!isCurrentFile(current))
current = getCurrentFile(); current = getCurrentFile();
@ -50,11 +46,12 @@ export default async (current, overrides = {}) => {
return; return;
const dirPath = getCurrentDirPath(); const dirPath = getCurrentDirPath();
const files = {
from : dirPath + from,
to : dirPath + to,
};
const fromFull = `${dirPath}${from}`; const [e] = await RESTful.mv(files);
const toFull = `${dirPath}${to}`;
const [e] = await RESTful.rename(fromFull, toFull);
if (e) if (e)
return; return;
@ -62,5 +59,5 @@ export default async (current, overrides = {}) => {
setCurrentName(to, current); setCurrentName(to, current);
Storage.remove(dirPath); Storage.remove(dirPath);
refresh(); CloudCmd.refresh();
}; };

View file

@ -1,26 +1,37 @@
import {test, stub} from 'supertape'; 'use strict';
import renameCurrent from './rename-current.js';
const test = require('supertape');
const stub = require('@cloudcmd/stub');
const mockRequire = require('mock-require');
const {reRequire} = mockRequire;
test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => { test('cloudcmd: client: dom: renameCurrent: isCurrentFile', async (t) => {
const current = {}; const current = {};
const isCurrentFile = stub(); const isCurrentFile = stub();
const currentFile = stubCurrentFile({ mockRequire('../dialog', stubDialog());
mockRequire('../current-file', stubCurrentFile({
isCurrentFile, isCurrentFile,
}); }));
await renameCurrent(current, { const renameCurrent = reRequire('./rename-current');
Dialog: stubDialog(), await renameCurrent(current);
currentFile,
});
t.calledWith(isCurrentFile, [current], 'should call isCurrentFile'); t.ok(isCurrentFile.calledWith(current), 'should call isCurrentFile');
t.end(); t.end();
}); });
test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => { test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const current = {}; const current = {};
const name = 'hello'; const name = 'hello';
const {CloudCmd} = global;
const CloudCmdStub = {
refresh: stub(),
};
global.CloudCmd = CloudCmdStub;
const prompt = stub().returns([null, name]); const prompt = stub().returns([null, name]);
const confirm = stub().returns([true]); const confirm = stub().returns([true]);
@ -28,24 +39,23 @@ test('cloudcmd: client: dom: renameCurrent: file exist', async (t) => {
const getCurrentByName = stub().returns(current); const getCurrentByName = stub().returns(current);
const getCurrentType = stub().returns('directory'); const getCurrentType = stub().returns('directory');
const Dialog = stubDialog({ mockRequire('../dialog', stubDialog({
confirm, confirm,
prompt, prompt,
}); }));
const currentFile = stubCurrentFile({ mockRequire('../current-file', stubCurrentFile({
getCurrentByName, getCurrentByName,
getCurrentType, getCurrentType,
}); }));
await renameCurrent(null, { const renameCurrent = reRequire('./rename-current');
Dialog, await renameCurrent();
currentFile,
});
const expected = 'Directory "hello" already exists. Proceed?'; const expected = 'Directory "hello" already exists. Proceed?';
global.CloudCmd = CloudCmd;
t.calledWith(confirm, [expected], 'should call confirm'); t.ok(confirm.calledWith(expected), 'should call confirm');
t.end(); t.end();
}); });
@ -84,3 +94,4 @@ const stubCurrentFile = (fns = {}) => {
setCurrentName, setCurrentName,
}; };
}; };

View file

@ -1,8 +1,12 @@
import {tryToCatch} from 'try-to-catch'; 'use strict';
import * as Dialog from '#dom/dialog';
import * as Images from '#dom/images'; const tryToCatch = require('try-to-catch');
import {encode} from '#common/entity';
import * as IO from './io/index.js'; const {encode} = require('../../common/entity');
const Images = require('./images');
const IO = require('./io');
const Dialog = require('./dialog');
const handleError = (promise) => async (...args) => { const handleError = (promise) => async (...args) => {
const [e, data] = await tryToCatch(promise, ...args); const [e, data] = await tryToCatch(promise, ...args);
@ -18,23 +22,22 @@ const handleError = (promise) => async (...args) => {
return [e, data]; return [e, data];
}; };
export const remove = handleError(IO.remove); module.exports.delete = handleError(IO.delete);
export const patch = handleError(IO.patch); module.exports.patch = handleError(IO.patch);
export const write = handleError(IO.write); module.exports.write = handleError(IO.write);
export const createDirectory = handleError(IO.createDirectory); module.exports.read = handleError(IO.read);
export const read = handleError(IO.read); module.exports.cp = handleError(IO.cp);
export const copy = handleError(IO.copy); module.exports.pack = handleError(IO.pack);
export const pack = handleError(IO.pack); module.exports.extract = handleError(IO.extract);
export const extract = handleError(IO.extract); module.exports.mv = handleError(IO.mv);
export const move = handleError(IO.move);
export const rename = handleError(IO.rename);
export const Config = { module.exports.Config = {
read: handleError(IO.Config.read), read: handleError(IO.Config.read),
write: handleError(IO.Config.write), write: handleError(IO.Config.write),
}; };
export const Markdown = { module.exports.Markdown = {
read: handleError(IO.Markdown.read), read: handleError(IO.Markdown.read),
render: handleError(IO.Markdown.render), render: handleError(IO.Markdown.render),
}; };

View file

@ -1,18 +1,22 @@
import {alert, prompt} from '#dom/dialog'; 'use strict';
import {getRegExp} from '#common/util';
import {getCurrentName} from './current-file.js';
import {
isSelected,
toggleSelectedFile,
} from './cmd.js';
let SelectType = '*.*'; let SelectType = '*.*';
export const selectByPattern = async (msg, files) => { const {getRegExp} = require('../../common/util');
const {
alert,
prompt,
} = require('./dialog');
const DOM = require('.');
module.exports = async (msg, files) => {
if (!files) if (!files)
return; return;
const allMsg = `Specify file type for ${msg} selection`; const allMsg = `Specify file type for ${msg} selection`;
/* eslint require-atomic-updates: 0 */
const [cancel, type] = await prompt(allMsg, SelectType); const [cancel, type] = await prompt(allMsg, SelectType);
if (cancel) if (cancel)
@ -24,23 +28,24 @@ export const selectByPattern = async (msg, files) => {
let matches = 0; let matches = 0;
for (const current of files) { for (const current of files) {
const name = getCurrentName(current); const name = DOM.getCurrentName(current);
if (name === '..' || !regExp.test(name)) if (name === '..' || !regExp.test(name))
continue; continue;
++matches; ++matches;
let selected = isSelected(current); let isSelected = DOM.isSelected(current);
const shouldSel = msg === 'expand'; const shouldSel = msg === 'expand';
if (shouldSel) if (shouldSel)
selected = !selected; isSelected = !isSelected;
if (selected) if (isSelected)
toggleSelectedFile(current); DOM.toggleSelectedFile(current);
} }
if (!matches) if (!matches)
alert('No matches found!'); alert('No matches found!');
}; };

View file

@ -1,26 +1,38 @@
'use strict';
const tryCatch = require('try-catch');
const {parse, stringify} = JSON; const {parse, stringify} = JSON;
const isObj = (a) => typeof a === 'object';
export const set = (name, data) => { module.exports.set = (name, data) => {
localStorage.setItem(name, data); const primitive = !isObj(data) ? data : stringify(data);
localStorage.setItem(name, primitive);
}; };
export const setJson = (name, data) => { module.exports.get = async (name) => {
localStorage.setItem(name, stringify(data));
};
export const get = (name) => {
return localStorage.getItem(name);
};
export const getJson = (name) => {
const data = localStorage.getItem(name); const data = localStorage.getItem(name);
return parse(data); const [, result = data] = tryCatch(parse, data);
return result;
}; };
export const clear = () => { module.exports.clear = () => {
localStorage.clear(); localStorage.clear();
}; };
export const remove = (item) => { module.exports.remove = (item) => {
localStorage.removeItem(item); localStorage.removeItem(item);
}; };
module.exports.removeMatch = (string) => {
const reg = RegExp('^' + string + '.*$');
const test = (a) => reg.test(a);
const remove = (a) => localStorage.removeItem(a);
Object.keys(localStorage)
.filter(test)
.forEach(remove);
};

View file

@ -1,105 +0,0 @@
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();
});

View file

@ -1,19 +1,25 @@
'use strict';
/* global CloudCmd */ /* 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 loadFile = wraptile(_loadFile); const {eachSeries} = require('execon');
const wraptile = require('wraptile');
const load = require('./load');
const Images = require('./images');
const {alert} = require('./dialog');
const {FS} = require('../../common/cloudfunc');
const onEnd = wraptile(_onEnd); const onEnd = wraptile(_onEnd);
const loadFile = wraptile(_loadFile);
export const uploadFiles = (dir, files) => { const {getCurrentDirPath: getPathWhenRootEmpty} = require('.');
module.exports = (dir, files) => {
if (!files) { if (!files) {
files = dir; files = dir;
dir = getCurrentDirPath(); dir = getPathWhenRootEmpty();
} }
const n = files.length; const n = files.length;
@ -49,8 +55,7 @@ function _loadFile(dir, n, file, callback) {
++i; ++i;
load load.put(api + path, file)
.put(api + path, file)
.on('error', showError) .on('error', showError)
.on('end', callback) .on('end', callback)
.on('progress', (count) => { .on('progress', (count) => {
@ -65,3 +70,4 @@ function _loadFile(dir, n, file, callback) {
function showError({message}) { function showError({message}) {
alert(message); alert(message);
} }

View file

@ -1,13 +1,40 @@
'use strict';
/* global DOM */ /* global DOM */
const Info = DOM.CurrentInfo;
/** /**
* Функция генерирует JSON из html-таблицы файлов и * Функция генерирует JSON из html-таблицы файлов и
* используеться при первом заходе в корень * используеться при первом заходе в корень
*/ */
export const getJsonFromFileTable = () => { module.exports = () => {
const Info = DOM.CurrentInfo;
const path = DOM.getCurrentDirPath(); const path = DOM.getCurrentDirPath();
const infoFiles = Info.files || []; 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);
const type = DOM.getCurrentType(current);
return {
name,
size,
mode,
owner,
date,
type,
};
};
const files = infoFiles const files = infoFiles
.filter(notParent) .filter(notParent)
.map(parse); .map(parse);
@ -20,25 +47,3 @@ export const getJsonFromFileTable = () => {
return fileTable; 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,
};
};

View file

@ -1,15 +0,0 @@
export const createBinder = () => {
let binded = false;
return {
isBind() {
return binded;
},
setBind() {
binded = true;
},
unsetBind() {
binded = false;
},
};
};

View file

@ -1,28 +0,0 @@
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();
});

View file

@ -1,544 +1,500 @@
/* global CloudCmd, DOM */ /* 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 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');
const Chars = fullstore(); 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([]); Chars([]);
const {assign} = Object; KeyProto.prototype = KEY;
const binder = createBinder(); CloudCmd.Key = KeyProto;
const {loadDir} = CloudCmd;
const bind = () => { function KeyProto() {
Events.addKey(listener, true); let Binded;
binder.setBind();
};
export const Key = assign(binder, KEY, { const Key = this;
bind,
});
export const _listener = listener; this.isBind = () => {
return Binded;
};
function getChar(event) { this.setBind = () => {
/* Binded = true;
* event.keyIdentifier deprecated in chrome v51 };
* but event.key is absent in chrome <= v51
*/
const {
key,
shift,
keyCode,
keyIdentifier,
} = event;
const char = key || fromCharCode(keyIdentifier); this.unsetBind = () => {
const symbol = getSymbol(shift, keyCode); Binded = false;
};
return [symbol, char]; this.bind = () => {
} Events.addKey(listener, true);
Binded = true;
};
async function listener(event, overrides = {}) { function getChar(event) {
const { /*
config = CloudCmd.config, * event.keyIdentifier deprecated in chrome v51
_config = CloudCmd._config, * but event.key is absent in chrome <= v51
switchKey = _switchKey, */
vim = _vim,
} = overrides;
const {keyCode} = event; if (event.key)
return event.key;
// strange chrome bug calls listener twice return fromCharCode(event.keyIdentifier);
// 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 ''; async function listener(event) {
} const {keyCode} = event;
function fromCharCode(keyIdentifier) { // strange chrome bug calles listener twice
const code = keyIdentifier.substring(2); // in second time event misses a lot fields
const hex = parseInt(code, 16); if (typeof event.altKey === 'undefined')
return;
return String.fromCharCode(hex); 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);
async function _switchKey(event) { let char = getChar(event);
const Info = DOM.CurrentInfo; let isSymbol = ~['.', '_', '-', '+', '='].indexOf(char);
let i;
let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
const { if (!isSymbol) {
name, isSymbol = getSymbol(shift, keyCode);
panel,
path,
isDir,
} = Info;
const { if (isSymbol)
Operation, char = isSymbol;
changeDir, }
config,
} = CloudCmd;
const {keyCode} = event; if (!Key.isBind())
return;
const alt = event.altKey; const isVim = CloudCmd.config('vim');
const shift = event.shiftKey;
const ctrl = event.ctrlKey;
const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
if (current) { if (!isVim && !isNumpad && !alt && !ctrl && !meta && (isBetween || isSymbol))
prev = current.previousSibling; return setCurrentByChar(char, Chars);
next = current.nextSibling;
Chars([]);
await switchKey(event);
if (keyCode >= KEY.F1 && keyCode <= KEY.F10)
return;
if (isVim)
vim(char, event);
} }
switch(keyCode) { function getSymbol(shift, keyCode) {
case KEY.TAB: switch(keyCode) {
DOM.changePanel(); case KEY.DOT:
event.preventDefault(); return '.';
break;
case KEY.INSERT: case KEY.HYPHEN:
DOM return shift ? '_' : '-';
.toggleSelectedFile(current)
.setCurrentFile(next);
break;
case KEY.INSERT_MAC: case KEY.EQUAL:
DOM return shift ? '+' : '=';
.toggleSelectedFile(current) }
.setCurrentFile(next); }
break;
case KEY.DELETE: function fromCharCode(keyIdentifier) {
if (shift) const code = keyIdentifier.substring(2);
Operation.show('delete:silent'); const hex = parseInt(code, 16);
else const char = String.fromCharCode(hex);
Operation.show('delete');
break; return char;
}
case KEY.ASTERISK: async function switchKey(event) {
DOM.toggleAllSelectedFiles(current); let i;
break; let isSelected;
let prev;
let next;
let current = Info.element;
let dataName;
case KEY.PLUS: const {
DOM.expandSelection(); name,
event.preventDefault(); panel,
break; path,
isDir,
} = Info;
case KEY.MINUS: const {Operation} = CloudCmd;
DOM.shrinkSelection(); const {keyCode} = event;
event.preventDefault();
break;
case KEY.F1: const alt = event.altKey;
CloudCmd.Help.show(); const shift = event.shiftKey;
event.preventDefault(); const ctrl = event.ctrlKey;
break; const meta = event.metaKey;
const ctrlMeta = ctrl || meta;
case KEY.F2: if (current) {
CloudCmd.UserMenu.show(); prev = current.previousSibling;
break; next = current.nextSibling;
}
case KEY.F3: switch(keyCode) {
event.preventDefault(); case Key.TAB:
DOM.changePanel();
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(); event.preventDefault();
} break;
break; case Key.INSERT:
DOM .toggleSelectedFile(current)
.setCurrentFile(next);
break;
/* navigation on file table: * case Key.INSERT_MAC:
* in case of pressing button 'up', * DOM .toggleSelectedFile(current)
* select previous row */ .setCurrentFile(next);
case KEY.UP: break;
if (shift)
DOM.toggleSelectedFile(current);
DOM.setCurrentFile(prev); case Key.DELETE:
event.preventDefault(); if (shift)
break; Operation.show('delete:silent');
/* 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 else
CloudCmd.EditNames.show(); 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:
if (Info.isDir)
await loadDir({
path,
});
else if (shift)
CloudCmd.Markdown.show(path);
else if (ctrlMeta)
CloudCmd.sortPanel('name');
else
CloudCmd.View.show();
event.preventDefault(); event.preventDefault();
} break;
break; case Key.F4:
if (shift)
CloudCmd.EditFileVim.show();
else
CloudCmd.EditFile.show();
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(); event.preventDefault();
} break;
break; case Key.F5:
if (ctrlMeta)
CloudCmd.sortPanel('date');
else if (alt)
Operation.show('pack');
else
Operation.show('copy');
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(); event.preventDefault();
} break;
break; case Key.F6:
if (ctrlMeta)
CloudCmd.sortPanel('size');
else if (shift)
DOM.renameCurrent(current);
else
Operation.show('move');
case KEY.DOT: event.preventDefault();
if (meta && shift) { break;
const showDotFiles = !CloudCmd.config('showDotFiles');
CloudCmd._config('showDotFiles', showDotFiles); case Key.F7:
CloudCmd.refresh(); if (shift)
await DOM.RESTful.Config.write({ DOM.promptNewFile();
showDotFiles, 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(current, callback);
}); });
}
break; 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;
case Key.ENTER:
if (Info.isDir)
await loadDir({path});
else
CloudCmd.View.show();
break;
case Key.BACKSPACE:
CloudCmd.goToParentDir();
event.preventDefault();
break;
case Key.BACKSLASH:
if (ctrlMeta)
await 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...');
await DOM.Storage.clear();
CloudCmd.log('storage cleared');
event.preventDefault();
}
break;
}
} }
} }

View file

@ -1,56 +0,0 @@
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();
});

View file

@ -1,58 +1,81 @@
export const BACKSPACE = 8; 'use strict';
export const TAB = 9;
export const ENTER = 13; module.exports = {
export const CAPSLOCK = 20; BACKSPACE : 8,
export const ESC = 27; TAB : 9,
export const SPACE = 32; ENTER : 13,
export const PAGE_UP = 33; ESC : 27,
export const PAGE_DOWN = 34;
export const END = 35; SPACE : 32,
export const HOME = 36; PAGE_UP : 33,
export const LEFT = 37; PAGE_DOWN : 34,
export const UP = 38; END : 35,
export const RIGHT = 39; HOME : 36,
export const DOWN = 40;
export const INSERT = 45; LEFT : 37,
export const DELETE = 46; UP : 38,
export const ZERO = 48; RIGHT : 39,
export const SEMICOLON = 52; DOWN : 40,
export const A = 65;
export const C = 67; INSERT : 45,
export const D = 68; DELETE : 46,
export const G = 71;
export const J = 74; ZERO : 48,
export const K = 75;
export const L = 76; SEMICOLON : 52,
export const M = 77;
export const O = 79; COLON : 54,
export const P = 80;
export const Q = 81; A : 65,
export const R = 82;
export const S = 83; C : 67,
export const T = 84; D : 68,
export const U = 85;
export const V = 86; G : 71,
export const X = 88;
export const Z = 90; J : 74,
export const INSERT_MAC = 96; K : 75,
export const ASTERISK = 106;
export const PLUS = 107; M : 77,
export const MINUS = 109;
export const F1 = 112; O : 79,
export const F2 = 113; P : 80,
export const F3 = 114; Q : 81,
export const F4 = 115; R : 82,
export const F5 = 116; S : 83,
export const F6 = 117; T : 84,
export const F7 = 118; U : 85,
export const F8 = 119;
export const F9 = 120; V : 86,
export const F10 = 121;
export const COLON = 186; X : 88,
export const EQUAL = 187;
export const HYPHEN = 189; Z : 90,
export const DOT = 190;
export const SLASH = 191; INSERT_MAC : 96,
export const TRA = 192;
export const BACKSLASH = 220; ASTERISK : 106,
export const BRACKET_CLOSE = 221; 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,
};

View file

@ -1,15 +1,18 @@
/* global DOM */ /* global DOM */
import {escapeRegExp} from '#common/util';
export default function setCurrentByChar(char, charStore) { 'use strict';
const Info = DOM.CurrentInfo;
const Info = DOM.CurrentInfo;
const {escapeRegExp} = require('../../common/util');
module.exports = function setCurrentByChar(char, charStore) {
let firstByName; let firstByName;
let skipCount = 0; let skipCount = 0;
let set = false; let setted = false;
let i = 0; let i = 0;
const escapeChar = escapeRegExp(char); const escapeChar = escapeRegExp(char);
const regExp = new RegExp(`^${escapeChar}.*$`, 'i'); const regExp = new RegExp('^' + escapeChar + '.*$', 'i');
const {files} = Info; const {files} = Info;
const chars = charStore(); const chars = charStore();
const n = chars.length; const n = chars.length;
@ -28,14 +31,12 @@ export default function setCurrentByChar(char, charStore) {
const isTest = (a) => regExp.test(a); const isTest = (a) => regExp.test(a);
const isRoot = (a) => a === '..'; const isRoot = (a) => a === '..';
const not = (f) => (a) => !f(a); const not = (f) => (a) => !f(a);
const setCurrent = (name) => { const setCurrent = (name) => {
const byName = DOM.getCurrentByName(name); const byName = DOM.getCurrentByName(name);
if (!skipCount) { if (!skipCount) {
set = true; setted = true;
DOM.setCurrentFile(byName); DOM.setCurrentFile(byName);
return true; return true;
} }
@ -50,8 +51,9 @@ export default function setCurrentByChar(char, charStore) {
.filter(not(isRoot)) .filter(not(isRoot))
.some(setCurrent); .some(setCurrent);
if (!set) { if (!setted) {
DOM.setCurrentFile(firstByName); DOM.setCurrentFile(firstByName);
charStore([char]); charStore([char]);
} }
} };

View file

@ -1,10 +1,12 @@
import {fullstore} from 'fullstore'; 'use strict';
import limier from 'limier';
const fullstore = require('fullstore');
const limier = require('limier');
const searchStore = fullstore([]); const searchStore = fullstore([]);
const searchIndex = fullstore(0); const searchIndex = fullstore(0);
export const find = (value, names) => { module.exports.find = (value, names) => {
const result = limier(value, names); const result = limier(value, names);
searchStore(result); searchStore(result);
@ -13,7 +15,7 @@ export const find = (value, names) => {
return result; return result;
}; };
export const findNext = () => { module.exports.findNext = () => {
const names = searchStore(); const names = searchStore();
const index = next(searchIndex(), names.length); const index = next(searchIndex(), names.length);
@ -21,7 +23,7 @@ export const findNext = () => {
return names[searchIndex()]; return names[searchIndex()];
}; };
export const findPrevious = () => { module.exports.findPrevious = () => {
const names = searchStore(); const names = searchStore();
const index = previous(searchIndex(), names.length); const index = previous(searchIndex(), names.length);
@ -29,8 +31,8 @@ export const findPrevious = () => {
return names[index]; return names[index];
}; };
export const _next = next; module.exports._next = next;
export const _previous = previous; module.exports._previous = previous;
function next(index, length) { function next(index, length) {
if (index === length - 1) if (index === length - 1)
@ -45,3 +47,4 @@ function previous(index, length) {
return --index; return --index;
} }

View file

@ -1,14 +1,16 @@
import test from 'supertape'; 'use strict';
import {getDOM} from './globals.fixture.js';
import { const test = require('supertape');
const dir = './';
const {getDOM} = require('./globals.fixture');
global.DOM = getDOM();
const {
_next, _next,
_previous, _previous,
find, } = require(dir + 'find');
findNext,
findPrevious,
} from './find.js';
globalThis.DOM = getDOM();
test('cloudcmd: client: vim: _next', (t) => { test('cloudcmd: client: vim: _next', (t) => {
const result = _next(1, 2); const result = _next(1, 2);
@ -17,13 +19,6 @@ test('cloudcmd: client: vim: _next', (t) => {
t.end(); 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) => { test('cloudcmd: client: vim: _previous', (t) => {
const result = _previous(0, 2); const result = _previous(0, 2);
@ -31,25 +26,3 @@ test('cloudcmd: client: vim: _previous', (t) => {
t.end(); 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();
});

View file

@ -1,12 +1,13 @@
const noop = () => {}; 'use strict';
export const getDOM = () => { module.exports.getDOM = () => {
const prompt = Promise.resolve.bind(Promise); const prompt = Promise.resolve.bind(Promise);
const CurrentInfo = { const CurrentInfo = {
element: {}, element: {},
files: [], files: [],
}; };
const noop = () => {};
const Buffer = { const Buffer = {
copy: noop, copy: noop,
paste: noop, paste: noop,
@ -27,20 +28,16 @@ export const getDOM = () => {
getCurrentName: noop, getCurrentName: noop,
setCurrentByName: noop, setCurrentByName: noop,
toggleSelectedFile: noop, toggleSelectedFile: noop,
prompNewDirectory: noop,
promptNewFile: noop,
}; };
}; };
export const getCloudCmd = () => { module.exports.getCloudCmd = () => {
const show = () => {}; const show = () => {};
return { return {
Operation: { Operation: {
show, show,
}, },
config: noop,
_config: noop,
}; };
}; };

View file

@ -1,167 +1,102 @@
import vim from './vim.js'; 'use strict';
import * as finder from './find.js';
import {
setCurrent,
selectFileNotParent,
} from './set-current.js';
export default (key, event, overrides = {}) => { /* global CloudCmd */
const defaults = { /* global DOM */
...globalThis.DOM,
...globalThis.CloudCmd,
};
const deps = { const vim = require('./vim');
...defaults, const finder = require('./find');
...overrides,
};
const operations = getOperations(event, deps); const Info = DOM.CurrentInfo;
const {Dialog} = DOM;
vim(key, operations, deps); module.exports = async (key, event) => {
const operations = getOperations(event);
await vim(key, operations);
}; };
const getOperations = (event, deps) => { const getOperations = (event) => {
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 { return {
makeFile: createMakeFile({ escape: DOM.unselectFiles,
promptNewFile,
preventDefault,
stopImmediatePropagation,
}),
findNext: createFindNext({
setCurrentByName,
}),
findPrevious: createFindPrevious({
setCurrentByName,
}),
escape: unselectFiles,
rename: () => {
event.preventDefault();
renameCurrent();
},
remove: () => { remove: () => {
Operation.show('delete'); CloudCmd.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: () => { copy: () => {
Buffer.copy(); DOM.Buffer.copy();
unselectFiles(); DOM.unselectFiles();
}, },
select: () => { select: () => {
const current = Info.element; const current = Info.element;
toggleSelectedFile(current); DOM.toggleSelectedFile(current);
}, },
paste: DOM.Buffer.paste,
paste: Buffer.paste,
moveNext: ({count, isVisual, isDelete}) => { moveNext: ({count, isVisual, isDelete}) => {
setCurrent('next', { setCurrent('next', {
count, count,
isVisual, isVisual,
isDelete, isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
}); });
}, },
movePrevious: ({count, isVisual, isDelete}) => { movePrevious: ({count, isVisual, isDelete}) => {
setCurrent('previous', { setCurrent('previous', {
count, count,
isVisual, isVisual,
isDelete, isDelete,
}, {
Info,
setCurrentFile,
unselectFiles,
Operation,
}); });
}, },
find: async () => { find: async () => {
preventDefault(); event.preventDefault();
const [, value] = await prompt('Find', ''); const [, value] = await Dialog.prompt('Find', '');
if (!value) if (!value)
return; return;
const names = Info.files.map(getCurrentName); const names = Info.files.map(DOM.getCurrentName);
const [result] = finder.find(value, names); const [result] = finder.find(value, names);
setCurrentByName(result); DOM.setCurrentByName(result);
},
findNext: () => {
const name = finder.findNext();
DOM.setCurrentByName(name);
},
findPrevious: () => {
const name = finder.findPrevious();
DOM.setCurrentByName(name);
}, },
}; };
}; };
export const selectFile = selectFileNotParent; module.exports.selectFile = selectFile;
const _createFindPrevious = (overrides = {}) => () => { function selectFile(current) {
const {setCurrentByName} = overrides; const name = DOM.getCurrentName(current);
const name = finder.findPrevious();
setCurrentByName(name); if (name === '..')
}; return;
const _createFindNext = (overrides = {}) => () => { DOM.selectFile(current);
const {setCurrentByName} = overrides; }
const name = finder.findNext();
setCurrentByName(name); function setCurrent(sibling, {count, isVisual, isDelete}) {
}; let current = Info.element;
const select = isVisual ? selectFile : DOM.unselectFile;
const _createMakeFile = (overrides = {}) => () => { select(current);
const {
promptNewFile, const position = `${sibling}Sibling`;
stopImmediatePropagation, for (let i = 0; i < count; i++) {
preventDefault, const next = current[position];
} = overrides;
if (!next)
break;
current = next;
select(current);
}
DOM.setCurrentFile(current);
if (isDelete)
CloudCmd.Operation.show('delete');
}
stopImmediatePropagation();
preventDefault();
promptNewFile();
};

View file

@ -1,35 +1,46 @@
import {test, stub} from 'supertape'; 'use strict';
import {getDOM, getCloudCmd} from './globals.fixture.js';
import vim, {selectFile as vimSelectFile} from './index.js';
import * as finder from './find.js';
globalThis.DOM = getDOM(); const {join} = require('path');
globalThis.CloudCmd = getCloudCmd();
const {assign} = Object; const test = require('supertape');
const {DOM} = globalThis; const stub = require('@cloudcmd/stub');
const mockRequire = require('mock-require');
const {reRequire, stopAll} = mockRequire;
const dir = '../';
const pathVim = join(dir, 'vim');
const pathFind = join(dir, 'vim', 'find');
const {
getDOM,
getCloudCmd,
} = require('./globals.fixture');
global.DOM = getDOM();
global.CloudCmd = getCloudCmd();
const {DOM} = global;
const {Buffer} = DOM; const {Buffer} = DOM;
const vim = require(pathVim);
test('cloudcmd: client: key: set next file: no', (t) => { test('cloudcmd: client: key: set next file: no', (t) => {
const element = {}; const element = {
const setCurrentFile = stub();
const unselectFiles = stub();
const Info = {
element,
}; };
vim('j', {}, { const setCurrentFile = stub();
Info,
setCurrentFile,
unselectFiles,
});
t.calledWith(setCurrentFile, [element], 'should set next file'); global.DOM.CurrentInfo.element = element;
global.DOM.setCurrentFile = setCurrentFile;
vim('j', {});
t.ok(setCurrentFile.calledWith(element), 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set next file current: j', async (t) => { test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling,
@ -37,21 +48,17 @@ test('cloudcmd: client: key: set next file current: j', async (t) => {
const setCurrentFile = stub(); const setCurrentFile = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
await vim('j', {}, { vim('j', {});
Info,
setCurrentFile, t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
unselectFiles: stub(),
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set next file current: mjj', (t) => { test('cloudcmd: client: key: set next file current', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling,
@ -59,21 +66,15 @@ test('cloudcmd: client: key: set next file current: mjj', (t) => {
const setCurrentFile = stub(); const setCurrentFile = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('m', {});
Info, vim('j', {});
setCurrentFile, vim('j', {});
unselectFiles: stub(),
};
vim('m', {}, deps); t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
vim('j', {}, deps);
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should set next file');
t.end(); t.end();
}); });
@ -85,44 +86,38 @@ test('cloudcmd: client: key: set next file current: g', (t) => {
const setCurrentFile = stub(); const setCurrentFile = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('g', {});
Info, vim('j', {});
setCurrentFile,
unselectFiles: stub(),
};
vim('g', {}, deps); t.ok(setCurrentFile.calledWith(nextSibling), 'should ignore g');
vim('j', {}, deps);
t.calledWith(setCurrentFile, [nextSibling], 'should ignore g');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set +2 file current', (t) => { test('cloudcmd: client: key: set +2 file current', (t) => {
const last = {}; const last = {};
const nextSibling = {
nextSibling: last,
};
const element = {
nextSibling,
};
const setCurrentFile = stub(); const setCurrentFile = stub();
const element = {};
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = {
setCurrentFile,
Info,
unselectFiles: stub(),
};
const event = {}; const event = {};
vim('2', event, deps); vim('2', event);
vim('j', event, deps); vim('j', event);
t.ok(setCurrentFile.calledWith(last), 'should set next file');
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end(); t.end();
}); });
@ -131,39 +126,25 @@ test('cloudcmd: client: key: select +2 files from current before delete', (t) =>
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last,
}; };
const element = { const element = {
nextSibling, nextSibling,
}; };
const setCurrentFile = stub(); const setCurrentFile = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
}; global.DOM.selectFile = stub();
global.DOM.getCurrentName = () => false;
const Operation = { global.CloudCmd.Operation.show = stub();
show: stub(),
};
const selectFile = stub();
const getCurrentName = stub().returns('x');
const event = {}; const event = {};
const deps = { vim('d', event);
Info, vim('2', event);
setCurrentFile, vim('j', event);
selectFile,
getCurrentName,
Operation,
};
vim('d', event, deps); t.ok(setCurrentFile.calledWith(last), 'should set next file');
vim('2', event, deps);
vim('j', event, deps);
t.calledWith(setCurrentFile, [last], 'should set next file');
t.end(); t.end();
}); });
@ -172,7 +153,6 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => {
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last,
}; };
const element = { const element = {
nextSibling, nextSibling,
}; };
@ -180,26 +160,20 @@ test('cloudcmd: client: key: delete +2 files from current', (t) => {
const setCurrentFile = stub(); const setCurrentFile = stub();
const show = stub(); const show = stub();
const deps = { global.DOM.CurrentInfo.element = element;
Info: { global.DOM.setCurrentFile = setCurrentFile;
element, global.DOM.selectFile = stub();
}, global.DOM.getCurrentName = () => false;
Operation: { global.CloudCmd.Operation.show = show;
show,
},
setCurrentFile,
selectFile: stub(),
getCurrentName: stub().returns('x'),
unselectFiles: stub(),
};
const event = {}; const event = {};
vim('d', event, deps); vim('d', event);
vim('2', event, deps); vim('2', event);
vim('j', event, deps); vim('j', event);
t.ok(show.calledWith('delete'), 'should call delete');
t.calledWith(show, ['delete'], 'should call delete');
t.end(); t.end();
}); });
@ -210,84 +184,49 @@ test('cloudcmd: client: key: set previous file current', (t) => {
}; };
const setCurrentFile = stub(); const setCurrentFile = stub();
const unselectFiles = stub();
const Info = { global.DOM.CurrentInfo.element = element;
element, global.DOM.setCurrentFile = setCurrentFile;
};
const deps = { vim('k', {});
Info,
setCurrentFile,
unselectFiles,
};
vim('k', {}, deps); t.ok(setCurrentFile.calledWith(previousSibling), 'should set previous file');
t.calledWith(setCurrentFile, [previousSibling], 'should set previous file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy: no', (t) => { test('cloudcmd: client: key: copy: no', (t) => {
const copy = stub(); const copy = stub();
vim('y', {}, { Buffer.copy = copy;
unselectFiles: stub(),
Buffer: { vim('y', {});
copy,
}, t.notOk(copy.called, 'should not copy files');
});
t.notCalled(copy, 'should not copy files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy', (t) => { test('cloudcmd: client: key: copy', (t) => {
const copy = stub(); const copy = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub(); Buffer.copy = copy;
const unselectFiles = stub();
const deps = { vim('v', {});
Info, vim('y', {});
unselectFiles,
toggleSelectedFile,
Buffer: {
copy,
},
};
vim('v', {}, deps); t.ok(copy.calledWith(), 'should copy files');
vim('y', {}, deps);
t.calledWithNoArgs(copy, 'should copy files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: copy: unselectFiles', (t) => { test('cloudcmd: client: key: copy: unselectFiles', (t) => {
const unselectFiles = stub(); const unselectFiles = stub();
const Info = {
element: {},
};
const toggleSelectedFile = stub(); DOM.unselectFiles = unselectFiles;
const deps = { vim('v', {});
Info, vim('y', {});
unselectFiles,
toggleSelectedFile,
Buffer: {
copy: stub(),
},
};
vim('v', {}, deps); t.ok(unselectFiles.calledWith(), 'should unselect files');
vim('y', {}, deps);
t.calledWithNoArgs(unselectFiles, 'should unselect files');
t.end(); t.end();
}); });
@ -296,91 +235,63 @@ test('cloudcmd: client: key: paste', (t) => {
Buffer.paste = paste; Buffer.paste = paste;
vim('p', {}, { vim('p', {});
Buffer,
}); t.ok(paste.calledWith(), 'should paste files');
t.calledWithNoArgs(paste, 'should paste files');
t.end(); t.end();
}); });
test('cloudcmd: client: key: selectFile: ..', (t) => { test('cloudcmd: client: key: selectFile: ..', (t) => {
const getCurrentName = stub().returns('..');
const selectFile = stub(); const selectFile = stub();
const getCurrentName = stub();
DOM.selectFile = selectFile;
DOM.getCurrentName = () => '..';
const current = {}; const current = {};
vim.selectFile(current);
vimSelectFile(current, { t.notOk(getCurrentName.called, 'should not call selectFile');
selectFile,
getCurrentName,
});
t.notCalled(selectFile, 'should not call selectFile');
t.end(); t.end();
}); });
test('cloudcmd: client: key: selectFile', (t) => { test('cloudcmd: client: key: selectFile', (t) => {
const selectFile = stub(); const selectFile = stub();
const getCurrentName = stub().returns('x');
DOM.selectFile = selectFile;
DOM.getCurrentName = (a) => a.name;
const current = {}; const current = {};
vimSelectFile(current, { vim.selectFile(current);
selectFile,
getCurrentName,
});
t.calledWith(selectFile, [current], 'should call selectFile'); t.ok(selectFile.calledWith(current), 'should call selectFile');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set last file current: shift + g', async (t) => { test('cloudcmd: client: key: set last file current', (t) => {
const last = 'last'; const last = 'last';
const nextSibling = { const nextSibling = {
nextSibling: last, nextSibling: last,
}; };
const element = { const element = {
nextSibling, nextSibling,
}; };
const setCurrentFile = stub(); const setCurrentFile = stub();
await vim('G', {}, { global.DOM.CurrentInfo.element = element;
Info: { global.DOM.setCurrentFile = setCurrentFile;
element,
}, vim('G', {});
setCurrentFile,
unselectFiles: stub(), t.ok(setCurrentFile.calledWith(last), 'should set last file');
});
t.calledWith(setCurrentFile, [last], 'should set last file');
t.end(); t.end();
}); });
test('cloudcmd: client: key: set last file current: $', (t) => { test('cloudcmd: client: key: set first 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 first = 'first';
const previousSibling = { const previousSibling = {
previousSibling: first, previousSibling: first,
@ -390,121 +301,66 @@ test('cloudcmd: client: key: set first file current: gg', (t) => {
previousSibling, previousSibling,
}; };
const Operation = {
show: stub(),
};
const unselectFiles = stub();
const setCurrentFile = stub(); const setCurrentFile = stub();
const deps = { global.DOM.CurrentInfo.element = element;
Operation, global.DOM.setCurrentFile = setCurrentFile;
unselectFiles,
setCurrentFile,
Info: {
element,
},
};
vim('g', {}, deps); vim('g', {});
vim('g', {}, deps); vim('g', {});
t.calledWith(setCurrentFile, [first], 'should set first file'); t.ok(setCurrentFile.calledWith(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(); t.end();
}); });
test('cloudcmd: client: key: visual', (t) => { test('cloudcmd: client: key: visual', (t) => {
const element = {}; const element = {
const toggleSelectedFile = stub();
const Info = {
element,
}; };
vim('v', {}, { const toggleSelectedFile = stub();
Info,
toggleSelectedFile, global.DOM.CurrentInfo.element = element;
}); global.DOM.toggleSelectedFile = toggleSelectedFile;
vim('v', {});
t.ok(toggleSelectedFile.calledWith(element), 'should toggle selection');
t.calledWith(toggleSelectedFile, [element], 'should toggle selection');
t.end(); t.end();
}); });
test('cloudcmd: client: key: ESC', (t) => { test('cloudcmd: client: key: ESC', (t) => {
const element = {}; const element = {
const unselectFiles = stub();
const Info = {
element,
}; };
vim('Escape', null, { const unselectFiles = stub();
Info,
unselectFiles,
});
t.calledWithNoArgs(unselectFiles, 'should toggle selection'); global.DOM.CurrentInfo.element = element;
global.DOM.unselectFiles = unselectFiles ;
vim('Escape');
t.ok(unselectFiles.calledWith(), 'should toggle selection');
t.end(); t.end();
}); });
test('cloudcmd: client: key: Enter', async (t) => { test('cloudcmd: client: key: Enter', (t) => {
const nextSibling = 'hello'; const nextSibling = 'hello';
const element = { const element = {
nextSibling, nextSibling,
}; };
const unselectFiles = stub();
const setCurrentFile = stub(); const setCurrentFile = stub();
const Info = { DOM.CurrentInfo.element = element;
element, DOM.setCurrentFile = setCurrentFile;
};
await vim('Enter', null, { vim('Enter');
Info,
setCurrentFile,
unselectFiles,
});
await vim('j', null, { vim('j');
Info,
setCurrentFile,
unselectFiles,
});
t.calledWith(setCurrentFile, [nextSibling], 'should set next file'); t.ok(setCurrentFile.calledWith(nextSibling), 'should set next file');
t.end(); t.end();
}); });
@ -512,245 +368,50 @@ test('cloudcmd: client: key: /', (t) => {
const preventDefault = stub(); const preventDefault = stub();
const element = {}; const element = {};
const Info = { DOM.CurrentInfo.element = element;
element, DOM.getCurrentName = () => '';
files: [],
};
const getCurrentName = stub().returns(''); vim('/', {
const event = {
preventDefault, preventDefault,
};
const prompt = stub().returns([]);
vim('/', event, {
getCurrentName,
Info,
prompt,
}); });
t.calledWithNoArgs(preventDefault); t.ok(preventDefault.calledWith(), 'should call 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(); t.end();
}); });
test('cloudcmd: client: key: n', (t) => { test('cloudcmd: client: key: n', (t) => {
const findNext = stub(); const findNext = stub();
const createFindNext = stub().returns(findNext);
const event = {}; mockRequire(pathFind, {
findNext,
vim('n', event, {
createFindNext,
}); });
t.calledWithNoArgs(findNext, 'should call findNext'); const vim = reRequire(pathVim);
const event = {};
vim('n', event);
stopAll(pathFind);
t.ok(findNext.calledWith(), 'should call findNext');
t.end(); t.end();
}); });
test('cloudcmd: client: key: N', (t) => { test('cloudcmd: client: key: N', (t) => {
const findPrevious = stub(); const findPrevious = stub();
const createFindPrevious = stub().returns(findPrevious);
mockRequire(pathFind, {
findPrevious,
});
const vim = reRequire(dir + 'vim');
const event = {}; const event = {};
vim('N', event, { vim('N', event);
createFindPrevious,
});
t.calledWithNoArgs(findPrevious); stopAll(pathFind);
t.ok(findPrevious.calledWith(), 'should call findPrevious');
t.end(); 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();
});

View file

@ -1,33 +0,0 @@
/* 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');
};

View file

@ -1,5 +1,6 @@
import {fullstore} from 'fullstore'; 'use strict';
const fullstore = require('fullstore');
const store = fullstore(''); const store = fullstore('');
const visual = fullstore(false); const visual = fullstore(false);
@ -20,11 +21,10 @@ const rmFirst = (a) => {
const noop = () => {}; const noop = () => {};
export default (key, operations = {}) => { module.exports = (key, operations) => {
const prevStore = store(); const prevStore = store();
const isVisual = visual(); const isVisual = visual();
const value = store(prevStore.concat(key)); const value = store(prevStore.concat(key));
const { const {
escape = noop, escape = noop,
moveNext = noop, moveNext = noop,
@ -36,13 +36,6 @@ export default (key, operations = {}) => {
find = noop, find = noop,
findNext = noop, findNext = noop,
findPrevious = noop, findPrevious = noop,
makeFile = noop,
makeDirectory = noop,
terminal = noop,
edit = noop,
operationCopy = noop,
operationMove = noop,
rename = noop,
} = operations; } = operations;
if (key === 'Enter') if (key === 'Enter')
@ -51,11 +44,10 @@ export default (key, operations = {}) => {
if (key === 'Escape') { if (key === 'Escape') {
visual(false); visual(false);
escape(); escape();
return end(); return end();
} }
if (key === 'j' || key === 'w') { if (key === 'j') {
const { const {
count, count,
isDelete, isDelete,
@ -71,7 +63,7 @@ export default (key, operations = {}) => {
return end(); return end();
} }
if (key === 'k' || key === 'b') { if (key === 'k') {
const { const {
count, count,
isDelete, isDelete,
@ -87,8 +79,11 @@ export default (key, operations = {}) => {
return end(); return end();
} }
if (value === 'gg' || key === '^') { if (/^gg$/.test(value)) {
const {isDelete, isVisual} = handleDelete(prevStore); const {
isDelete,
isVisual,
} = handleDelete(prevStore);
movePrevious({ movePrevious({
count: Infinity, count: Infinity,
@ -99,49 +94,13 @@ export default (key, operations = {}) => {
return end(); 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')) { if (key === 'd' && (visual() || prevStore === 'd')) {
stopVisual(); stopVisual();
remove(); remove();
return end(); return end();
} }
if (key === 'G' || key === '$') { if (key === 'G') {
moveNext({ moveNext({
count: Infinity, count: Infinity,
isVisual, isVisual,
@ -156,7 +115,6 @@ export default (key, operations = {}) => {
stopVisual(); stopVisual();
copy(); copy();
return end(); return end();
} }
@ -168,7 +126,6 @@ export default (key, operations = {}) => {
if (/^v$/i.test(key)) { if (/^v$/i.test(key)) {
visual(!visual()); visual(!visual());
select(); select();
return end(); return end();
} }
@ -186,9 +143,6 @@ export default (key, operations = {}) => {
findPrevious(); findPrevious();
return end(); return end();
} }
if (key === ' ')
return end();
}; };
function handleDelete(prevStore) { function handleDelete(prevStore) {
@ -218,3 +172,4 @@ function getNumber(value) {
return parseInt(value); return parseInt(value);
} }

View file

@ -1,5 +1,7 @@
import {test, stub} from 'supertape'; 'use strict';
import vim from './vim.js';
const test = require('supertape');
const vim = require('./vim');
test('vim: no operations', (t) => { test('vim: no operations', (t) => {
const result = vim('hello', {}); const result = vim('hello', {});
@ -8,93 +10,3 @@ test('vim: no operations', (t) => {
t.end(); 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();
});

View file

@ -1,10 +1,11 @@
import currify from 'currify'; 'use strict';
export const getIndex = currify((array, item) => { module.exports = (array, item) => {
const index = array.indexOf(item); const index = array.indexOf(item);
if (!~index) if (!~index)
return 0; return 0;
return index; return index;
}); };

View file

@ -1,4 +1,6 @@
export const getRange = (indexFrom, indexTo, files) => { 'use strict';
module.exports = (indexFrom, indexTo, files) => {
if (indexFrom < indexTo) if (indexFrom < indexTo)
return files.slice(indexFrom, indexTo + 1); return files.slice(indexFrom, indexTo + 1);
@ -7,3 +9,4 @@ export const getRange = (indexFrom, indexTo, files) => {
return [files[indexFrom]]; return [files[indexFrom]];
}; };

View file

@ -1,52 +0,0 @@
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();
});

View file

@ -1,30 +1,36 @@
/* global DOM, CloudCmd */ /* global DOM, CloudCmd */
import exec from 'execon';
import itype from 'itype'; 'use strict';
import currify from 'currify';
import {tryToCatch} from 'try-to-catch'; const exec = require('execon');
import clipboard from '@cloudcmd/clipboard'; const itype = require('itype');
import * as Events from '#dom/events'; const currify = require('currify');
import {uploadFiles} from '#dom/upload-files'; const tryToCatch = require('try-to-catch');
import {FS} from '#common/cloudfunc'; const clipboard = require('@cloudcmd/clipboard');
import {getRange} from './get-range.js';
import {getIndex} from './get-index.js'; const getRange = require('./get-range');
const getIndex = currify(require('./get-index'));
const uploadFiles = require('../dom/upload-files');
const {FS} = require('../../common/cloudfunc');
const NBSP_REG = RegExp(String.fromCharCode(160), 'g'); const NBSP_REG = RegExp(String.fromCharCode(160), 'g');
const SPACE = ' '; const SPACE = ' ';
export async function init() { module.exports.init = () => {
contextMenu(); contextMenu();
dragndrop(); dragndrop();
unload(); unload();
pop(); pop();
resize(); resize();
config();
header(); header();
await config(); };
}
CloudCmd.Listeners = module.exports;
const unselect = (event) => { const unselect = (event) => {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
const { const {
shiftKey, shiftKey,
metaKey, metaKey,
@ -42,9 +48,14 @@ const execAll = currify((funcs, event) => {
fn(event); fn(event);
}); });
const Info = DOM.CurrentInfo;
const {Events} = DOM;
const EventsFiles = { const EventsFiles = {
mousedown: exec.with(execIfNotUL, setCurrentFileByEvent), mousedown: exec.with(execIfNotUL, setCurrentFileByEvent),
click: execAll([onClick, exec.with(execIfNotMobile, unselect)]), click: execAll([
onClick,
unselect,
]),
dragstart: exec.with(execIfNotUL, onDragStart), dragstart: exec.with(execIfNotUL, onDragStart),
dblclick: exec.with(execIfNotUL, onDblClick), dblclick: exec.with(execIfNotUL, onDblClick),
touchstart: exec.with(execIfNotUL, onTouch), touchstart: exec.with(execIfNotUL, onTouch),
@ -55,7 +66,6 @@ let EXT;
function header() { function header() {
const fm = DOM.getFM(); const fm = DOM.getFM();
const isDataset = (el) => el.dataset; const isDataset = (el) => el.dataset;
const isPanel = (el) => { const isPanel = (el) => {
return /^js-(left|right)$/.test(el.dataset.name); return /^js-(left|right)$/.test(el.dataset.name);
}; };
@ -67,7 +77,8 @@ function header() {
if (parent.dataset.name !== 'js-fm-header') if (parent.dataset.name !== 'js-fm-header')
return; return;
const name = (el.dataset.name || '').replace('js-', ''); const name = (el.dataset.name || '')
.replace('js-', '');
if (!/^(name|size|date)$/.test(name)) if (!/^(name|size|date)$/.test(name))
return; return;
@ -90,58 +101,58 @@ function getPath(el, path = []) {
async function config() { async function config() {
const [, config] = await tryToCatch(DOM.Files.get, 'config'); const [, config] = await tryToCatch(DOM.Files.get, 'config');
const type = config?.packer; const type = config && config.packer;
EXT = DOM.getPackerExt(type); EXT = DOM.getPackerExt(type);
} }
export const initKeysPanel = () => { module.exports.initKeysPanel = () => {
const keysElement = DOM.getById('js-keyspanel'); const keysElement = DOM.getById('js-keyspanel');
if (!keysElement) if (!keysElement)
return; return;
Events.addClick(keysElement, (event) => { Events.addClick(keysElement, ({target}) => {
const {target} = event;
const {id} = target; const {id} = target;
const operation = (name) => { const operation = (name) => {
const {Operation} = CloudCmd; const {Operation} = CloudCmd;
const fn = Operation.show.bind(null, name);
return Operation.show.bind(null, name); return fn;
}; };
const clickFuncs = { const clickFuncs = {
'f1': CloudCmd.Help.show, 'f1' : CloudCmd.Help.show,
'f2': CloudCmd.UserMenu.show, 'f2' : initF2,
'f3': CloudCmd.View.show, 'f3' : CloudCmd.View.show,
'f4': CloudCmd.EditFile.show, 'f4' : CloudCmd.EditFile.show,
'f5': operation('copy'), 'f5' : operation('copy'),
'f6': operation('move'), 'f6' : operation('move'),
'f7': DOM.promptNewDir, 'f7' : DOM.promptNewDir,
'f8': operation('delete'), 'f8' : operation('delete'),
'f9': () => { 'f9' : CloudCmd.Menu.show,
event.stopPropagation(); 'f10' : CloudCmd.Config.show,
CloudCmd.Menu.show(); '~' : CloudCmd.Konsole.show,
}, 'shift~' : CloudCmd.Terminal.show,
'f10': CloudCmd.Config.show, 'contact' : CloudCmd.Contact.show,
'~': CloudCmd.Konsole.show,
'shift~': CloudCmd.Terminal.show,
'contact': CloudCmd.Contact.show,
}; };
exec(clickFuncs[id]); exec(clickFuncs[id]);
}); });
}; };
function initF2() {
CloudCmd.UserMenu.show();
}
const getPanel = (side) => { const getPanel = (side) => {
if (!itype.string(side)) if (!itype.string(side))
return side; return side;
return DOM.getByDataName(`js-${side}`); return DOM.getByDataName('js-' + side);
}; };
export const setOnPanel = (side) => { module.exports.setOnPanel = (side) => {
const panel = getPanel(side); const panel = getPanel(side);
const filesElement = DOM.getByDataName('js-files', panel); const filesElement = DOM.getByDataName('js-files', panel);
@ -157,7 +168,6 @@ function getPathListener(panel) {
} }
function isNoCurrent(panel) { function isNoCurrent(panel) {
const Info = DOM.CurrentInfo;
const infoPanel = Info.panel; const infoPanel = Info.panel;
if (!infoPanel) if (!infoPanel)
@ -176,13 +186,13 @@ function decodePath(path) {
return decodeURI(path) return decodeURI(path)
.replace(url, '') .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('%%', '%25%')
.replace(NBSP_REG, SPACE) || '/'; .replace(NBSP_REG, SPACE) || '/';
} }
async function onPathElementClick(panel, event) { async function onPathElementClick(panel, event) {
const Info = DOM.CurrentInfo;
event.preventDefault(); event.preventDefault();
const element = event.target; const element = event.target;
@ -204,26 +214,19 @@ async function onPathElementClick(panel, event) {
const {href} = element; const {href} = element;
const path = decodePath(href); const path = decodePath(href);
await CloudCmd.changeDir(path, { await CloudCmd.loadDir({
path,
isRefresh: false, isRefresh: false,
panel: noCurrent ? panel : Info.panel, panel: noCurrent ? panel : Info.panel,
}); });
} }
function copyPath(el) { function copyPath(el) {
clipboard clipboard.writeText(el.parentElement.title)
.writeText(el.parentElement.title)
.then(CloudCmd.log) .then(CloudCmd.log)
.catch(CloudCmd.log); .catch(CloudCmd.log);
} }
function execIfNotMobile(callback, event) {
const isMobile = DOM.getCSSVar('is-mobile');
if (!isMobile)
callback(event);
}
function execIfNotUL(callback, event) { function execIfNotUL(callback, event) {
const {target} = event; const {target} = event;
const {tagName} = target; const {tagName} = target;
@ -238,14 +241,14 @@ function onClick(event) {
} }
function toggleSelect(key, files) { function toggleSelect(key, files) {
const isMac = /Mac/.test(globalThis.navigator.platform); const isMac = /Mac/.test(window.navigator.platform);
if (!key) if (!key)
throw Error('key should not be undefined!'); throw Error('key should not be undefined!');
const [file] = files; const [file] = files;
if (isMac && key.meta) if (isMac && key.meta || key.ctrl)
return DOM.toggleSelectedFile(file); return DOM.toggleSelectedFile(file);
if (key.shift) if (key.shift)
@ -253,7 +256,6 @@ function toggleSelect(key, files) {
} }
function changePanel(element) { function changePanel(element) {
const Info = DOM.CurrentInfo;
const {panel} = Info; const {panel} = Info;
const files = DOM.getByDataName('js-files', panel); const files = DOM.getByDataName('js-files', panel);
const ul = getULElement(element); const ul = getULElement(element);
@ -263,16 +265,21 @@ function changePanel(element) {
} }
async function onDblClick(event) { async function onDblClick(event) {
event.preventDefault();
const current = getLIElement(event.target); const current = getLIElement(event.target);
const isDir = DOM.isCurrentIsDir(current); const isDir = DOM.isCurrentIsDir(current);
const path = DOM.getCurrentPath(current); const path = DOM.getCurrentPath(current);
if (!isDir) if (isDir) {
return CloudCmd.View.show(); await CloudCmd.loadDir({
path: path === '/' ? '/' : path + '/',
});
await CloudCmd.changeDir(path); event.preventDefault();
} else {
CloudCmd.View.show();
event.preventDefault();
}
} }
async function onTouch(event) { async function onTouch(event) {
@ -287,7 +294,9 @@ async function onTouch(event) {
if (!isCurrent) if (!isCurrent)
return; return;
await CloudCmd.changeDir(DOM.getCurrentPath(current)); await CloudCmd.loadDir({
path: DOM.getCurrentPath(current),
});
} }
/* /*
@ -295,7 +304,6 @@ async function onTouch(event) {
* in Chrome (HTML5) * in Chrome (HTML5)
*/ */
function onDragStart(event) { function onDragStart(event) {
const Info = DOM.CurrentInfo;
const {prefixURL} = CloudCmd; const {prefixURL} = CloudCmd;
const element = getLIElement(event.target); const element = getLIElement(event.target);
const {isDir} = Info; const {isDir} = Info;
@ -312,8 +320,8 @@ function onDragStart(event) {
event.dataTransfer.setData( event.dataTransfer.setData(
'DownloadURL', 'DownloadURL',
'application/octet-stream' + ':' + name + 'application/octet-stream' + ':' +
':' + name + ':' +
link, link,
); );
} }
@ -336,7 +344,6 @@ function getULElement(element) {
} }
function setCurrentFileByEvent(event) { function setCurrentFileByEvent(event) {
const Info = DOM.CurrentInfo;
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const key = { const key = {
@ -400,7 +407,10 @@ function dragndrop() {
}; };
const onDrop = (event) => { const onDrop = (event) => {
const {files, items} = event.dataTransfer; const {
files,
items,
} = event.dataTransfer;
const {length: filesCount} = files; const {length: filesCount} = files;
@ -410,10 +420,7 @@ function dragndrop() {
return uploadFiles(files); return uploadFiles(files);
const isFile = (item) => item.kind === 'file'; const isFile = (item) => item.kind === 'file';
const dirFiles = Array.from(items).filter(isFile);
const dirFiles = Array
.from(items)
.filter(isFile);
if (dirFiles.length) if (dirFiles.length)
return DOM.uploadDirectory(dirFiles); return DOM.uploadDirectory(dirFiles);
@ -425,7 +432,7 @@ function dragndrop() {
}; };
/** /**
* In macOS Chrome dropEffect = 'none' * In Mac OS Chrome dropEffect = 'none'
* so drop do not firing up when try * so drop do not firing up when try
* to upload file from download bar * to upload file from download bar
*/ */
@ -450,9 +457,9 @@ function dragndrop() {
} }
function unload() { function unload() {
Events.add(['unload', 'beforeunload'], (event) => { DOM.Events.add(['unload', 'beforeunload'], (event) => {
const {Key} = CloudCmd; const {Key} = CloudCmd;
const isBind = Key?.isBind(); const isBind = Key && Key.isBind();
if (isBind) if (isBind)
return; return;
@ -470,8 +477,8 @@ function pop() {
return CloudCmd.route(location.hash); return CloudCmd.route(location.hash);
const history = false; const history = false;
await CloudCmd.loadDir({
await CloudCmd.changeDir(path, { path,
history, history,
}); });
}); });
@ -479,8 +486,7 @@ function pop() {
function resize() { function resize() {
Events.add('resize', () => { Events.add('resize', () => {
const Info = DOM.CurrentInfo; const is = window.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
const is = globalThis.innerWidth < CloudCmd.MIN_ONE_PANEL_WIDTH;
if (!is) if (!is)
return; return;
@ -500,3 +506,4 @@ function resize() {
DOM.changePanel(); DOM.changePanel();
}); });
} }

View file

@ -1,16 +1,19 @@
/* global CloudCmd */ 'use strict';
import exec from 'execon';
import {tryToCatch} from 'try-to-catch';
import {js as loadJS} from 'load.js';
import pascalCase from 'just-pascal-case';
/* global CloudCmd */
const exec = require('execon');
const tryToCatch = require('try-to-catch');
const loadJS = require('load.js').js;
const pascalCase = require('just-pascal-case');
const noJS = (a) => a.replace(/.js$/, ''); const noJS = (a) => a.replace(/.js$/, '');
/** /**
* function load modules * function load modules
* @params = {name, path, func, dobefore, arg} * @params = {name, path, func, dobefore, arg}
*/ */
export const loadModule = (params) => { module.exports = function loadModule(params) {
if (!params) if (!params)
return; return;
@ -22,24 +25,24 @@ export const loadModule = (params) => {
if (CloudCmd[name]) if (CloudCmd[name])
return; return;
CloudCmd[name] = async () => { CloudCmd[name] = () => {
exec(doBefore); exec(doBefore);
const {prefix} = CloudCmd;
const pathFull = prefix + CloudCmd.DIRCLIENT_MODULES + path + '.js';
const {DIR_MODULES} = CloudCmd; return loadJS(pathFull).then(async () => {
const pathFull = `${DIR_MODULES}/${path}.js`; const newModule = async (f) => f && f();
const module = CloudCmd[name];
await loadJS(pathFull); Object.assign(newModule, module);
const newModule = async (f) => f && f();
const module = CloudCmd[name];
Object.assign(newModule, module); CloudCmd[name] = newModule;
CloudCmd[name] = newModule; CloudCmd.log('init', name);
CloudCmd.log('init', name); await module.init();
await module.init(); return newModule;
});
return newModule;
}; };
CloudCmd[name].show = async (...args) => { CloudCmd[name].show = async (...args) => {
@ -49,8 +52,9 @@ export const loadModule = (params) => {
const [e, a] = await tryToCatch(m); const [e, a] = await tryToCatch(m);
if (e) if (e)
return; return console.error(e);
return await a.show(...args); await a.show(...args);
}; };
}; };

View file

@ -1,32 +1,31 @@
/* global CloudCmd, filepicker */ /* global CloudCmd, filepicker */
import exec from 'execon';
import currify from 'currify'; 'use strict';
import load from 'load.js';
import {ajax} from '#dom/load'; const exec = require('execon');
import * as Files from '#dom/files'; const currify = require('currify');
import * as Images from '#dom/images'; const load = require('load.js');
const {log} = CloudCmd; const {log} = CloudCmd;
const {ajax} = require('../dom/load');
const Files = require('../dom/files');
const Images = require('../dom/images');
const upload = currify(_upload); const upload = currify(_upload);
const Name = 'Cloud'; const Name = 'Cloud';
CloudCmd[Name] = module.exports;
CloudCmd[Name] = { module.exports.init = async () => {
init,
uploadFile,
saveFile,
};
export async function init() {
const [modules] = await loadFiles(); const [modules] = await loadFiles();
const {key} = modules.data.FilePicker; const {key} = modules.data.FilePicker;
filepicker.setKey(key); filepicker.setKey(key);
Images.hide(); Images.hide();
} };
export function uploadFile(filename, data) { module.exports.uploadFile = (filename, data) => {
const mimetype = ''; const mimetype = '';
filepicker.store(data, { filepicker.store(data, {
@ -35,14 +34,17 @@ export function uploadFile(filename, data) {
}, (fpFile) => { }, (fpFile) => {
filepicker.exportFile(fpFile, log, log); filepicker.exportFile(fpFile, log, log);
}); });
} };
export function saveFile(callback) { module.exports.saveFile = (callback) => {
filepicker.pick(upload(callback)); filepicker.pick(upload(callback));
} };
function _upload(callback, file) { function _upload(callback, file) {
const {url, filename} = file; const {
url,
filename,
} = file;
const responseType = 'arraybuffer'; const responseType = 'arraybuffer';
const success = exec.with(callback, filename); const success = exec.with(callback, filename);
@ -54,7 +56,7 @@ function _upload(callback, file) {
}); });
} }
function loadFiles() { async function loadFiles() {
const js = '//api.filepicker.io/v2/filepicker.js'; const js = '//api.filepicker.io/v2/filepicker.js';
return Promise.all([ return Promise.all([
@ -62,3 +64,4 @@ function loadFiles() {
load.js(js), load.js(js),
]); ]);
} }

View file

@ -1,31 +0,0 @@
/* 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() {}

View file

@ -1,23 +1,28 @@
import '../../../css/config.css'; 'use strict';
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; /* global CloudCmd, DOM, io */
require('../../../css/config.css');
const rendy = require('rendy');
const currify = require('currify');
const wraptile = require('wraptile');
const squad = require('squad');
const {promisify} = require('es6-promisify');
const tryToCatch = require('try-to-catch');
const load = require('load.js');
const createElement = require('@cloudcmd/create-element');
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 {Dialog, setTitle} = DOM;
const Name = 'Config'; const Name = 'Config';
CloudCmd[Name] = module.exports;
const loadSocket = promisify(DOM.loadSocket); const loadSocket = promisify(DOM.loadSocket);
@ -41,25 +46,28 @@ let Template;
const loadCSS = load.css; const loadCSS = load.css;
export async function init() { module.exports.init = async () => {
if (!CloudCmd.config('configDialog')) if (!CloudCmd.config('configDialog'))
return; return;
showLoad(); showLoad();
const {DIR_DIST} = CloudCmd; const {prefix} = CloudCmd;
[Template] = await Promise.all([ [Template] = await Promise.all([
Files.get('config-tmpl'), Files.get('config-tmpl'),
loadSocket(), loadSocket(),
loadCSS(`${DIR_DIST}/config.css`), loadCSS(prefix + '/dist/config.css'),
CloudCmd.View(), CloudCmd.View(),
]); ]);
initSocket(); initSocket();
} };
const {config, Key} = CloudCmd; const {
config,
Key,
} = CloudCmd;
let Element; let Element;
@ -69,20 +77,24 @@ function getHost() {
origin, origin,
protocol, protocol,
} = location; } = location;
const href = origin || `${protocol}//${host}`;
return origin || `${protocol}//${host}`; return href;
} }
function initSocket() { function initSocket() {
const href = getHost(); const href = getHost();
const {prefixSocket, prefix} = CloudCmd; const {
prefixSocket,
prefix,
} = CloudCmd;
const ONE_MINUTE = 60 * 1000; const ONE_MINUTE = 60 * 1000;
const socket = globalThis.io.connect(href + prefixSocket + '/config', { const socket = io.connect(href + prefixSocket + '/config', {
reconnectionAttempts: Infinity, reconnectionAttempts: Infinity,
reconnectionDelay: ONE_MINUTE, reconnectionDelay: ONE_MINUTE,
path: `${prefix}/socket.io`, path: prefix + '/socket.io',
}); });
const save = (data) => { const save = (data) => {
@ -113,7 +125,9 @@ function authCheck(socket) {
Config.save = saveHttp; Config.save = saveHttp;
export async function show() { module.exports.show = show;
async function show() {
if (!CloudCmd.config('configDialog')) if (!CloudCmd.config('configDialog'))
return; return;
@ -128,27 +142,21 @@ async function fillTemplate() {
const { const {
editor, editor,
menu,
packer, packer,
columns, columns,
theme,
configAuth, configAuth,
configPort,
...obj ...obj
} = input.convert(config); } = input.convert(config);
obj[`${menu}-selected`] = 'selected'; obj[editor + '-selected'] = 'selected';
obj[`${editor}-selected`] = 'selected'; obj[packer + '-selected'] = 'selected';
obj[`${packer}-selected`] = 'selected'; obj[columns + '-selected'] = 'selected';
obj[`${columns}-selected`] = 'selected';
obj[`${theme}-selected`] = 'selected';
obj.configAuth = configAuth ? '' : 'hidden'; obj.configAuth = configAuth ? '' : 'hidden';
obj.configPort = configPort ? '' : 'hidden';
const innerHTML = rendy(Template, obj); const innerHTML = rendy(Template, obj);
Element = createElement('form', { Element = createElement('form', {
className: 'config', className : 'config',
innerHTML, innerHTML,
}); });
@ -165,20 +173,20 @@ async function fillTemplate() {
const getTarget = ({target}) => target; const getTarget = ({target}) => target;
const handleChange = squad(onChange, getTarget); const handleChange = squad(onChange, getTarget);
Array Array.from(inputs)
.from(inputs)
.map(addKey(onKey)) .map(addKey(onKey))
.map(addChange(handleChange)); .map(addChange(handleChange));
const autoSize = true; const autoSize = true;
CloudCmd.View.show(Element, { CloudCmd.View.show(Element, {
autoSize, autoSize,
afterShow, afterShow,
}); });
} }
export function hide() { module.exports.hide = hide;
function hide() {
CloudCmd.View.hide(); CloudCmd.View.hide();
} }
@ -220,7 +228,7 @@ function onAuthChange(checked) {
const elUsername = input.getElementByName('username', Element); const elUsername = input.getElementByName('username', Element);
const elPassword = input.getElementByName('password', Element); const elPassword = input.getElementByName('password', Element);
elUsername.disabled = !checked; elUsername.disabled =
elPassword.disabled = !checked; elPassword.disabled = !checked;
} }
@ -230,18 +238,13 @@ function onNameChange(name) {
})); }));
} }
async function onKey({keyCode, target}) { function onKey({keyCode, target}) {
switch(keyCode) { switch(keyCode) {
case Key.ESC: case Key.ESC:
return hide(); return hide();
case Key.ENTER: case Key.ENTER:
return await onChange(target); return onChange(target);
} }
} }
CloudCmd[Name] = {
init,
show,
hide,
};

View file

@ -1,17 +1,23 @@
import {encode} from '#common/entity'; 'use strict';
const isBool = (a) => typeof a === 'boolean'; const currify = require('currify');
const isString = (a) => typeof a === 'string';
const {keys} = Object; const isType = currify((type, object, name) => {
return typeof object[name] === type;
});
export function getElementByName(selector, element) { const isBool = isType('boolean');
module.exports.getElementByName = getElementByName;
function getElementByName(selector, element) {
const str = `[data-name="js-${selector}"]`; const str = `[data-name="js-${selector}"]`;
return element.querySelector(str); return element
.querySelector(str);
} }
export const getName = (element) => { module.exports.getName = (element) => {
const name = element const name = element
.getAttribute('data-name') .getAttribute('data-name')
.replace(/^js-/, ''); .replace(/^js-/, '');
@ -19,21 +25,17 @@ export const getName = (element) => {
return name; return name;
}; };
export const convert = (config) => { module.exports.convert = (config) => {
const result = config; const result = {
...config,
};
const array = Object.keys(config);
for (const name of keys(config)) { const filtered = array.filter(isBool(config));
for (const name of filtered) {
const item = config[name]; const item = config[name];
result[name] = setState(item);
if (isBool(item)) {
result[name] = setState(item);
continue;
}
if (isString(item)) {
result[name] = encode(item);
continue;
}
} }
return result; return result;
@ -46,7 +48,7 @@ function setState(state) {
return ''; return '';
} }
export const getValue = (name, element) => { module.exports.getValue = (name, element) => {
const el = getElementByName(name, element); const el = getElementByName(name, element);
const {type} = el; const {type} = el;
@ -62,7 +64,7 @@ export const getValue = (name, element) => {
} }
}; };
export const setValue = (name, value, element) => { module.exports.setValue = (name, value, element) => {
const el = getElementByName(name, element); const el = getElementByName(name, element);
const {type} = el; const {type} = el;
@ -76,3 +78,4 @@ export const setValue = (name, value, element) => {
break; break;
} }
}; };

View file

@ -1,140 +0,0 @@
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 <world>',
});
const expected = {
name: 'hello &lt;world&gt;',
};
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();
});

View file

@ -1,34 +1,36 @@
/* global CloudCmd */ /* global CloudCmd */
/* global DOM */ /* global DOM */
import olark from '@cloudcmd/olark';
import * as Images from '#dom/images';
CloudCmd.Contact = { 'use strict';
init,
show, CloudCmd.Contact = exports;
hide,
}; const olark = require('@cloudcmd/olark');
const Images = require('../dom/images');
const {Events} = DOM; const {Events} = DOM;
const {Key} = CloudCmd; const {Key} = CloudCmd;
export function init() { module.exports.show = show;
module.exports.hide = hide;
module.exports.init = () => {
Events.addKey(onKey); Events.addKey(onKey);
olark.identify('6216-545-10-4223'); olark.identify('6216-545-10-4223');
olark('api.box.onExpand', show); olark('api.box.onExpand', show);
olark('api.box.onShow', show); olark('api.box.onShow', show);
olark('api.box.onShrink', hide); olark('api.box.onShrink', hide);
} };
export function show() { function show() {
Key.unsetBind(); Key.unsetBind();
Images.hide(); Images.hide();
olark('api.box.expand'); olark('api.box.expand');
} }
export function hide() { function hide() {
Key.setBind(); Key.setBind();
olark('api.box.hide'); olark('api.box.hide');
} }
@ -37,3 +39,4 @@ function onKey({keyCode}) {
if (keyCode === Key.ESC) if (keyCode === Key.ESC)
hide(); hide();
} }

View file

@ -1,12 +1,10 @@
import * as Events from '#dom/events'; 'use strict';
const {CloudCmd} = globalThis; /* global CloudCmd */
CloudCmd.EditFileVim = { CloudCmd.EditFileVim = exports;
init,
show, const Events = require('../dom/events');
hide,
};
const {Key} = CloudCmd; const {Key} = CloudCmd;
@ -18,29 +16,34 @@ const ConfigView = {
}, },
}; };
export async function init() { module.exports.init = async () => {
await CloudCmd.EditFile(); await CloudCmd.EditFile();
} };
export async function show() { module.exports.show = () => {
Events.addKey(listener); Events.addKey(listener);
const editFile = await CloudCmd.EditFile.show(ConfigView); CloudCmd.EditFile
.show(ConfigView)
editFile
.getEditor() .getEditor()
.setKeyMap('vim'); .setKeyMap('vim');
} };
export function hide() { module.exports.hide = hide;
function hide() {
CloudCmd.Edit.hide(); CloudCmd.Edit.hide();
} }
function listener(event) { function listener(event) {
const {keyCode, shiftKey} = event; const {
keyCode,
shiftKey,
} = event;
if (shiftKey && keyCode === Key.ESC) { if (shiftKey && keyCode === Key.ESC) {
event.preventDefault(); event.preventDefault();
hide(); hide();
} }
} }

Some files were not shown because too many files have changed in this diff Show more