first commit
This commit is contained in:
46
administrator/components/com_ffexplorer/assets/explorer/dist/app.css
vendored
Normal file
46
administrator/components/com_ffexplorer/assets/explorer/dist/app.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
administrator/components/com_ffexplorer/assets/explorer/dist/app.js
vendored
Normal file
2
administrator/components/com_ffexplorer/assets/explorer/dist/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
administrator/components/com_ffexplorer/assets/explorer/dist/app.js.LICENSE.txt
vendored
Normal file
18
administrator/components/com_ffexplorer/assets/explorer/dist/app.js.LICENSE.txt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
/*!
|
||||
* Vue.js v2.6.14
|
||||
* (c) 2014-2021 Evan You
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* vuex v3.6.2
|
||||
* (c) 2021 Evan You
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**!
|
||||
* Sortable 1.10.2
|
||||
* @author RubaXa <trash@rubaxa.org>
|
||||
* @author owenm <owen23355@gmail.com>
|
||||
* @license MIT
|
||||
*/
|
||||
BIN
administrator/components/com_ffexplorer/assets/explorer/dist/fonts/element-icons.ttf
vendored
Normal file
BIN
administrator/components/com_ffexplorer/assets/explorer/dist/fonts/element-icons.ttf
vendored
Normal file
Binary file not shown.
BIN
administrator/components/com_ffexplorer/assets/explorer/dist/fonts/element-icons.woff
vendored
Normal file
BIN
administrator/components/com_ffexplorer/assets/explorer/dist/fonts/element-icons.woff
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
3569
administrator/components/com_ffexplorer/assets/explorer/package-lock.json
generated
Normal file
3569
administrator/components/com_ffexplorer/assets/explorer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "explorer",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dev": "webpack --env dev=true",
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"css-loader": "^6.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"node-sass": "^6.0.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"vue-loader": "^15.9.8",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.52.0",
|
||||
"webpack-cli": "^4.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-ui": "^2.15.6",
|
||||
"lodash": "^4.17.21",
|
||||
"vue": "^2.6.14",
|
||||
"vue-context": "^5.1.0",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="ffexplorer-app">
|
||||
<el-button type="primary" icon="el-icon-folder" @click="setApp('explorer')">File Manager</el-button>
|
||||
<el-button type="primary" icon="el-icon-coin" @click="setApp('database')">Database</el-button>
|
||||
<el-dialog
|
||||
width="99%"
|
||||
class="ffexplorer-dialog explorer-app-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:append-to-body="true"
|
||||
:visible.sync="explorerDialog"
|
||||
:before-close="handleClose"
|
||||
@opened="opened">
|
||||
<EApp v-show="app === 'explorer'"/>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
width="99%"
|
||||
class="ffexplorer-dialog database-app-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:append-to-body="true"
|
||||
:visible.sync="databaseDialog"
|
||||
:before-close="handleClose"
|
||||
@opened="opened">
|
||||
<DApp v-show="app === 'database'"/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EApp from './components/EApp.vue';
|
||||
import DApp from './components/DApp.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EApp,
|
||||
DApp,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
explorerDialog: false,
|
||||
databaseDialog: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
app() {
|
||||
return this.$store.state.app;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
opened() {
|
||||
jQuery(window).trigger('resize.ffexplorer');
|
||||
},
|
||||
|
||||
setApp(app) {
|
||||
if (app === 'explorer') {
|
||||
this.explorerDialog = true;
|
||||
}
|
||||
|
||||
if (app === 'database') {
|
||||
this.databaseDialog = true;
|
||||
}
|
||||
|
||||
this.$store.dispatch('setApp', app);
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.explorerDialog = false;
|
||||
this.databaseDialog = false;
|
||||
this.$store.dispatch('setApp', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.container-fluid.container-main {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.el-message-box__input input {
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
.ffexplorer-dialog.el-dialog__wrapper {
|
||||
overflow: hidden;
|
||||
|
||||
.el-dialog {
|
||||
margin-top: 0 !important;
|
||||
width: 99%;
|
||||
height: 98%;
|
||||
top: 1%;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 0px 20px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ffexplorer-dialog.database-app-dialog {
|
||||
.el-dialog__header {
|
||||
padding: 20px 20px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="d-app">
|
||||
<DSidebar />
|
||||
<DContent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DSidebar from './DSidebar.vue';
|
||||
import DContent from './DContent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DSidebar,
|
||||
DContent,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.d-app {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,603 @@
|
||||
<template>
|
||||
<div class="d-content"
|
||||
v-loading="loading">
|
||||
<div class="d-content-header">
|
||||
<div class="header-toolbar" v-if="table">
|
||||
<div class="header-toolbar-col" style="margin-right: 10px; flex-grow: 1;">
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
@click="openDialogInsert">Insert</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="openDialogDelete">Delete</el-button>
|
||||
</div>
|
||||
<div class="header-toolbar-col filter-bar">
|
||||
<div class="filter-col" style="margin-right: 5px; display: flex;">
|
||||
<el-select size="small" clearable v-model="filterCol">
|
||||
<el-option
|
||||
v-for="col in filterColumns"
|
||||
:key="col"
|
||||
:value="col" >{{col}}</el-option>
|
||||
</el-select>
|
||||
<el-select size="small" v-model="filterMethod">
|
||||
<el-option :key="'equal'" :value="'equal'" :label="'='"></el-option>
|
||||
<el-option :key="'like_both'" :value="'like_both'" :label="'like %...%'"></el-option>
|
||||
<el-option :key="'like_start'" :value="'like_start'" :label="'like ...%'"></el-option>
|
||||
<el-option :key="'like_end'" :value="'like_end'" :label="'like %...'"></el-option>
|
||||
</el-select>
|
||||
<el-input size="small" placeholder="value of column" v-model="filterValue" />
|
||||
</div>
|
||||
<div class="filter-col">
|
||||
<el-button size="small" type="primary" @click="doFilter">Filter</el-button>
|
||||
<el-button size="small" @click="clearFilter">Clear</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-content-inner" :style="{height, overflow: loading ? 'hidden' : 'auto'}">
|
||||
<table class="d-content-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="column.name">{{column.name}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, row) in items"
|
||||
:key="row"
|
||||
:class="{selected: activeRow === row}"
|
||||
@click="selectRow(row)">
|
||||
<td v-for="(value, column) in item"
|
||||
:key="column"
|
||||
:class="{selected: activeNode === (row + column + '')}"
|
||||
@click="selectNode(row + column + '', item, column)">{{value ? ('' + value).substring(0, 100) : ''}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top: 5px; text-align: right;">
|
||||
<el-pagination
|
||||
layout="jumper, prev, pager, next"
|
||||
:page-size="50"
|
||||
:hide-on-single-page="true"
|
||||
:current-page.sync="currentPage"
|
||||
:total="total"
|
||||
@current-change="changePage">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<el-dialog
|
||||
width="60%"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!saving"
|
||||
:show-close="!saving"
|
||||
:destroy-on-close="true"
|
||||
:custom-class="'dialog-edit'"
|
||||
:title="activeColumn + ' - Editor'"
|
||||
:visible.sync="dialogEdit">
|
||||
<textarea
|
||||
v-model="dialogValue"
|
||||
style="width: 100%; box-sizing: border-box;"
|
||||
rows="15"
|
||||
:disabled="saving"></textarea>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="saving"
|
||||
@click="dialogEdit = false">Close</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="saveNode">Save</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
width="60%"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!saving"
|
||||
:show-close="!saving"
|
||||
:destroy-on-close="true"
|
||||
:custom-class="'dialog-insert'"
|
||||
:title="table + ' - Insert record'"
|
||||
:visible.sync="dialogInsert">
|
||||
<form class="form-insert" v-loading="saving">
|
||||
<table>
|
||||
<tr v-for="column in cloneColumns" :key="column.name">
|
||||
<th>{{column.name}}</th>
|
||||
<td>{{column.type}}</td>
|
||||
<td>
|
||||
<span v-if="column.extra === 'auto_increment'">Auto Increment</span>
|
||||
<span v-else-if="column.default === 'CURRENT_TIMESTAMP'">Current Timestamp</span>
|
||||
<input
|
||||
type="text"
|
||||
v-else
|
||||
v-model="column.default"
|
||||
:name="column.name">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="saving"
|
||||
@click="dialogInsert = false">Close</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="insertRecord">Insert</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
width="50%"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!saving"
|
||||
:show-close="!saving"
|
||||
:destroy-on-close="true"
|
||||
:custom-class="'dialog-insert'"
|
||||
:title="'Table `' + table + '`: Do you want to delete this record?'"
|
||||
:visible.sync="dialogDelete">
|
||||
<table>
|
||||
<tr v-for="(value, key) in activeItem" :key="key">
|
||||
<th>{{key}}</th>
|
||||
<td>{{value ? ('' + value).substring(0, 100) : ''}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="saving"
|
||||
@click="dialogDelete = false">Close</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="deleteRecord">Delete</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
columns: [],
|
||||
cloneColumns: [],
|
||||
total: 0,
|
||||
items: [],
|
||||
height: '0px',
|
||||
currentPage: 1,
|
||||
activeNode: '',
|
||||
activeColumn: '',
|
||||
activeRow: -1,
|
||||
activeItem: {},
|
||||
dialogValue: '',
|
||||
dialogEdit: false,
|
||||
saving: false,
|
||||
dialogInsert: false,
|
||||
dialogDelete: false,
|
||||
filterCol: '',
|
||||
filterValue: '',
|
||||
filterMethod: 'equal',
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.setContentHeight();
|
||||
});
|
||||
|
||||
jQuery(window).on('resize.ffexplorer', () => {
|
||||
this.setContentHeight();
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
table() {
|
||||
return this.$store.state.activeTable;
|
||||
},
|
||||
|
||||
filterColumns() {
|
||||
return this.columns.map(col => col.name);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
table(name) {
|
||||
this.currentPage = 1;
|
||||
|
||||
this.resetFilter();
|
||||
this.resetActiveNode();
|
||||
this.initTable(name);
|
||||
this.resetScrollPosition();
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clearFilter() {
|
||||
this.currentPage = 1;
|
||||
this.resetActiveNode();
|
||||
this.resetFilter();
|
||||
this.resetScrollPosition();
|
||||
this.initTable();
|
||||
},
|
||||
|
||||
doFilter() {
|
||||
this.currentPage = 1;
|
||||
this.initTable();
|
||||
},
|
||||
|
||||
openDialogDelete() {
|
||||
if (this.activeRow === -1) {
|
||||
return alert('You need select a record to delete');
|
||||
}
|
||||
|
||||
this.dialogDelete = true;
|
||||
},
|
||||
|
||||
deleteRecord() {
|
||||
this.saving = true;
|
||||
|
||||
const cols = this.getConditionColumns();
|
||||
const condition = {};
|
||||
cols.forEach(col => {
|
||||
condition[col] = this.activeItem[col]
|
||||
});
|
||||
|
||||
this.$ajax({
|
||||
task: 'db.deleteRecord',
|
||||
table: this.table,
|
||||
condition: JSON.stringify(condition),
|
||||
})
|
||||
.then(res => {
|
||||
if (res.error) {
|
||||
return alert(res.error);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
this.resetActiveNode();
|
||||
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'delete succesfully'
|
||||
});
|
||||
return this.initTable(this.table, this.currentPage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('delete error');
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
this.dialogDelete = false;
|
||||
});
|
||||
},
|
||||
|
||||
openDialogInsert() {
|
||||
Vue.set(this, 'cloneColumns', JSON.parse(JSON.stringify(this.columns)));
|
||||
this.dialogInsert = true;
|
||||
},
|
||||
|
||||
insertRecord() {
|
||||
this.saving = true;
|
||||
|
||||
const data = jQuery('form.form-insert').serializeArray();
|
||||
|
||||
this.$ajax({
|
||||
task: 'db.insertRecord',
|
||||
table: this.table,
|
||||
data: JSON.stringify(data),
|
||||
})
|
||||
.then(res => {
|
||||
if (res.error) {
|
||||
return alert(res.error);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'insert succesfully'
|
||||
});
|
||||
return this.initTable(this.table, this.currentPage);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('insert error');
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
this.dialogInsert = false;
|
||||
});
|
||||
},
|
||||
|
||||
saveNode() {
|
||||
this.saving = true;
|
||||
|
||||
const cols = this.getConditionColumns();
|
||||
const condition = {};
|
||||
cols.forEach(col => {
|
||||
condition[col] = this.activeItem[col];
|
||||
});
|
||||
|
||||
this.$ajax({
|
||||
task: 'db.saveNode',
|
||||
table: this.table,
|
||||
condition: JSON.stringify(condition),
|
||||
column: this.activeColumn,
|
||||
value: this.dialogValue,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.error) {
|
||||
return alert(res.error);
|
||||
}
|
||||
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'Save successfully',
|
||||
});
|
||||
|
||||
return this.initTable(this.table, this.currentPage);
|
||||
})
|
||||
.catch(error => {
|
||||
alert('save error');
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
});
|
||||
},
|
||||
|
||||
getConditionColumns() {
|
||||
const priCols = this.columns.filter(col => {
|
||||
return col.key === 'PRI';
|
||||
})
|
||||
.map(col => {
|
||||
return col.name;
|
||||
});
|
||||
|
||||
if (priCols.length) {
|
||||
return priCols;
|
||||
} else {
|
||||
return this.columns.map(col => col.name);
|
||||
}
|
||||
},
|
||||
|
||||
resetActiveNode() {
|
||||
this.activeNode = '';
|
||||
this.activeColumn = '';
|
||||
this.activeRow = -1;
|
||||
Vue.set(this, 'activeItem', {});
|
||||
},
|
||||
|
||||
resetFilter() {
|
||||
this.filterCol = '';
|
||||
this.filterValue = '';
|
||||
this.filterMethod = 'equal';
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.resetActiveNode();
|
||||
this.initTable(this.table, page).then(() => {
|
||||
this.resetScrollPosition();
|
||||
});
|
||||
},
|
||||
|
||||
selectRow(row) {
|
||||
this.activeRow = row;
|
||||
},
|
||||
|
||||
selectNode(nodeId, item, column) {
|
||||
if (this.activeNode !== nodeId) {
|
||||
this.activeNode = nodeId;
|
||||
this.activeColumn = column;
|
||||
this.dialogValue = item[column];
|
||||
Vue.set(this, 'activeItem', item);
|
||||
} else {
|
||||
this.dialogEdit = true;
|
||||
}
|
||||
},
|
||||
|
||||
setContentHeight: debounce(function() {
|
||||
const $ = jQuery;
|
||||
const wHeight = $(window).height();
|
||||
if ($('.d-content-inner').length) {
|
||||
this.height = (wHeight - $('.d-content-inner').offset().top - 50) + 'px';
|
||||
}
|
||||
}, 100),
|
||||
|
||||
initTable(name, page) {
|
||||
return new Promise((resolve, reject) => {
|
||||
name = name ? name : this.table;
|
||||
page = page ? page : 1;
|
||||
|
||||
this.loading = true;
|
||||
const {filterCol, filterValue, filterMethod} = this;
|
||||
this.$ajax({
|
||||
task: 'db.initTable',
|
||||
name,
|
||||
page,
|
||||
filterCol,
|
||||
filterValue,
|
||||
filterMethod,
|
||||
})
|
||||
.then(res => {
|
||||
if (res.error) {
|
||||
return alert(res.error);
|
||||
}
|
||||
|
||||
if (res.data) {
|
||||
this.total = +res.data.total;
|
||||
Vue.set(this, 'columns', res.data.columns);
|
||||
Vue.set(this, 'items', res.data.items);
|
||||
this.setContentHeight();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('init table error');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
resetScrollPosition() {
|
||||
const $inner = this.$el.querySelector('.d-content-inner');
|
||||
$inner.scrollTop = 0;
|
||||
$inner.scrollLeft = 0;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.d-content {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-loading-mask {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.d-content-header {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
|
||||
.header-toolbar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.header-toolbar-col {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.filter-col {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
width: unset;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-content-inner {
|
||||
overflow: auto;
|
||||
border: solid 1px #ddd;
|
||||
}
|
||||
|
||||
.d-content-table {
|
||||
position: relative;
|
||||
|
||||
th {
|
||||
text-align: unset;
|
||||
max-width: 300px;
|
||||
padding: 3px 10px;
|
||||
word-break: normal;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #eee;
|
||||
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.4);
|
||||
border-right: solid 1px #ddd;
|
||||
border-bottom: solid 1px #ddd;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: background-color 300ms;
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
> td {
|
||||
max-width: 300px;
|
||||
padding: 3px 10px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
transition: background-color 300ms;
|
||||
border-right: solid 1px #ddd;
|
||||
border-bottom: solid 1px #ddd;
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(103, 186, 224, 0.73);
|
||||
outline: dashed 1px #3a8ee6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
.el-input__inner {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button + .el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-edit {
|
||||
.el-dialog__body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-insert {
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
> tr {
|
||||
border-bottom: dashed 1px #ddd;
|
||||
|
||||
th {
|
||||
text-align: unset;
|
||||
padding: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="d-sidebar">
|
||||
<div class="d-sidebar-header">
|
||||
<el-input
|
||||
class="d-sidebar-table-search"
|
||||
type="text"
|
||||
placeholder="search table..."
|
||||
size="small"
|
||||
v-model="keyword"
|
||||
@input="searchDb" />
|
||||
</div>
|
||||
<DSidbarItems />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DSidbarItems from './DSidebarItems.vue';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DSidbarItems,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
app: 'database',
|
||||
keyword: '',
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeApp() {
|
||||
this.$store.dispatch('setApp', this.app);
|
||||
},
|
||||
|
||||
searchDb: debounce(function() {
|
||||
this.$store.commit('searchDb', this.keyword);
|
||||
}),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.d-sidebar {
|
||||
width: 300px;
|
||||
|
||||
.d-sidebar-header {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 3px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="d-sidebar-items" :style="{ height }">
|
||||
<table class="d-item-list">
|
||||
<tr
|
||||
class="d-item"
|
||||
v-for="item in filtedList"
|
||||
:key="item.name"
|
||||
:class="{active: item.name === activeTable}"
|
||||
@click="selectTable(item.name)">
|
||||
<td class="item-label">{{item.name}}</td>
|
||||
<td class="item-size">
|
||||
<span>{{parseSize(item.size)}}</span>
|
||||
<div class="item-size-percent" :style="{width: getSizePercent(item.size) + '%' }"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
height: '0px',
|
||||
list: [],
|
||||
maxSize: 0,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.setListHeight();
|
||||
});
|
||||
|
||||
jQuery(window).on('resize.ffexplorer', () => {
|
||||
this.setListHeight();
|
||||
});
|
||||
|
||||
this.$ajax({
|
||||
task: 'db.tableList'
|
||||
})
|
||||
.then(res => {
|
||||
if (res.error) {
|
||||
return alert(res.error);
|
||||
}
|
||||
|
||||
if (res.data) {
|
||||
Vue.set(this, 'list', res.data);
|
||||
|
||||
const sizes = res.data.map(item => +item.size);
|
||||
|
||||
this.maxSize = sizes.reduce((a, b) => {
|
||||
return Math.max(a, b);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
filtedList() {
|
||||
const keyword = this.$store.state.db_keyword.toLowerCase();
|
||||
|
||||
return this.list.filter(item => {
|
||||
return item.name.toLowerCase().indexOf(keyword) > -1;
|
||||
});
|
||||
},
|
||||
|
||||
activeTable() {
|
||||
return this.$store.state.activeTable;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectTable(name) {
|
||||
this.$store.commit('setActiveTable', name);
|
||||
},
|
||||
|
||||
setListHeight: debounce(function() {
|
||||
const $ = jQuery;
|
||||
const wHeight = $(window).height();
|
||||
|
||||
if ($('.d-sidebar-items').length) {
|
||||
this.height = (wHeight - $('.d-sidebar-items').offset().top - 45) + 'px';
|
||||
}
|
||||
}, 100),
|
||||
|
||||
parseSize(size) {
|
||||
const kb = Math.round(size / 1024);
|
||||
|
||||
if (kb < 1024) {
|
||||
return kb + ' kB';
|
||||
}
|
||||
|
||||
const mb = Math.round(size / 1024 / 1024);
|
||||
if (mb < 1024) {
|
||||
return mb + ' mB';
|
||||
}
|
||||
|
||||
const gB = Math.round(size / 1024 / 1024);
|
||||
return gB + ' gB';
|
||||
},
|
||||
|
||||
getSizePercent(size) {
|
||||
const rouned = Math.round(size * 100 * 100 / this.maxSize);
|
||||
return rouned / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.d-sidebar-items {
|
||||
overflow: auto;
|
||||
border: solid 1px #ddd;
|
||||
padding: 5px;
|
||||
|
||||
.d-item-list {
|
||||
min-width: 100%;
|
||||
|
||||
> tr {
|
||||
border-bottom: dashed 1px #ddd;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 300ms;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(221, 221, 221, 0.32);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #ccc;
|
||||
|
||||
.item-size-percent {
|
||||
background-color: #03A9F4;
|
||||
}
|
||||
}
|
||||
|
||||
> td {
|
||||
padding: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-size {
|
||||
position: relative;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
border-left: dashed 1px #eaeaea;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.item-size-percent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: #d4d4d4;
|
||||
opacity: 0.5;
|
||||
border-radius: 3px;
|
||||
z-index: 0;
|
||||
transition: background-color 300ms;
|
||||
}
|
||||
}
|
||||
|
||||
.item-label {
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="e-app">
|
||||
<ESidebar />
|
||||
<EContent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ESidebar from './ESidebar.vue';
|
||||
import EContent from './EContent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ESidebar,
|
||||
EContent,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.e-app {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="e-content">
|
||||
<draggable
|
||||
class="e-content-tabs"
|
||||
v-model="files"
|
||||
v-bind="dragOptions"
|
||||
>
|
||||
<div
|
||||
class="e-content-tab-item"
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
:class="{active: file.path === current, deleted: file.status === 'deleted'}"
|
||||
:title="file.path"
|
||||
@click="open(file)"
|
||||
>
|
||||
<div class="file-status is-saving" v-if="file.status === 'saving'">•</div>
|
||||
<div class="file-status is-dirty" v-if="file.status === 'dirty'">•</div>
|
||||
<div class="tab-item-title">{{file.name}}</div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fas"
|
||||
data-icon="times"
|
||||
class="svg-inline--fa fa-times fa-w-11"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
license="https://fontawesome.com/license"
|
||||
viewBox="0 0 352 512"
|
||||
:style="{visibility: file.path === current ? 'visible' : 'hidden'}"
|
||||
@click.prevent.stop="close(file.path)"
|
||||
>
|
||||
<path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</draggable>
|
||||
<EContentEditor
|
||||
ref="editor"
|
||||
@removeFile="close"
|
||||
@statusChange="onStatusChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
import {EventBus} from '../event-bus';
|
||||
import EContentEditor from "./EContentEditor.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EContentEditor,
|
||||
draggable,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
dragOptions: {
|
||||
ghostClass: "ghost",
|
||||
},
|
||||
current: '',
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const $tabs = this.$el.querySelector('.e-content-tabs');
|
||||
|
||||
$tabs.addEventListener('wheel', e => {
|
||||
if (e.deltaY > 0) {
|
||||
$tabs.scrollLeft += 20;
|
||||
} else {
|
||||
$tabs.scrollLeft -= 20;
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on("openFileEditor", ({item, force}) => {
|
||||
const inList = this.files.find(file => file.path === item.path);
|
||||
const currentIdx = this.files.findIndex(file => file.path === this.current);
|
||||
|
||||
if (!inList) {
|
||||
const idx = currentIdx === -1 ? 0 : currentIdx + 1;
|
||||
|
||||
this.files.splice(idx, 0, {
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
status: 'opening',
|
||||
});
|
||||
}
|
||||
|
||||
this.current = item.path;
|
||||
this.open(item, force);
|
||||
|
||||
if (force) {
|
||||
setTimeout(() => {
|
||||
const $active = $tabs.querySelector('.active');
|
||||
if (!$active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($tabs.scrollLeft + $tabs.offsetWidth < $active.offsetLeft + $active.offsetWidth) {
|
||||
$tabs.scrollLeft = $active.offsetLeft + $active.offsetWidth - $tabs.offsetWidth;
|
||||
}
|
||||
|
||||
if ($tabs.scrollLeft > $active.offsetLeft + $active.offsetWidth) {
|
||||
$tabs.scrollLeft = $active.offsetLeft;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('fileNameChanged', (newFile, oldFile) => {
|
||||
const item = this.files.find(file => file.path === oldFile.path);
|
||||
|
||||
if (item) {
|
||||
item.path = newFile.path;
|
||||
item.name = newFile.name;
|
||||
|
||||
if (this.current === oldFile.path) {
|
||||
this.current = newFile.path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('fileDeleted', deletedFile => {
|
||||
const item = this.files.find(file => file.path === deletedFile.path);
|
||||
|
||||
if (item) {
|
||||
item.status = 'deleted';
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('folderNameChanged', (newFolder, oldFolder) => {
|
||||
this.files.forEach(file => {
|
||||
const idx = file.path.indexOf(oldFolder.path);
|
||||
|
||||
if (idx !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tail = file.path.slice(oldFolder.path.length - file.path.length);
|
||||
|
||||
if (this.current === file.path) {
|
||||
this.current = newFolder.path + tail;
|
||||
}
|
||||
|
||||
file.path = newFolder.path + tail;
|
||||
});
|
||||
});
|
||||
|
||||
EventBus.$on('folderDeleted', deletedFolder => {
|
||||
this.files.forEach(file => {
|
||||
const idx = file.path.indexOf(deletedFolder.path);
|
||||
|
||||
if (idx !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
file.status = 'deleted';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
open(file, force) {
|
||||
this.current = file.path;
|
||||
this.$refs.editor.initEditor(file, force);
|
||||
|
||||
this.$store.commit('setHistory', file.path);
|
||||
},
|
||||
|
||||
close(path) {
|
||||
const current = this.files.find(file => file.path === path);
|
||||
if (current.status === 'saving') {
|
||||
alert('Saving. Can not close for now.')
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.status === 'opening') {
|
||||
alert('Opening. Can not close for now.')
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.status === 'dirty') {
|
||||
const ok = confirm("Your content haven't saved yet. Do you want to close without saving?");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const idx = this.files.findIndex(file => file.path === path);
|
||||
|
||||
if (idx !== -1) {
|
||||
this.files.splice(idx, 1);
|
||||
this.$refs.editor.removeFile(path);
|
||||
this.$store.commit('deleteHistory', path);
|
||||
|
||||
const {history} = this.$store.state;
|
||||
if (this.current === path && history.length) {
|
||||
const prevFile = this.files.find(file => file.path === history[history.length - 1]);
|
||||
|
||||
if (prevFile) {
|
||||
this.open(prevFile);
|
||||
} else {
|
||||
this.current = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.files.length) {
|
||||
this.current = '';
|
||||
this.$refs.editor.resetEditor();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onStatusChange({status, path}) {
|
||||
const file = this.files.find(file => file.path === path);
|
||||
|
||||
if (file) {
|
||||
file.status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.e-content {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.e-content-tabs {
|
||||
position: relative;
|
||||
min-height: 43px;
|
||||
line-height: 15px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.e-content-tab-item {
|
||||
position: relative;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
padding-right: 17px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
.file-status {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 2px;
|
||||
font-size: 18px;
|
||||
|
||||
&.is-dirty {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
&.is-saving {
|
||||
color:#9e9e9e;
|
||||
}
|
||||
}
|
||||
|
||||
svg.fa-times {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 3px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
outline: solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom: solid 1px rgb(199, 199, 199);
|
||||
|
||||
svg {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #409eff !important;
|
||||
border-bottom: solid 1px #409eff;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="e-content-editor" :style="{height: editorHeight}"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {EventBus} from '../event-bus';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
let editor = false;
|
||||
let eData = {};
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
editorHeight: '0px',
|
||||
current: '',
|
||||
initedSnippet: false,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
eData = {};
|
||||
editor = false;
|
||||
|
||||
EventBus.$on('fileNameChanged', (newFile, oldFile) => {
|
||||
if (eData[oldFile.path]) {
|
||||
const data = eData[oldFile.path];
|
||||
|
||||
data.path = newFile.path;
|
||||
eData[newFile.path] = data;
|
||||
|
||||
delete eData[oldFile.path];
|
||||
|
||||
if (this.current === oldFile.path) {
|
||||
this.current = newFile.path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('fileDeleted', deletedFile => {
|
||||
const data = eData[deletedFile.path];
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.readOnly = true;
|
||||
|
||||
if (this.current === deletedFile.path) {
|
||||
editor.updateOptions({readOnly: true});
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('folderNameChanged', (newFolder, oldFolder) => {
|
||||
for (const key in eData) {
|
||||
const idx = key.indexOf(oldFolder.path);
|
||||
|
||||
if (idx !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tail = key.slice(oldFolder.path.length - key.length);
|
||||
const newKey = newFolder.path + tail;
|
||||
|
||||
eData[newKey] = eData[key];
|
||||
delete eData[key];
|
||||
|
||||
if (this.current === key) {
|
||||
this.current = newKey;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.$on('folderDeleted', deletedFolder => {
|
||||
for (const key in eData) {
|
||||
const idx = key.indexOf(deletedFolder.path);
|
||||
|
||||
if (idx !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
eData[key].readOnly = true;
|
||||
|
||||
if (this.current === key) {
|
||||
editor.updateOptions({readOnly: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(window).on('resize.ffexplorer', () => {
|
||||
this.computeEditorHeight();
|
||||
this.resizeEditorLayout();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: 'Refreshing',
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
customClass: 'compress-loading'
|
||||
});
|
||||
|
||||
this.loader().then(() => {
|
||||
window.require.config({
|
||||
paths: {
|
||||
'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.21.2/min/vs'
|
||||
}
|
||||
});
|
||||
|
||||
this.createEditor().then(() => {
|
||||
loading.close();
|
||||
this.resizeEditorLayout();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.computeEditorHeight();
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
loader() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const head = document.getElementsByTagName('head')[0]
|
||||
const script = document.createElement('script')
|
||||
|
||||
script.type = 'text/javascript'
|
||||
script.addEventListener('load', function () {
|
||||
resolve(script)
|
||||
})
|
||||
script.src = Joomla.getOptions('system.paths').root + '/administrator/components/com_ffexplorer/assets/monaco/loader.js';
|
||||
|
||||
head.appendChild(script)
|
||||
});
|
||||
},
|
||||
|
||||
async initEditor(file, force) {
|
||||
if (!editor) {
|
||||
await this.createEditor();
|
||||
}
|
||||
|
||||
const path = file.path;
|
||||
|
||||
if (this.current === path && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current && eData[this.current]) {
|
||||
eData[this.current].model = editor.getModel();
|
||||
eData[this.current].state = editor.saveViewState();
|
||||
}
|
||||
|
||||
this.current = path;
|
||||
|
||||
if (!eData[path] || force) {
|
||||
const language = this.parseLanguage(file.name);
|
||||
const sampleModel = monaco.editor.createModel('loading...', language);
|
||||
|
||||
const _data = {
|
||||
model: sampleModel,
|
||||
state: {},
|
||||
lastSaved: sampleModel.getAlternativeVersionId(),
|
||||
status: 'opening',
|
||||
readOnly: true,
|
||||
path: path,
|
||||
};
|
||||
|
||||
eData[path] = _data;
|
||||
|
||||
this.emitEditorStatus(_data);
|
||||
this.$store.commit('lock', path);
|
||||
|
||||
this.getFileContent(path).then(res => {
|
||||
_data.status = 'normal';
|
||||
_data.readOnly = false;
|
||||
_data.model = monaco.editor.createModel(res.content, language);
|
||||
_data.model.onDidChangeContent(() => {
|
||||
if (_data.status === 'saving') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checkDirty(_data);
|
||||
this.emitEditorStatus(_data);
|
||||
});
|
||||
|
||||
this.emitEditorStatus(_data);
|
||||
|
||||
if (this.current === _data.path) {
|
||||
editor.layout();
|
||||
this.updateEditor(_data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Could not open this file');
|
||||
|
||||
_data.status = 'normal';
|
||||
this.emitEditorStatus(_data);
|
||||
this.$emit('removeFile', _data.path);
|
||||
})
|
||||
.finally(() => {
|
||||
this.$store.commit('unlock', path);
|
||||
});
|
||||
}
|
||||
|
||||
this.updateEditor(eData[path]);
|
||||
},
|
||||
|
||||
checkDirty(data) {
|
||||
const isDirty = data.lastSaved !== data.model.getAlternativeVersionId();
|
||||
data.status = isDirty ? 'dirty' : 'normal';
|
||||
},
|
||||
|
||||
updateEditor({model, state, readOnly}) {
|
||||
editor.setModel(model);
|
||||
editor.restoreViewState(state);
|
||||
editor.updateOptions({
|
||||
readOnly: readOnly
|
||||
});
|
||||
|
||||
editor.layout();
|
||||
editor.focus();
|
||||
},
|
||||
|
||||
parseLanguage(name) {
|
||||
const langs = {
|
||||
js: 'javascript',
|
||||
php: 'php',
|
||||
scss: 'scss',
|
||||
css: 'css',
|
||||
less: 'less',
|
||||
sql: 'mysql',
|
||||
ini: 'ini',
|
||||
xml: 'xml',
|
||||
html: 'html',
|
||||
svg: 'html',
|
||||
json: 'json',
|
||||
md: 'markdown',
|
||||
gitignore: 'markdown',
|
||||
};
|
||||
|
||||
if (name.indexOf('.') === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const ext = name.split('.').pop();
|
||||
return langs[ext] ? langs[ext] : '';
|
||||
},
|
||||
|
||||
saveContent(path, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$ajax({
|
||||
task: 'explorer.saveContent',
|
||||
path,
|
||||
content,
|
||||
})
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
emitEditorStatus({status, path}) {
|
||||
this.$emit('statusChange', {
|
||||
status,
|
||||
path,
|
||||
});
|
||||
},
|
||||
|
||||
getFileContent(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.$ajax({
|
||||
task: 'explorer.openFile',
|
||||
path: path
|
||||
})
|
||||
.then(res => {
|
||||
if (!res || res.error) {
|
||||
reject();
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addSnippet(monaco) {
|
||||
monaco.languages.registerCompletionItemProvider('php', {
|
||||
provideCompletionItems: function(model, position) {
|
||||
const textUntilPosition = model.getValueInRange({startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column});
|
||||
const fragsA = textUntilPosition.split('<?php');
|
||||
const fragsB = textUntilPosition.split('?>');
|
||||
if (textUntilPosition.indexOf('<?php') === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fragsB.length === 1 || fragsB.pop().length > fragsA.pop().length) {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'pre',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
documentation: "Print Result",
|
||||
insertText: "echo '<pre>' . print_r(${1:true}, 1) . '</pre>';",
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
range: range
|
||||
},
|
||||
{
|
||||
label: 'd',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
documentation: "Print Result and Die",
|
||||
insertText: "die('<pre>'.print_r(${1:true}, 1).'</pre>');",
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
range: range
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
monaco.languages.registerCompletionItemProvider('javascript', {
|
||||
provideCompletionItems: function(model, position) {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'cl',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
documentation: "console.log",
|
||||
insertText: "console.log(${1:true});",
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
range: range
|
||||
},
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createEditor() {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.require(['vs/editor/editor.main'], () => {
|
||||
const monaco = window.monaco;
|
||||
if (!this.initedSnippet) {
|
||||
this.addSnippet(monaco);
|
||||
this.initedSnippet = true;
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
editor = monaco.editor.create(this.$el, {
|
||||
model: null,
|
||||
});
|
||||
}
|
||||
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => {
|
||||
const path = this.current;
|
||||
const data = eData[path];
|
||||
|
||||
if (!data || data.status === 'saving' || data.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.status = 'saving';
|
||||
|
||||
this.emitEditorStatus(data);
|
||||
this.$store.commit('lock', path);
|
||||
|
||||
const content = editor.getValue();
|
||||
const tmpSaved = data.model.getAlternativeVersionId();
|
||||
|
||||
this.saveContent(path, content).then(res => {
|
||||
if (res.error) {
|
||||
alert(res.error)
|
||||
} else {
|
||||
data.lastSaved = tmpSaved;
|
||||
}
|
||||
|
||||
this.$store.commit('unlock', path);
|
||||
})
|
||||
.catch(error => {
|
||||
alert('save error');
|
||||
console.log(error);
|
||||
|
||||
this.$store.commit('unlock', path);
|
||||
})
|
||||
.finally(() => {
|
||||
this.checkDirty(data);
|
||||
this.emitEditorStatus(data);
|
||||
});
|
||||
});
|
||||
|
||||
editor.addAction({
|
||||
id: 'copy-relative-path',
|
||||
label: 'Copy Relative Path',
|
||||
contextMenuGroupId: 'navigation',
|
||||
run: ed => {
|
||||
navigator.clipboard.writeText(this.current).then(() => {
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'Copied'
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeFile(path) {
|
||||
delete eData[path];
|
||||
},
|
||||
|
||||
resetEditor() {
|
||||
editor && editor.dispose();
|
||||
editor = '';
|
||||
this.current = '';
|
||||
},
|
||||
|
||||
resizeEditorLayout: debounce(function() {
|
||||
editor && editor.layout();
|
||||
}, 100),
|
||||
|
||||
computeEditorHeight: debounce(function() {
|
||||
const $ = jQuery;
|
||||
const wHeight = $(window).height();
|
||||
|
||||
if ($('.e-folders').length) {
|
||||
this.editorHeight = (wHeight - $('.e-folders').offset().top - 35) + 'px';
|
||||
}
|
||||
}, 100),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.e-content-editor {
|
||||
border: solid 1px #ddd;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="e-sidebar">
|
||||
<div class="e-sidebar-header">
|
||||
</div>
|
||||
<ETree />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ETree from './ETree.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ETree,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
app: 'explorer',
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeApp() {
|
||||
this.$store.dispatch('setApp', this.app);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.e-sidebar {
|
||||
width: 300px;
|
||||
|
||||
.e-sidebar-header {
|
||||
min-height: 40px;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 3px;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<li class="e-tree-item" :class="{'e-folder': isFolder, 'e-file': !isFolder}">
|
||||
<div
|
||||
class="e-tree-item-title"
|
||||
:class="{selected: isSelected}"
|
||||
@click="onClick"
|
||||
@dblclick="onDblclick"
|
||||
@contextmenu.stop.prevent="openContextMenu"
|
||||
>
|
||||
<span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span> {{ item.name }}
|
||||
</div>
|
||||
|
||||
<ul v-show="isOpen" v-if="isFolder">
|
||||
<ETreeItem
|
||||
v-for="child in item.children"
|
||||
:key="child.path"
|
||||
:item="child"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { EventBus } from '../event-bus.js';
|
||||
import { arrange } from '../utils';
|
||||
|
||||
export default {
|
||||
name: 'ETreeItem',
|
||||
|
||||
props: {
|
||||
item: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: this.item.path === '\\' ? true : false,
|
||||
fetching: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isFolder() {
|
||||
return this.item.type === 'folder';
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
return this.item.path === this.$store.state.selectedPath;
|
||||
},
|
||||
|
||||
isRoot() {
|
||||
return this.item.path === '\\';
|
||||
},
|
||||
|
||||
isImage() {
|
||||
if (this.isFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pieces = this.item.name.split('.');
|
||||
if (pieces.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ext = pieces.pop().toLowerCase();
|
||||
const imageExt = ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'];
|
||||
|
||||
if (imageExt.indexOf(ext) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
openContextMenu(event) {
|
||||
this.$store.commit('selectPath', this.item.path);
|
||||
|
||||
EventBus.$emit('openContextMenu', {event, item: this.item});
|
||||
},
|
||||
|
||||
onDblclick() {
|
||||
if (this.isFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isImage) {
|
||||
return EventBus.$emit('openImage', this.item);
|
||||
}
|
||||
|
||||
const {lockedFiles} = this.$store.state;
|
||||
const isLocked = lockedFiles.indexOf(this.item.path) > -1;
|
||||
|
||||
if (isLocked) {
|
||||
alert('File is locked for opening or saving. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
EventBus.$emit('openFileEditor', {
|
||||
item: this.item,
|
||||
force: true
|
||||
});
|
||||
},
|
||||
|
||||
onClick() {
|
||||
const path = this.isFolder ? '' : this.item.path;
|
||||
|
||||
this.$store.commit('selectPath', path);
|
||||
|
||||
if (this.fetching || !this.isFolder || this.isRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = !this.isOpen;
|
||||
|
||||
if (!this.item.children) {
|
||||
this.fetching = true;
|
||||
|
||||
this.$ajax({
|
||||
task: 'explorer.explodeFolder',
|
||||
path: this.item.path,
|
||||
})
|
||||
.then(res => {
|
||||
this.fetching = false;
|
||||
|
||||
Vue.set(this.item, 'children', arrange(res));
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false;
|
||||
alert('error');
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.e-tree-item-title {
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color 300ms;
|
||||
|
||||
&.selected {
|
||||
background-color: #cacaca !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Vue from 'vue';
|
||||
export const EventBus = new Vue();
|
||||
@@ -0,0 +1,70 @@
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import store from './store';
|
||||
import {
|
||||
Select,
|
||||
Option,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
MessageBox,
|
||||
Loading,
|
||||
Dialog,
|
||||
Upload,
|
||||
Pagination,
|
||||
} from "element-ui";
|
||||
import lang from 'element-ui/lib/locale/lang/en';
|
||||
import locale from 'element-ui/lib/locale';
|
||||
import {translate} from './utils';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
__webpack_public_path__ = FF_EXPLORER_DATA.path.root + 'administrator/components/com_ffexplorer/assets/explorer/dist/';
|
||||
|
||||
locale.use(lang)
|
||||
|
||||
Vue.use(Select);
|
||||
Vue.use(Option);
|
||||
Vue.use(Input);
|
||||
Vue.use(Button);
|
||||
Vue.use(Dialog);
|
||||
Vue.use(Upload);
|
||||
Vue.use(Pagination);
|
||||
Vue.use(Loading.directive);
|
||||
|
||||
Vue.prototype.$loading = Loading.service;
|
||||
Vue.prototype.$msgbox = MessageBox;
|
||||
Vue.prototype.$alert = MessageBox.alert;
|
||||
Vue.prototype.$confirm = MessageBox.confirm;
|
||||
Vue.prototype.$prompt = MessageBox.prompt;
|
||||
Vue.prototype.$message = Message;
|
||||
|
||||
Vue.prototype.$t = function(text) {
|
||||
return translate(text);
|
||||
}
|
||||
|
||||
Vue.prototype.$ajax = function(options, method) {
|
||||
const $ = jQuery;
|
||||
const {path, params} = FF_EXPLORER_DATA;
|
||||
const url = path.ajax;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax({
|
||||
method: method ? method : 'post',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
data: $.extend({}, params, options),
|
||||
})
|
||||
.done(response => {
|
||||
resolve(response);
|
||||
})
|
||||
.fail(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#explorer-app');
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: true,
|
||||
|
||||
state: {
|
||||
app: '',
|
||||
selectedPath: '',
|
||||
lockedFiles: [],
|
||||
history: [],
|
||||
db_keyword: '',
|
||||
activeTable: '',
|
||||
},
|
||||
|
||||
actions: {
|
||||
setApp({state, commit}, payload) {
|
||||
commit('setApp', payload);
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
setActiveTable(state, value) {
|
||||
state.activeTable = value;
|
||||
},
|
||||
|
||||
searchDb(state, keyword) {
|
||||
state.db_keyword = keyword;
|
||||
},
|
||||
|
||||
setApp(state, app) {
|
||||
state.app = app;
|
||||
},
|
||||
|
||||
selectPath(state, path) {
|
||||
state.selectedPath = path;
|
||||
},
|
||||
|
||||
lock({lockedFiles}, path) {
|
||||
if (lockedFiles.indexOf(path) < 0) {
|
||||
lockedFiles.push(path);
|
||||
}
|
||||
},
|
||||
|
||||
unlock({lockedFiles}, path) {
|
||||
const idx = lockedFiles.findIndex(item => item === path);
|
||||
|
||||
if (idx !== -1) {
|
||||
lockedFiles.splice(idx, 1);
|
||||
}
|
||||
},
|
||||
|
||||
setHistory({history}, path) {
|
||||
const idx = history.findIndex(item => item === path);
|
||||
|
||||
if (idx !== -1) {
|
||||
history.splice(idx, 1);
|
||||
}
|
||||
|
||||
history.push(path);
|
||||
},
|
||||
|
||||
deleteHistory({history}, path) {
|
||||
const idx = history.findIndex(item => item === path);
|
||||
|
||||
if (idx !== -1) {
|
||||
history.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
export const arrange = function(items) {
|
||||
items.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'folder' ? -1 : 1;
|
||||
}
|
||||
|
||||
const min = Math.min(a.name.length, b.name.length);
|
||||
|
||||
for (let i = 0; i < min; i++) {
|
||||
const aSub = a.name.toLowerCase().substring(i, 1);
|
||||
const bSub = b.name.toLowerCase().substring(i, 1);
|
||||
|
||||
if (aSub !== bSub) {
|
||||
return aSub < bSub ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return a.name.length < b.name.length ? - 1 : 1;
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export const translate = function(text) {
|
||||
const {language} = FF_EXPLORER_DATA;
|
||||
return language[text] || text;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
const path = require('path');
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = env => {
|
||||
const isDev = env && env.dev;
|
||||
|
||||
return {
|
||||
watch: isDev ? true : false,
|
||||
|
||||
mode: isDev ? 'development' : 'production',
|
||||
|
||||
entry: './src/index.js',
|
||||
|
||||
output: {
|
||||
filename: 'app.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|woff)$/i,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
plugins: [
|
||||
[
|
||||
'component',
|
||||
{
|
||||
libraryName: 'element-ui',
|
||||
styleLibraryName: 'theme-chalk',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'app.css',
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
1911
administrator/components/com_ffexplorer/assets/monaco/loader.js
Normal file
1911
administrator/components/com_ffexplorer/assets/monaco/loader.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user