virtualsplit and searchbar vue component, TestSearch Controller and View for Demonstration, minor change to SearchBar Controller to work with posted json

This commit is contained in:
Harald Bamberger
2022-06-24 19:26:37 +02:00
parent 147ab92bae
commit 47f7c03075
18 changed files with 1056 additions and 4 deletions
@@ -34,8 +34,12 @@ class SearchBar extends FHC_Controller
*/
public function search()
{
$searchstr = $this->input->post(self::SEARCHSTR_PARAM);
$types = $this->input->post(self::TYPES_PARAM);
//$searchstr = $this->input->post(self::SEARCHSTR_PARAM);
//$types = $this->input->post(self::TYPES_PARAM);
$json = json_decode($this->input->raw_input_stream, true);
$searchstr = $json[self::SEARCHSTR_PARAM];
$types = $json[self::TYPES_PARAM];
$this->outputJson($this->searchbarlib->search($searchstr, $types));
}
@@ -0,0 +1,44 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
* Test Search Vue Component
*/
class TestSearch extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct(
array(
'index' => 'system/developer:r'
)
);
// Loads WidgetLib
$this->load->library('WidgetLib');
// Loads phrases system
$this->loadPhrases(
array(
'global',
'ui',
'filter'
)
);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* Everything has a beginning
*/
public function index()
{
$this->load->view('system/logs/testSearch.php');
}
}
+1 -1
View File
@@ -191,7 +191,7 @@ class SearchBarLib
/**
*
*/
private function raum($searchstr, $type)
private function _raum($searchstr, $type)
{
return array();
}
@@ -0,0 +1,60 @@
<?php
$includesArray = array(
'title' => 'Test Search',
'jquery3' => true,
'bootstrap5' => true,
'fontawesome6' => true,
'tablesorter2' => true,
'vue3' => true,
'ajaxlib' => true,
'jqueryui1' => true,
'filtercomponent' => true,
'navigationcomponent' => true,
'phrases' => array(
'global' => array('mailAnXversandt'),
'ui' => array('bitteEintragWaehlen')
),
'customCSSs' => array(
'public/css/components/verticalsplit.css',
'public/css/components/searchbar.css',
),
'customJSs' => array('vendor/axios/axios/axios.min.js'),
'customJSModules' => array('public/js/apps/TestSearch.js')
);
$this->load->view('templates/FHC-Header', $includesArray);
?>
<div id="main">
<!-- Navigation component -->
<core-navigation-cmpt :add-side-menu-entries="appSideMenuEntries"></core-navigation-cmpt>
<div id="content">
<div class="row">
<div class="col-lg-12">
<h3 class="page-header">
Test Search
</h3>
</div>
</div>
<div>
<searchbar :searchoptions="searchbaroptions" :searchfunction="searchfunction"></searchbar>
<verticalsplit>
<template #top>
<searchbar :searchoptions="searchbaroptions" :searchfunction="searchfunctiondummy"></searchbar>
</template>
<template #bottom>
<!-- Filter component -->
<core-filter-cmpt filter-type="LogsViewer" @nw-new-entry="newSideMenuEntryHandler"></core-filter-cmpt>
</template>
</verticalsplit>
</div>
</div>
</div>
<?php $this->load->view('templates/FHC-Footer', $includesArray); ?>
+12 -1
View File
@@ -325,6 +325,17 @@
"type": "file"
}
}
},
{
"type": "package",
"package": {
"name": "axios/axios",
"version": "0.27.2",
"dist": {
"url": "https://unpkg.com/axios@0.27.2/dist/axios.min.js",
"type": "file"
}
}
}
],
"require": {
@@ -367,7 +378,7 @@
"twbs/bootstrap5": "5.1.*",
"vuejs/vuejs3": "3.2.33",
"axios/axios": "0.27.2",
"alvaro-prieto/colresizable": "1.6",
+81
View File
@@ -0,0 +1,81 @@
/*
Created on : Jun 15, 2022, 3:21:25 PM
Author : bambi
*/
.searchbar_settings {
position: absolute;
z-index: 9999;
background-color: #fff;
border: 2px solid #666;
padding: 1rem;
}
.searchbar_results {
position: absolute;
z-index: 9998;
background-color: #fff;
border: 2px solid #666;
padding: 1rem;
overflow-y: auto;
}
.searchbar_result {
border-bottom: 1px solid #666;
margin-bottom: 1rem;
}
.searchbar_grid {
display: grid;
grid-template-columns: [icon] 100px [data] auto;
}
.searchbar_icon {
grid-column-start: icon;
grid-column-end: span 1;
}
.searchbar_data {
grid-column-start: data;
grid-column-end: span 1;
}
.searchbar_actions {
display: flex;
padding: 0;
margin-top: 1rem;
}
.searchbar_actions li {
list-style: none;
text-align: center;
padding: 3px;
}
.searchbar_table {
display: table;
}
.searchbar_tablerow {
display: table-row;
}
.searchbar_tablecell {
display: table-cell;
min-width: 175px;
padding: 0 6px;
}
.searchbar_settings_types {
display: flex;
padding: 0;
}
.searchbar_settings_types li {
list-style: none;
padding: 5px;
}
.searchbar_settings_types li label {
padding: 3px;
}
+59
View File
@@ -0,0 +1,59 @@
/*
Created on : May 19, 2022, 10:16:21 AM
Author : bambi
*/
:root {
--fhc-verticalsplit-vsplitter-bg-color: #eee;
--fhc-verticalsplit-vsplitter-border-color: #eee;
--fhc-verticalsplit-vsplitter-splitactions-color: #000;
}
.verticalsplitted {
overflow-y: auto;
}
.verticalsplitter {
text-align: center;
}
.verticalsplitter.top {
border-top: solid 3px var(--fhc-verticalsplit-vsplitter-border-color);
}
.verticalsplitter.bottom {
border-bottom: solid 3px var(--fhc-verticalsplit-vsplitter-border-color);
}
.splitactions {
background-color: var(--fhc-verticalsplit-vsplitter-bg-color);
color: var(--fhc-verticalsplit-vsplitter-splitactions-color);
display: inline-block;
padding: 0 5px 0 5px;
}
.splitactions.top {
border-radius: 0 0 40% 40%;
}
.splitactions.bottom {
border-radius: 40% 40% 0 0;
}
.splitaction {
display: inline-block;
width: 25px;
cursor: pointer;
}
.splitaction.resize {
cursor: row-resize;
}
#content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
#content > div:first-child {
margin-top: 30px;
}
+195
View File
@@ -0,0 +1,195 @@
import {CoreFilterCmpt} from '../components/Filter.js';
import {CoreNavigationCmpt} from '../components/Navigation.js';
import verticalsplit from "../components/verticalsplit/verticalsplit.js";
import searchbar from "../components/searchbar/searchbar.js";
import fhcapifactory from "./api/fhcapifactory.js";
Vue.$fhcapi = fhcapifactory;
Vue.createApp({
"data": function() {
return {
"title": "Test Search",
"appSideMenuEntries": {},
"searchbaroptions": {
"types": [
"person",
"raum",
"mitarbeiter",
"student",
"prestudent",
"document",
"cms",
"organisationunit"
],
"actions": {
"person": {
"defaultaction": {
"type": "link",
"action": function(data) {
//alert('person defaultaction ' + JSON.stringify(data));
//window.location.href = data.profil;
return data.profil;
}
},
"childactions": [
{
"label": "testchildaction1",
"icon": "fas fa-check-circle",
"type": "function",
"action": function(data) {
alert('person testchildaction 01 ' + JSON.stringify(data));
}
},
{
"label": "testchildaction2",
"icon": "fas fa-file-csv",
"type": "function",
"action": function(data) {
alert('person testchildaction 02 ' + JSON.stringify(data));
}
}
]
},
"raum": {
"defaultaction": {
"type": "function",
"action": function(data) {
alert('raum defaultaction ' + JSON.stringify(data));
}
},
"childactions": [
{
"label": "Rauminformation",
"icon": "fas fa-info-circle",
"type": "link",
"action": function(data) {
return data.infolink;
}
},
{
"label": "Raumreservierung",
"icon": "fas fa-bookmark",
"type": "link",
"action": function(data) {
return data.booklink;
}
}
]
},
"employee": {
"defaultaction": {
"type": "function",
"action": function(data) {
alert('employee defaultaction ' + JSON.stringify(data));
}
},
"childactions": [
{
"label": "testchildaction1",
"icon": "fas fa-address-book",
"type": "function",
"action": function(data) {
alert('employee testchildaction 01 ' + JSON.stringify(data));
}
},
{
"label": "testchildaction2",
"icon": "fas fa-user-slash",
"type": "function",
"action": function(data) {
alert('employee testchildaction 02 ' + JSON.stringify(data));
}
},
{
"label": "testchildaction3",
"icon": "fas fa-bell",
"type": "function",
"action": function(data) {
alert('employee testchildaction 03 ' + JSON.stringify(data));
}
},
{
"label": "testchildaction4",
"icon": "fas fa-calculator",
"type": "function",
"action": function(data) {
alert('employee testchildaction 04 ' + JSON.stringify(data));
}
}
]
},
"organisationunit": {
"defaultaction": {
"type": "function",
"action": function(data) {
alert('organisationunit defaultaction ' + JSON.stringify(data));
}
},
"childactions": []
}
}
},
"searchbaroptions2": {
"types": [
"raum",
"organisationunit"
],
"actions": {
"raum": {
"defaultaction": {
"type": "function",
"action": function(data) {
alert('raum defaultaction ' + JSON.stringify(data));
}
},
"childactions": [
{
"label": "Rauminformation",
"icon": "fas fa-info-circle",
"type": "link",
"action": function(data) {
return data.infolink;
}
},
{
"label": "Raumreservierung",
"icon": "fas fa-bookmark",
"type": "link",
"action": function(data) {
return data.booklink;
}
}
]
},
"organisationunit": {
"defaultaction": {
"type": "function",
"action": function(data) {
alert('organisationunit defaultaction ' + JSON.stringify(data));
}
},
"childactions": []
}
}
}
};
},
"components": {
"CoreNavigationCmpt": CoreNavigationCmpt,
"CoreFilterCmpt": CoreFilterCmpt,
"verticalsplit": verticalsplit,
"searchbar": searchbar
},
"methods": {
"newSideMenuEntryHandler": function(payload) {
this.appSideMenuEntries = payload;
},
"searchfunction": function(searchsettings) {
return Vue.$fhcapi.Search.search(searchsettings);
},
"searchfunctiondummy": function(searchsettings) {
return Vue.$fhcapi.Search.searchdummy(searchsettings);
}
}
}).mount('#main');
+5
View File
@@ -0,0 +1,5 @@
import Search from "./search.js";
export default {
"Search": Search
};
+12
View File
@@ -0,0 +1,12 @@
export default {
search: function(searchsettings) {
const url = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'index.ci.php/components/SearchBar/search';
return axios.post(url, searchsettings);
},
searchdummy: function(searchsettings) {
const url = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'public/js/apps/api/dummyapi.php/Search';
return axios.post(url, searchsettings);
}
};
+31
View File
@@ -0,0 +1,31 @@
export default {
props: {
res: {
type: Object
},
action: {
type: Object
},
cssclass: {
type: String,
default: ''
}
},
emits: [ 'actionexecuted' ],
template: `
<a :class="this.cssclass" :href="this.getactionhref()"
@click="(this.action.type === 'function') ? this.execaction() : null">
<slot>Action</slot>
</a>
`,
methods: {
getactionhref: function() {
return (this.action.type === 'link') ? this.action.action(this.res)
: 'javascript:void(0);';
},
execaction: function() {
this.action.action(this.res);
this.$emit('actionexecuted');
}
}
};
+28
View File
@@ -0,0 +1,28 @@
import action from "./action.js";
export default {
props: [ "res", "actions" ],
components: {
action: action
},
emits: [ 'actionexecuted' ],
template: `
<ul class="searchbar_actions" v-if="this.actions.length > 0">
<li v-for="(action, index) in this.actions" :key="action.label">
<action :res="this.res" :action="action" :cssclass="'btn btn-primary btn-sm'" @actionexecuted="$emit('actionexecuted')">
<i v-if="this.hasicon(index)" :class="this.geticonclass(index)"></i>
<span class="p-2">{{ action.label }}</span>
</action>
</li>
</ul>
<div class="mb-3" v-else=""></div>
`,
methods: {
hasicon: function(index) {
return (typeof this.actions[index].icon !== "undefined");
},
geticonclass: function(index) {
return this.actions[index].icon;
}
}
};
@@ -0,0 +1,76 @@
import action from "./action.js";
import actions from "./actions.js";
export default {
props: [ "res", "actions" ],
components: {
action: action,
actions: actions
},
emits: [ 'actionexecuted' ],
template: `
<div class="searchbar_result searchbar_employee">
<div class="searchbar_grid">
<div class="searchbar_icon">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<img v-if="res.foto !== null" :src="res.foto"
class="rounded-circle" height="100" />
<i v-else class="fas fa-user-circle fa-5x"></i>
</action>
</div>
<div class="searchbar_data">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<span class="fw-bold">{{ res.name }}</span>
</action>
<div class="mb-3"></div>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">OrgEinheit</div>
<div class="searchbar_tablecell">
{{ res.organisationunit_name }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMail</div>
<div class="searchbar_tablecell">
<a :href="this.mailtourl">
{{ res.email }}
</a>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Phone</div>
<div class="searchbar_tablecell">
<a :href="this.telurl">
{{ res.phone }}
</a>
</div>
</div>
</div>
<actions :res="this.res" :actions="this.actions.childactions" @actionexecuted="$emit('actionexecuted')"></actions>
</div>
</div>
</div>
`,
methods: {
},
computed: {
mailtourl: function() {
return 'mailto:' + this.res.email;
},
telurl: function() {
return 'tel:' + this.res.phone;
}
}
};
@@ -0,0 +1,79 @@
import action from "./action.js";
import actions from "./actions.js";
export default {
props: [ "res", "actions" ],
components: {
action: action,
actions: actions
},
emits: [ 'actionexecuted' ],
template: `
<div class="searchbar_result searchbar_organisationunit">
<div class="searchbar_grid">
<div class="searchbar_icon">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<i class="fas fa-sitemap fa-4x"></i>
</action>
</div>
<div class="searchbar_data">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<span class="fw-bold">{{ res.name }}</span>
</action>
<div class="mb-3"></div>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">übergeordnete OrgEinheit</div>
<div class="searchbar_tablecell">
{{ res.parentoe_name }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Gruppen-EMail</div>
<div class="searchbar_tablecell">
<a :href="this.mailtourl">
{{ res.mailgroup }}
</a>
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Leiter</div>
<div class="searchbar_tablecell">
{{ res.leader_name }}
</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Mitarbeiter-Anzahl</div>
<div class="searchbar_tablecell">
{{ res.number_of_people }}
</div>
</div>
</div>
<actions :res="this.res" :actions="this.actions.childactions" @actionexecuted="$emit('actionexecuted')"></actions>
</div>
</div>
</div>
`,
methods: {
},
computed: {
mailtourl: function() {
return 'mailto:' + this.res.mailgroup;
},
telurl: function() {
return 'tel:' + this.res.phone;
}
}
};
+55
View File
@@ -0,0 +1,55 @@
import action from "./action.js";
import actions from "./actions.js";
export default {
props: [ "res", "actions" ],
components: {
action: action,
actions: actions
},
emits: [ 'actionexecuted' ],
template: `
<div class="searchbar_result searchbar_person">
<div class="searchbar_grid">
<div class="searchbar_icon">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<img v-if="res.foto !== null" :src="res.foto"
class="rounded-circle" height="100" />
<i v-else class="fas fa-user-circle fa-5x"></i>
</action>
</div>
<div class="searchbar_data">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<span class="fw-bold">{{ res.gn }} {{ res.sn }}</span>
</action>
<div class="mb-3"></div>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">EMail</div>
<div class="searchbar_tablecell">
<a :href="this.mailtourl">
{{ res.mail }}
</a>
</div>
</div>
</div>
<actions :res="this.res" :actions="this.actions.childactions" @actionexecuted="$emit('actionexecuted')"></actions>
</div>
</div>
</div>
`,
methods: {
},
computed: {
mailtourl: function() {
return 'mailto:' + this.res.mail;
}
}
};
+51
View File
@@ -0,0 +1,51 @@
import action from "./action.js";
import actions from "./actions.js";
export default {
props: [ "res", "actions" ],
components: {
action: action,
actions: actions
},
emits: [ 'actionexecuted' ],
template: `
<div class="searchbar_result searchbar_raum">
<div class="searchbar_grid">
<div class="searchbar_icon">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<i class="fas fa-door-open fa-4x"></i>
</action>
</div>
<div class="searchbar_data">
<action :res="this.res" :action="this.actions.defaultaction" @actionexecuted="$emit('actionexecuted')">
<span class="fw-bold">{{ res.r }}</span>
</action>
<div class="mb-3"></div>
<div class="searchbar_table">
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Gebäude</div>
<div class="searchbar_tablecell">{{ res.g }}</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Stockwerk</div>
<div class="searchbar_tablecell">{{ res.s }}</div>
</div>
<div class="searchbar_tablerow">
<div class="searchbar_tablecell">Raumnummer</div>
<div class="searchbar_tablecell">{{ res.rn }}</div>
</div>
</div>
<actions :res="this.res" :actions="this.actions.childactions" @actionexecuted="$emit('actionexecuted')"></actions>
</div>
</div>
</div>
`,
methods: {
}
};
+132
View File
@@ -0,0 +1,132 @@
import person from "./person.js";
import raum from "./raum.js";
import employee from "./employee.js";
import organisationunit from "./organisationunit.js";
export default {
props: [ "searchoptions", "searchfunction" ],
data: function() {
return {
searchtimer: null,
showsettings: false,
searchsettings: {
searchstr: '',
types: []
},
showresult: false,
searchresult: [],
searching: false,
error: null
};
},
components: {
person: person,
raum: raum,
employee: employee,
organisationunit: organisationunit
},
template: `
<form class="d-flex me-3">
<div class="input-group me-2 bg-white">
<input ref="searchbox" @keyup="this.search" @focus="this.showsearchresult" v-model="this.searchsettings.searchstr" class="form-control" type="search" placeholder="Search" aria-label="Search">
<button ref="settingsbutton" @click="this.togglesettings" class="btn btn-outline-secondary" type="button" id="search-filter"><i class="fas fa-cog"></i></button>
</div>
<button @click="this.search" class="btn btn-outline-success" type="button">Search</button>
</form>
<div v-show="this.showresult" ref="result" class="searchbar_results">
<div v-if="this.searching">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div v-else-if="this.error !== null">{{ this.error }}</div>
<div v-else-if="this.searchresult.length < 1">Es wurden keine Ergebnisse gefunden.</div>
<template v-else="" v-for="res in this.searchresult">
<person v-if="res.type === 'person'" :res="res" :actions="this.searchoptions.actions.person" @actionexecuted="this.hideresult"></person>
<employee v-else-if="res.type === 'mitarbeiter'" :res="res" :actions="this.searchoptions.actions.employee" @actionexecuted="this.hideresult"></employee>
<organisationunit v-else-if="res.type === 'organisationunit'" :res="res" :actions="this.searchoptions.actions.organisationunit" @actionexecuted="this.hideresult"></organisationunit>
<raum v-else-if="res.type === 'raum'" :res="res" :actions="this.searchoptions.actions.raum" @actionexecuted="this.hideresult"></raum>
<div v-else="">Unbekannter Ergebnistyp: '{{ res.type }}'.</div>
</template>
</div>
<div v-show="this.showsettings" ref="settings" class="searchbar_settings">
<div class="btn-group" v-if="this.searchoptions.types.length > 0">
<template v-for="(type, index) in this.searchoptions.types" :key="type">
<input type="checkbox" class="btn-check" :id="this.$.uid + 'search_type_' + index" :value="type" v-model="this.searchsettings.types"/>
<label class="btn btn-outline-secondary" :for="this.$.uid + 'search_type_' + index">{{ type }}</label>
</template>
</div>
<div class="mb-2"></div>
<button ref="settingsrefreshsearch" @click="this.refreshsearch" class="btn btn-primary">Übernehmen</button>
</div>
`,
beforeMount: function() {
this.updateSearchOptions();
},
methods: {
updateSearchOptions: function() {
this.searchsettings.types = [];
for( const idx in this.searchoptions.types ) {
this.searchsettings.types.push(this.searchoptions.types[idx]);
}
},
search: function() {
if( this.searchtimer !== null ) {
clearTimeout(this.searchtimer);
}
if( this.searchsettings.searchstr.length >= 3 ) {
var rect = this.$refs.searchbox.getBoundingClientRect();
//console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect));
this.$refs.result.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.result.style.right = Math.floor(window.innerWidth - rect.right) + 'px';
this.$refs.result.style.width = Math.floor(window.innerWidth * 0.75) + 'px';
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.75) + 'px';
this.searchtimer = setTimeout(
this.callsearchapi,
500
);
} else {
this.showresult = false;
}
},
callsearchapi: function() {
var that = this;
this.error = null;
this.searchresult = [];
this.searching = true;
this.showsearchresult();
this.searchfunction(this.searchsettings)
.then(function(response) {
that.searchresult = response.data.data;
})
.catch(function(error) {
that.error = 'Bei der Suche ist ein Fehler aufgetreten.'
+ ' ' + error.message;
})
.finally(function() {
that.searching = false;
});
},
refreshsearch: function() {
this.search();
this.togglesettings();
},
togglesettings: function() {
this.showsettings = !this.showsettings;
var rect = this.$refs.settingsbutton.getBoundingClientRect();
//console.log(window.innerWidth + ' ' + window.innerHeight + ' ' + JSON.stringify(rect));
this.$refs.settings.style.top = Math.floor(rect.bottom + 3) + 'px';
this.$refs.settings.style.right = Math.floor(window.innerWidth - rect.right) + 'px';
this.$refs.settings.style.width = Math.floor(window.innerWidth * 0.5) + 'px';
//this.$refs.settings.style.height = Math.floor(window.innerHeight * 0.5) + 'px';
},
hideresult: function(e) {
//window.removeEventListener('click', this.hideresult);
this.showresult = false;
},
showsearchresult: function() {
if( this.searchsettings.searchstr.length >= 3 ) {
this.showresult = true;
//window.addEventListener('click', this.hideresult);
}
}
}
};
@@ -0,0 +1,129 @@
export default {
data: function() {
return {
availHeight: 0,
topheight: 0,
bottomheight: 0,
mousePosY: 0,
resize: false,
vsplitter: null,
vsplitterOffset: 0,
selfOffsetTop: 0
};
},
template: `
<div ref="verticalsplit">
<div ref="toppanel" class="verticalsplitted"
:style="{height: this.topheightcss}">
<slot name="top">
<p>Top Panel</p>
</slot>
</div>
<div ref="vsplitter" class="verticalsplitter"
:class="this.topOrBottomClass" @mousedown="this.dragStart">
<div class="splitactions" :class="this.topOrBottomClass">
<span @click="this.collapseTop" class="splitaction">
<i class="fas fa-angle-up"></i>
</span>
<span @dblclick="this.showBoth" class="splitaction resize">
<i class="fas fa-grip-horizontal"></i>
</span>
<span @click="this.collapseBottom" class="splitaction">
<i class="fas fa-angle-down"></i>
</span>
</div>
</div>
<div ref="bottompanel" class="verticalsplitted"
:style="{height: this.bottomheightcss}">
<slot name="bottom">
<p>Bottom Panel</p>
</slot>
</div>
</div>
`,
mounted: function() {
this.calcHeights();
this.trackVerticalSplitterOffsetTop();
window.addEventListener('resize', this.calcHeights);
},
updated: function() {
this.trackVerticalSplitterOffsetTop();
},
methods: {
calcHeights: function() {
var windowheight = window.innerHeight;
var oldavailHeight = this.availHeight;
this.selfOffsetTop = this.$refs.verticalsplit.offsetTop;
this.availHeight = windowheight - this.selfOffsetTop - this.$refs.vsplitter.offsetHeight;
if( (this.topheight === 0 && this.bottomheight === 0) || oldavailHeight === 0 ) {
this.topheight = Math.floor(this.availHeight/2);
} else {
this.topheight = Math.floor( ((((this.topheight * 100) / oldavailHeight) / 100) * this.availHeight) );
}
this.bottomheight = this.availHeight - this.topheight;
},
collapseTop: function() {
this.calcHeights();
this.topheight = 0;
this.bottomheight = this.availHeight;
},
collapseBottom: function() {
this.calcHeights();
this.topheight = this.availHeight;
this.bottomheight = 0;
},
showBoth: function() {
this.topheight = Math.floor(this.availHeight/2);
this.bottomheight = Math.floor(this.availHeight/2);
},
dragStart: function(e) {
e.preventDefault();
e.stopPropagation();
window.addEventListener('mouseup', this.dragEnd);
window.addEventListener('mousemove', this.drag);
this.resize = true;
this.mousePosY = e.clientY;
},
drag: function(e) {
if( !this.resize ) {
return;
}
e.preventDefault();
e.stopPropagation();
var offsetY = e.clientY - this.mousePosY;
this.topheight = this.topheight + offsetY;
if( this.topheight < 0 ) {
this.topheight = 0;
}
if( this.topheight > this.availHeight ) {
this.topheight = this.availHeight;
}
this.bottomheight = this.availHeight - this.topheight;
this.mousePosY = e.clientY;
},
dragEnd: function(e) {
e.preventDefault();
e.stopPropagation();
window.removeEventListener('mousemove', this.drag);
window.removeEventListener('mouseup', this.dragEnd);
this.resize = false;
this.mousePosY = e.clientY;
},
trackVerticalSplitterOffsetTop: function() {
this.vsplitterOffset = this.$refs.vsplitter.offsetTop;
}
},
computed: {
topOrBottomClass: function() {
return ((this.vsplitterOffset - this.selfOffsetTop) <= Math.floor(this.availHeight/2))
? 'top'
: 'bottom';
},
topheightcss: function() {
return this.topheight + 'px';
},
bottomheightcss: function() {
return this.bottomheight + 'px';
}
}
};