ln.vue/js/lib/ln.vue.components.js

712 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,
},
},
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"
:value="key"
:selected="key == value"
>{{ 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();"
>&#xf107</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>`,
});
})();