724 lines
18 KiB
JavaScript
724 lines
18 KiB
JavaScript
(function(){
|
|
|
|
let nextTooltipKey = 0;
|
|
var tooltipData = {};
|
|
|
|
var tootlipVisible = false;
|
|
var tooltipEl = LN.$(`<div class="ln-tooltip"></div>`);
|
|
|
|
|
|
|
|
function findTooltipData(el){
|
|
let tooltip = null;
|
|
while (!tooltip)
|
|
{
|
|
tooltip = tooltipData[el.dataset['tooltip']];
|
|
el = el.parentElement;
|
|
}
|
|
return tooltip;
|
|
}
|
|
|
|
function tooltipShow(ev,tooltip){
|
|
tooltipEl.innerText = tooltip.value;
|
|
tooltipEl.setAttribute("VISIBLE","");
|
|
|
|
tootlipVisible = true;
|
|
}
|
|
function tooltipHide(ev,tooltip){
|
|
tooltipEl.removeAttribute("VISIBLE");
|
|
tootlipVisible = false;
|
|
}
|
|
|
|
function tooltipMouseOver(ev){
|
|
if (!document.body.contains(tooltipEl)){
|
|
document.body.appendChild(tooltipEl);
|
|
LN.$idle(()=>{ tooltipMouseOver(ev);});
|
|
return;
|
|
}
|
|
|
|
let tooltip = findTooltipData(ev.target);
|
|
if (!tooltip)
|
|
console.log(ev);
|
|
|
|
if (tooltipEl){
|
|
tooltipEl.style.left = `${ev.pageX+3}px`;
|
|
tooltipEl.style.top = `${ev.pageY+3}px`;
|
|
}
|
|
|
|
if (tooltip.timeout)
|
|
clearTimeout(tooltip.timeout);
|
|
if (tootlipVisible){
|
|
tooltip.timeout = setTimeout(() => {
|
|
tooltipHide(ev,tooltip);
|
|
}, (tooltip.delay ? (tooltip.delay / 4) : 200));
|
|
} else {
|
|
tooltip.timeout = setTimeout(() => {
|
|
tooltipShow(ev,tooltip);
|
|
}, tooltip.delay || 800);
|
|
}
|
|
}
|
|
|
|
function tooltipMouseOut(ev){
|
|
let tooltip = findTooltipData(ev.target);
|
|
if (tooltip.timeout)
|
|
clearTimeout(tooltip.timeout);
|
|
tooltipHide(ev,tooltip);
|
|
}
|
|
|
|
Vue.directive('tooltip',{
|
|
bind: function(el, binding, vnode){
|
|
let tooltip = {
|
|
value: binding.value,
|
|
timeout: null,
|
|
delay: null,
|
|
};
|
|
let tooltipKey = nextTooltipKey++;
|
|
tooltipData[tooltipKey] = tooltip;
|
|
el.dataset['tooltip'] = tooltipKey;
|
|
|
|
el.addEventListener('mousemove',tooltipMouseOver);
|
|
el.addEventListener('mouseout',tooltipMouseOut);
|
|
|
|
},
|
|
update: function(el, binding, vnode, oldVnode){
|
|
let tooltip = findTooltipData(el);
|
|
tooltip.value = binding.value;
|
|
},
|
|
unbind: function(el, binding, vnode){
|
|
el.removeEventListener('mouseover',tooltipMouseOver);
|
|
el.removeEventListener('mouseout',tooltipMouseOut);
|
|
},
|
|
});
|
|
|
|
|
|
Vue.component('ln-statusbar',{
|
|
data: function(){
|
|
return {
|
|
current: LN.Promise.getCurrentPromises(),
|
|
};
|
|
},
|
|
computed: {
|
|
CurrentPromises: function(){
|
|
return this.current;
|
|
}
|
|
},
|
|
template: `
|
|
<div
|
|
class="ln-statusbar"
|
|
>
|
|
<div>{{ LNVue.$_.statusText }}</div>
|
|
<div style="flex-grow: 0;">
|
|
<span
|
|
v-if="!LNVue.$_.identity.UniqueID">NOT LOGGED IN
|
|
</span>
|
|
<span
|
|
v-if="LNVue.$_.identity.UniqueID">{{ LNVue.$_.identity.IdentityName }}
|
|
</span>
|
|
</div>
|
|
<div style="flex-grow: 0;">{{ LNVue.$_.socket && LNVue.$_.socket._state }}</div>
|
|
<div
|
|
class="ln-background-tasks"
|
|
>{{ Object.keys(CurrentPromises).length || "No" }} Background Tasks
|
|
<div
|
|
v-if="Object.keys(CurrentPromises).length"
|
|
>
|
|
<div
|
|
class="block"
|
|
v-for="promise in CurrentPromises"
|
|
:state="promise.state"
|
|
>{{ promise.label }}</div>
|
|
</div>
|
|
</div>
|
|
<div style="flex-grow: 0;">{{ LNVue.$_.Version() }}</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('ln-navitem',{
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
key: String,
|
|
},
|
|
template: `
|
|
<div
|
|
class="ln-navitem"
|
|
:id="'nav-' + key"
|
|
>
|
|
<router-link
|
|
class="ln-navitem"
|
|
v-if="value.path"
|
|
:to="value.path"
|
|
v-slot="{ href, route, navigate, isActive, isExactActive }"
|
|
>
|
|
<a
|
|
:href="href"
|
|
@click="navigate"
|
|
>{{ value.label || value.path }}</a>
|
|
</router-link>
|
|
<span
|
|
v-if="!value.path"
|
|
class="ln-navitem"
|
|
>{{ value.label }}</span>
|
|
<div class="ln-nav-children"
|
|
v-if="value.navigation"
|
|
>
|
|
<ln-navitem
|
|
v-for="(item,key) in value.navigation"
|
|
v-bind:value="item"
|
|
v-bind:key="key"
|
|
></ln-navitem>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('ln-navbar',{
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default: function(){
|
|
return LNVue.$_.navigation;
|
|
}
|
|
},
|
|
component: {
|
|
type: [ Object, String ],
|
|
default: 'ln-navitem',
|
|
},
|
|
},
|
|
template: `
|
|
<div class="ln-navbar">
|
|
<div
|
|
:is="component"
|
|
v-for="(item,key) in value"
|
|
v-bind:value="item"
|
|
v-bind:key="key"
|
|
>{{ item.label || item.path }}</div>
|
|
</div>`
|
|
});
|
|
|
|
Vue.component('ln-textfield',{
|
|
props: {
|
|
value: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
template: `<input
|
|
type="text"
|
|
v-bind:value="value"
|
|
v-on:input="$emit('input', $event.target.value)">`,
|
|
});
|
|
Vue.component('ln-password',{
|
|
props: {
|
|
value: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
template: `<input
|
|
type="password"
|
|
v-bind:value="value"
|
|
autocomplete="current-password"
|
|
v-on:input="$emit('input', $event.target.value)">`,
|
|
});
|
|
|
|
Vue.component('ln-textarea',{
|
|
props: {
|
|
value: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
},
|
|
template: `<textarea v-on:input="$emit('input', $event.target.value)">{{ value }}</textarea>`,
|
|
});
|
|
|
|
Vue.component('ln-number',{
|
|
model: {
|
|
prop: "value",
|
|
event: "input",
|
|
},
|
|
props: {
|
|
value: {
|
|
required: false,
|
|
type: Number,
|
|
},
|
|
float: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
|
|
template: `
|
|
<input
|
|
type="number"
|
|
v-bind:value="value"
|
|
v-on:input="$emit('input', $event.target.value )"
|
|
v-bind:step="float ? 'any' : 1"
|
|
>`,
|
|
});
|
|
|
|
Vue.component('ln-slider',{
|
|
model: {
|
|
prop: 'value',
|
|
event: 'change',
|
|
},
|
|
props: {
|
|
value: {
|
|
type: Number,
|
|
default: 1,
|
|
},
|
|
min: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
max: {
|
|
type: Number,
|
|
default: 100,
|
|
},
|
|
step: {
|
|
type: Number,
|
|
default: 1,
|
|
},
|
|
islogarithmic: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
values: {
|
|
default: null,
|
|
}
|
|
},
|
|
data: function(){
|
|
let d = {
|
|
};
|
|
|
|
if (this.values instanceof Array){
|
|
this.min = 0;
|
|
this.max = this.values.length - 1;
|
|
this.step = 1;
|
|
} else if (this.values instanceof Object){
|
|
this.min = 0;
|
|
this.max = Object.keys(this.values).length - 1;
|
|
this.step = 1;
|
|
} else if (this.islogarithmic){
|
|
/* ToDo: Implement */
|
|
} else {
|
|
|
|
}
|
|
|
|
// console.log(`min=${this.min} max=${this.max} step=${this.step} value=${this.value}`);
|
|
|
|
return d;
|
|
},
|
|
computed: {
|
|
keys: function(){
|
|
if (this.values instanceof Array)
|
|
{
|
|
let keys = new Array(this.values.length);
|
|
for (let n=0;n<keys.length;n++)
|
|
keys[n] = n;
|
|
return keys;
|
|
} else if (this.values instanceof Object)
|
|
{
|
|
return Object.keys(this.values);
|
|
}
|
|
console.log("ln-slider.keys: ???");
|
|
},
|
|
displayValue: function(){
|
|
if (this.values)
|
|
{
|
|
return this.values[this.value];
|
|
}
|
|
return this.mapFromRange(this.value);
|
|
},
|
|
sliderValue: {
|
|
get: function(){ return this.mapToRange(this.value); },
|
|
set: function(v){ this.value = this.mapFromRange(v); this.$emit('change', this.value); },
|
|
}
|
|
},
|
|
methods: {
|
|
mapToRange: function(v){
|
|
if (this.values)
|
|
{
|
|
if (this.values)
|
|
{
|
|
for (let idx=0;idx<this.keys.length;idx++)
|
|
{
|
|
if (v == this.keys[idx])
|
|
{
|
|
return idx;
|
|
}
|
|
}
|
|
}
|
|
console.log("slider position not found!");
|
|
console.log(this.values);
|
|
console.log(v);
|
|
return 0;
|
|
}
|
|
return this.islogarithmic ? null : v;
|
|
},
|
|
mapFromRange: function(v){
|
|
if (this.values)
|
|
{
|
|
return this.keys[v];
|
|
}
|
|
return this.islogarithmic ? null : v;
|
|
},
|
|
fireChange: function(){
|
|
this.$emit('change',this.value);
|
|
}
|
|
},
|
|
template: `
|
|
<div class='ln-slider'>
|
|
<input
|
|
class="ln-slider"
|
|
type="range"
|
|
v-bind:min="min"
|
|
v-bind:max="max"
|
|
v-bind:step="step"
|
|
v-model="sliderValue"
|
|
@change="fireChange"
|
|
>{{ displayValue }}
|
|
</div>`,
|
|
|
|
});
|
|
|
|
Vue.component("ln-file",{
|
|
props: {
|
|
file: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
remove: {
|
|
type: Function,
|
|
required: false,
|
|
}
|
|
},
|
|
methods: {
|
|
},
|
|
template: `<div
|
|
class="ln-upload-file"
|
|
>{{ file.name }} ({{ file.size }}bytes)
|
|
<sym
|
|
@click="remove && remove(file)"
|
|
class="trash"
|
|
style="position: absolute; right: 2px;"
|
|
></sym>
|
|
</div>`
|
|
});
|
|
|
|
Vue.component('ln-upload',{
|
|
model: {
|
|
prop: "files",
|
|
event: "change",
|
|
},
|
|
props: {
|
|
maxSize: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
files: {
|
|
type: Array,
|
|
default: [],
|
|
}
|
|
},
|
|
data: function(){ return { maxSize: this.maxSize, files: this.files, }; },
|
|
methods: {
|
|
drop: function(dropEvent){
|
|
dropEvent.preventDefault();
|
|
|
|
for (let n=0;n<dropEvent.dataTransfer.files.length;n++)
|
|
{
|
|
//let file = dropEvent.dataTransfer.files[n];
|
|
let item = dropEvent.dataTransfer.items[n];
|
|
let file = item.getAsFile();
|
|
|
|
this.files.push({
|
|
name: file.name,
|
|
size: file.size,
|
|
type: file.type,
|
|
file: file,
|
|
});
|
|
}
|
|
|
|
this.$emit("change",this.files);
|
|
console.log("files dropped...");
|
|
},
|
|
remove(file){
|
|
console.log(file);
|
|
let idx = this.files.indexOf(file);
|
|
if (idx != -1)
|
|
{
|
|
this.files.splice(idx,1);
|
|
this.$emit("change",this.files);
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<div
|
|
class="ln-upload"
|
|
@drop="drop"
|
|
ondragover="event.preventDefault();"
|
|
>Dateien hierher ziehen oder auswählen:<br>
|
|
<input type="file" value=""><br>
|
|
<ln-file
|
|
v-for="file in files"
|
|
v-bind:file="file"
|
|
v-bind:remove="remove"
|
|
></ln-file>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('ln-select',{
|
|
model: {
|
|
prop: "value",
|
|
event: "change",
|
|
},
|
|
props: {
|
|
items: {
|
|
required: true,
|
|
},
|
|
value: {
|
|
required: true,
|
|
},
|
|
empty: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
render: {
|
|
type: Function,
|
|
default: function(value){
|
|
return value;
|
|
}
|
|
},
|
|
key: {
|
|
type: Function,
|
|
default: function(key,value){
|
|
return key;
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
prepared: {
|
|
get: function(){
|
|
let prepared = {};
|
|
LN.$each(this.items,(element,index) => {
|
|
prepared[index] = element;
|
|
});
|
|
return prepared;
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
changed: function(ev){
|
|
this.$emit("change", ev.target.value );
|
|
},
|
|
},
|
|
|
|
template: `<div class="ln-select">
|
|
<select @change="changed">
|
|
<option
|
|
v-if="empty"
|
|
value=""
|
|
></option>
|
|
<option
|
|
v-for="(item,_key) in prepared"
|
|
v-bind:value="key(_key,item)"
|
|
:selected="_key == value"
|
|
>{{ render(item) }}</option>
|
|
</select>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('ln-identity',{
|
|
data: function(){
|
|
return {
|
|
popupVisible: false,
|
|
};
|
|
},
|
|
methods: {
|
|
logout(){
|
|
LNVue.$_.authenticate("",null,"","");
|
|
this.popupVisible = false;
|
|
},
|
|
popup(){
|
|
this.popupVisible = !this.popupVisible;
|
|
},
|
|
},
|
|
template: `<div class="ln-identity">
|
|
<div
|
|
v-if="LNVue.$_.identity.UniqueID"
|
|
v-tooltip="LNVue.$_.identity.UniqueID"
|
|
>
|
|
{{ LNVue.$_.identity.IdentityName }}
|
|
</div>
|
|
<div
|
|
v-if="!LNVue.$_.identity.UniqueID"
|
|
@click="popup()"
|
|
>Not logged in
|
|
</div>
|
|
<button
|
|
class="no-border fa-solid"
|
|
@click="popup();"
|
|
></button>
|
|
<div
|
|
class="popup"
|
|
v-if="popupVisible"
|
|
>
|
|
<div
|
|
v-if="LNVue.$_.identity.UniqueID"
|
|
><button
|
|
@click="logout()"
|
|
>Logout</button>
|
|
</div>
|
|
<div
|
|
v-if="!LNVue.$_.identity.UniqueID"
|
|
is="ln-login-pane"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>`,
|
|
});
|
|
|
|
Vue.component('ln-popup',{
|
|
props: {
|
|
title: String,
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
methods: {
|
|
show: function(){
|
|
this.visible = true;
|
|
},
|
|
hide: function(){
|
|
this.visible = false;
|
|
}
|
|
},
|
|
template: `<div class="ln-popup">
|
|
<div
|
|
class="ln-popup-frame"
|
|
><div
|
|
class="ln-popup-title"
|
|
v-if="title"
|
|
>{{ title }}</div>
|
|
<div class="ln-popup-content">
|
|
<slot></slot>
|
|
</div>
|
|
<div
|
|
class="ln-popup-footer"
|
|
>
|
|
<button
|
|
@click="hide()"
|
|
>schliessen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
});
|
|
|
|
Vue.component('ln-login-pane',{
|
|
props: {
|
|
},
|
|
methods: {
|
|
stage1: function(){
|
|
console.log("login stage1", this.identityName);
|
|
|
|
LNVue.$_
|
|
.requestChallenges(this.identityName)
|
|
.then((challenges)=>{
|
|
challenges = challenges.message.Challenges;
|
|
if (challenges.length > 0){
|
|
LNVue.$_.identity.IdentityName = this.identityName;
|
|
LNVue.$_.identity.challenges = challenges;
|
|
}
|
|
});
|
|
},
|
|
logout(){
|
|
LNVue.$_.authenticate("",null,"","");
|
|
},
|
|
authenticateSeededPassword(challenge){
|
|
let encoder = new TextEncoder();
|
|
let seed = LNVue.decodeHex(challenge.AuthenticationParameters);
|
|
let password = encoder.encode(challenge.prove);
|
|
|
|
let secretSource = ArrayBuffer.combine(seed, password, seed);
|
|
|
|
crypto.subtle.digest("SHA-256",secretSource)
|
|
.then((secret)=>{
|
|
let challengebytes = LN.Base64.decode(challenge.Challenge);
|
|
secret = new Uint8Array(secret);
|
|
|
|
let proveSource = ArrayBuffer.combine(challengebytes, secret, challengebytes);
|
|
|
|
crypto.subtle.digest("SHA-256",proveSource)
|
|
.then((prove)=>{
|
|
prove = LN.Base64.encode(new Uint8Array(prove));
|
|
LNVue.$_.authenticate(this.identityName,challenge.SecureAttributeID,challenge.Challenge,prove);
|
|
});
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
data: ()=>{
|
|
return {
|
|
identityName: "",
|
|
};
|
|
},
|
|
template: `<div>
|
|
<div
|
|
v-if="LNVue.$_.identity.UniqueID"
|
|
>
|
|
<div>Logged in as</div>
|
|
{{ LNVue.$_.identity.IdentityName }}
|
|
<button @click="logout()">Logout</button>
|
|
</div>
|
|
<div
|
|
v-if="!LNVue.$_.identity.UniqueID">
|
|
<div
|
|
v-if="LNVue.$_.identity.IdentityName == ''"
|
|
><form @submit.prevent="stage1();">
|
|
<label for="username">Username</label>
|
|
<ln-textfield
|
|
name="username"
|
|
autocomplete="username"
|
|
v-model="identityName"></ln-textfield>
|
|
<button
|
|
:disabled="identityName == ''"
|
|
type="submit"
|
|
>continue</button>
|
|
</form>
|
|
</div>
|
|
<div
|
|
v-if="LNVue.$_.identity.IdentityName != ''"
|
|
>
|
|
<div
|
|
v-for="challenge in LNVue.$_.identity.challenges"
|
|
>
|
|
<form @submit.prevent="authenticateSeededPassword(challenge); return false;">
|
|
<input style="display: none;" type="text" name="username" autocomplete="username" :value="LNVue.$_.identity.IdentityName">
|
|
<div
|
|
v-if="challenge.SecureAttributeTypeName == 'SeededPassword'"
|
|
>{{ challenge.SecureAttributeLabel }}
|
|
<ln-password
|
|
v-model="challenge.prove"
|
|
></ln-password>
|
|
<button
|
|
type="submit"
|
|
:disabled="challenge.prove==''"
|
|
>login</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`,
|
|
});
|
|
|
|
|
|
})(); |