@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
744 lines (694 loc) • 23.5 kB
JavaScript
import { XPathUtil } from '../xpath-util';
import './fx-log-item.js';
import { FxLogSettings } from './fx-log-settings.js';
export class FxActionLog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.listenTo = [];
this.listeners = [];
}
connectedCallback() {
const style = `
:host {
display:block;
position:relative;
width:100%;
border:thin solid #efefef;
background:transparent;
font-family: Verdana, Sans;
margin:0;
padding:0.25rem;
}
a,a:link,a:visited{
color:black;
}
a{
position:relative;
}
a[alt]:hover::after {
content:attr(alt);
position:absolute;
left:0;
bottom:-0.5em;
border:thin solid;
padding:0.5em;
background:white;
z-index:1;
min-width:5em;
border:thin solid;
max-width:90%;
}
.details{
padding:0.25em 0;
}
.key{
width:20%;
display:inline-block;
min-width:5rem;
border-bottom:1px solid #ddd;
background:#efefef;
vertical-align:top;
}
.value{
display:inline-block;
width:60%;
}
.buttons{
position:absolute;
top:0;
right:0;
}
.buttons button{
padding:0;
}
button{
float:right;
}
button{
border:none;
background:transparent;
width:2.25rem;
height:2.25rem;
cursor:pointer;
}
button#reset{
padding:0;
height:1rem;
}
.info{
padding:0 0.5em;
margin:0.1rem 0;
background:white;
position:relative;
display:grid;
grid-template-areas: "left right"
"bottom .";
grid-template-columns: 75% 25%;
}
.info a{
grid-area:right;
justify-self:end;
}
.details > section{
display:flex;
flex-wrap:wrap;
padding:0.5em 0;
}
fx-log-item{
}
header{
padding:0.5rem;
margin:0;
border-bottom:2px solid #ddd;
}
.info:hover{
outline:3px solid lightblue;
transition:height 0.4s;
}
ol{
background: #efefef;
padding: 0.5em 0 0 2em;
border-left:3px solid red;
}
.event-name{
display:inline-block;
}
#log{
margin-bottom:10em;
margin-right:2em;
}
.log-row{
margin:0;
padding:0;
position:relative;
font-size:0.8em;
border-left:4px solid transparent;
padding-left:5px;
width:calc(100% - 2em);
margin-bottom:0.25em;
}
.log-row summary{
display:flex;
flex-wrap:wrap;
padding:0.5em 0;
cursor:pointer;
}
.log-row summary > span {
width:calc(90% div 3);
}
.log-name{
position:relative;
}
.short-info{
flex:3;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
}
.log-row.no-detail summary{
position:relative;
}
.log-row.no-detail summary{
list-style:none;
padding-left:1rem;
}
.log-row.no-detail summary::-webkit-details-marker {
display: none;
}
.log-row.nested{
margin-left:1em;
}
.nested .event-name{
display:none;
}
.setvalue .value{
background:lightyellow;
}
summary{
padding:1em;
border-bottom:2px solid #ddd;
}
.outer-details{
height:100%;
overflow:auto;
margin-top:2rem;
background:rgba(250, 250, 250, 0.9);
}
.outer-details > header{
position:absolute;
top:-1px;
width:calc(100% - 2rem);
border-bottom:2px solid #ddd;
font-size:1rem;
height:1rem;
}
.outer-details > summary{
font-size:1em;
}
ul{
list-style:none;
padding:0;
margin:0.1em 0;
border-left:3px solid steelblue;
padding:0.1em 0;
}
ul .log-row{
padding-left:3px;
width:calc(100% - 1em);
}
`;
if (localStorage.getItem('fx-action-log-filters')) {
this.listenTo = JSON.parse(localStorage.getItem('fx-action-log-filters'));
} else {
this.listenTo = FxLogSettings.defaultSettings();
}
if (localStorage.getItem('fx-log-settings')) {
this.listenTo = JSON.parse(localStorage.getItem('fx-log-settings'));
} else {
this.listenTo = FxLogSettings.defaultSettings();
}
const html = `
<section open class="outer-details">
<header>Log
<span class="buttons">
<button id="del"" title="empty log - Ctrl+d">
<svg viewBox="0 0 24 24" style="width:24px;height:24px;" preserveAspectRatio="xMidYMid meet" focusable="true"><g><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"></path></g></svg></a>
</button>
</span>
</header>
<div id="log"></div>
</section>
`;
this.shadowRoot.innerHTML = `
<style>
${style}
</style>
${html}
`;
const fore = window.document.querySelector('fx-fore');
if (!fore) {
console.error('fx-fore element not found in this page.');
}
const log = this.shadowRoot.querySelector('#log');
// fore.classList.add('action-log');
this.listenTo.forEach(eventName => {
if (eventName.show) {
document.addEventListener(eventName.name, e => {
this._log(e, log);
});
}
});
// const boxes = this.shadowRoot.querySelector('.boxes');
/*
build the list of checkboxes for the filtering settings
*/
/*
this.listenTo.forEach(item => {
const wrapper = document.createElement('span');
boxes.append(wrapper);
const lbl = document.createElement('label');
lbl.setAttribute('title', item.description);
lbl.setAttribute('for', item.name);
lbl.innerText = item.name;
const cbx = document.createElement('input');
cbx.setAttribute('type', 'checkbox');
cbx.setAttribute('name', item.name);
cbx.setAttribute('id', item.name);
if (item.show) {
cbx.setAttribute('checked', '');
}
wrapper.append(cbx);
wrapper.append(lbl);
cbx.addEventListener('click', e => {
console.log('filter box ticked', e);
if (!e.target.checked) {
// remove event listener
const fore = document.querySelector('fx-fore');
fore.removeEventListener(item.name, this._log);
// e.preventDefault();
// e.stopPropagation();
}
const t = this.listenTo.find(evt => evt.name === item.name);
e.target.checked ? t.show = true : t.show = false;
// console.log('filter', this.listenTo);
localStorage.setItem('fx-action-log-filters', JSON.stringify(this.listenTo));
})
// boxes.appendChild(lbl);
});
*/
document.addEventListener(
'outermost-action-start',
e => {
this.outermost = true;
},
{ capture: true },
);
document.addEventListener(
'outermost-action-end',
e => {
this.outermost = false;
this.outermostAppender = null;
},
{ capture: true },
);
// buttons
const del = this.shadowRoot.querySelector('#del');
del.addEventListener('click', e => {
this.shadowRoot.querySelector('#log').innerHTML = '';
});
document.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === 'd') {
this.shadowRoot.querySelector('#log').innerHTML = '';
}
});
}
_defaultSettings() {
this.listenTo = [
{
name: 'action-performed',
show: false,
description: 'fires after an action has been performed',
},
{ name: 'click', show: false, description: '' },
{
name: 'deleted',
show: false,
description: 'fires after a delete action has been executed',
},
{ name: 'deselect', show: false, description: 'fires when fx-case is deselected' },
{ name: 'dialog-hidden', show: false, description: 'fires after fx-dialog has been hidden' },
{ name: 'dialog-shown', show: false, description: 'fired when a dialog has been shown' },
{ name: 'error', show: false, description: 'fires after an error occurred' },
{ name: 'execute-action', show: true, description: 'fires when an action executes' },
{ name: 'init', show: false, description: 'fires when a control initializes' },
{ name: 'index-changed', show: false, description: 'fires when the repeat index changes' },
{ name: 'insert', show: false, description: 'fires when an fx-insert is executed' },
{
name: 'instance-loaded',
show: false,
description: 'fires after an fx-instance has been loaded',
},
{ name: 'invalid', show: false, description: 'fires after a control became invalid' },
{ name: 'item-changed', show: false, description: 'fires when a repeat item was changed' },
{ name: 'item-created', show: false, description: 'fires when a repeat item was created' },
{ name: 'loaded', show: false, description: 'fires after a fx-load has loaded' },
{ name: 'model-construct', show: false, description: 'fires when a model gets constructed' },
{
name: 'model-construct-done',
show: false,
description: 'fires after model initialization',
},
{
name: 'nonrelevant',
show: false,
description: 'fires after an fx-control became nonrelevant',
},
{ name: 'optional', show: false, description: 'fires after an fx-control became optional' },
{
name: 'outermost-action-end',
show: false,
description: 'fires when an outermost action block is finished',
},
{
name: 'outermost-action-start',
show: false,
description: 'fires when an outermost action block is started',
},
{
name: 'path-mutated',
show: false,
description: 'fires when a path in a repeat has been mutated',
},
{ name: 'readonly', show: false, description: 'fires after an fx-control became readonly' },
{ name: 'readwrite', show: false, description: 'fires after an fx-control became readwrite' },
{
name: 'ready',
show: false,
description: 'fires after a fx-fore page has been completely initialized',
},
{ name: 'rebuild-done', show: false, description: 'fires after a rebuild has taken place' },
{
name: 'recalculate-done',
show: false,
description: 'fires after a recalculate has taken place',
},
{ name: 'refresh-done', show: false, description: 'fires after a refresh has been done' },
{
name: 'relevant',
show: false,
description: 'fires after a fx-control has become relevant',
},
{ name: 'reload', show: false, description: 'fires when a fx-reload action executes' },
{
name: 'required',
show: false,
description: 'fires after an fx-control has become required',
},
{
name: 'return',
show: false,
description: 'fired by embedded Fore controls to return their bound value',
},
{ name: 'select', show: false, description: 'fires when an fx-case has been selected' },
{ name: 'submit', show: false, description: 'fires before a submission takes place' },
{
name: 'submit-done',
show: false,
description: 'fires after a submission has successfully been executed',
},
{
name: 'submit-error',
show: false,
description: 'fires when a submission returned an error',
},
{ name: 'valid', show: false, description: 'fires after a fx-control has become valid' },
{
name: 'value-changed',
show: false,
description: 'fires after a fx-control has changed its value',
},
];
}
_log(e, log) {
const elementName = e.target.nodeName;
if (elementName === 'FX-ACTION-LOG') return;
// e.preventDefault();
// e.stopPropagation();
const row = document.createElement('div');
row.classList.add('log-row');
const logRow = this._logDetails(e);
if (
e.detail &&
Object.keys(e.detail).length === 0 &&
Object.getPrototypeOf(e.detail) === Object.prototype
) {
row.classList.add('no-detail');
}
row.innerHTML = logRow;
if (this.outermost) {
/*
outermost-action-start and outermost-action-end are use as marker events only to start/end a list.
They don't have aditional information to log.
*/
if (e.type === 'outermost-action-start') return; // we don't want this event to actualy log something
if (!this.outermostAppender) {
this.outermostAppender = document.createElement('ul');
log.append(this.outermostAppender);
}
const li = document.createElement('li');
// li.innerHTML = logRow;
li.append(row);
this.outermostAppender.append(li);
} else {
log.append(row);
}
if (this.parentPath && elementName !== 'FX-ACTION') {
row.classList.add('nested');
}
const targetElement = e.target;
row.addEventListener('click', ev => {
// console.log('clicked inspect item', targetElement);
// console.log('clicked inspect item', ev.target.getAttribute('xpath'));
this._highlight(targetElement);
});
const logRowTarget = row.querySelector('.event-target');
if (!logRowTarget) return;
logRowTarget.addEventListener('click', e => {
const alreadyLogged = document.querySelectorAll('.fx-action-log-debug');
alreadyLogged.forEach(logged => {
logged.classList.remove('fx-action-log-debug');
});
targetElement.dispatchEvent(
new CustomEvent('log-action', {
composed: false,
bubbles: true,
cancelable: true,
detail: { target: targetElement },
}),
);
targetElement.classList.add('fx-action-log-debug');
targetElement.setAttribute('data-name', targetElement.nodeName);
this._highlight(targetElement);
});
// log.append(logElements);
}
/**
* logs all configured events.
* Special treatment is given to action-execute event to log out all actions that
* are triggered.
*
* @param e the event to log
* @returns {string}
* @private
*/
_logDetails(e) {
const eventType = e.type;
const path = XPathUtil.getDocPath(e.target);
// console.log('>>>> _logDetails', path);
const cut = path.substring(path.indexOf('/fx-fore'), path.length);
const xpath = `/${cut}`;
const short = cut.replaceAll('fx-', '');
if (this.parentPath && !xpath.startsWith(this.parentPath)) {
this.parentPath = null;
}
switch (eventType) {
case 'deleted':
const { deletedNodes } = e.detail;
const s = new XMLSerializer();
let serialized = '';
deletedNodes.forEach(node => {
serialized += s.serializeToString(node);
});
return `
<fx-log-item event-name="${eventType}"
xpath="${xpath}"
short-info="${e.detail.ref}"
short-name="${e.target.nodeName.toLowerCase()}">
<section class="details">
<header>Details</header>
<section>
<span class="key">Deleted Nodes</span>
<textarea class="value" rows="5">${serialized.trim()}</textarea>
</section>
</section>
</fx-log-item>
`;
break;
case 'outermost-action-start':
return 'start';
break;
case 'outermost-action-end':
return '';
break;
case 'execute-action':
// ##### here actions will be handled
const actionElement = e.detail.action;
return this._renderAction(actionElement, xpath, short, e);
break;
default:
return `
<fx-log-item event-name="${eventType}"
xpath="${xpath}"
short-name="${e.target.nodeName.toLowerCase()}">
<section class="details">
${this._listEventDetails(e)}
</section>
</fx-log-item>
`;
}
// }
}
_renderAction(actionElement, xpath, short, e) {
const stripped = actionElement.nodeName.split('-')[1];
let eventName;
switch (actionElement.nodeName) {
case 'FX-ACTION':
this.parentPath = xpath;
eventName = e.target.currentEvent
? e.target.currentEvent.type
: e.detail.event
? e.detail.event
: '';
return `
<fx-log-item event-name="${eventName}"
xpath="${xpath}"
short-name="ACTION"
data-path="${e.detail.path}"
class="action">
<section class="details">
<header>Attributes</header>
<section>
${Array.from(actionElement.attributes)
.map(
item => `
<span class="key">${item.nodeName}</span>
<span class="value">${item.nodeValue}</span>
`,
)
.join('')}
</section>
</section>
</fx-log-item>
`;
// break;
case 'FX-MESSAGE':
const message = e.detail.action.messageTextContent;
return `
<fx-log-item event-name="${e.detail.event}"
xpath="${xpath}"
short-name="MESSAGE"
short-info="${message}" class="action">
<section class="details">
<span>${message}</span>
</section>
</fx-log-item>
`;
// break;
case 'FX-SEND':
const submission = document.querySelector(`#${e.detail.action.getAttribute('submission')}`);
const event = e.detail.event ? e.detail.event : '';
return `
<fx-log-item short-name="SEND"
short-info="${submission.getAttribute('id')}"
event-name="${event}"
xpath="${xpath}" class="action"
data-path="${e.detail.path}" >
<section class="details">
<header>Submission</header>
<section class="attributes">
${Array.from(submission.attributes)
.map(
item => `
<span class="key">${item.nodeName}</span>
<span class="value">${item.nodeValue}</span>
`,
)
.join('')}
</section>
</section>
</fx-log-item>
`;
// break;
case 'FX-SETVALUE':
const instPath = XPathUtil.getPath(e.target.nodeset);
const listensOn =
e.target.nodeName === 'FX-CONTROL'
? e.target.updateEvent
: e.detail.event
? e.detail.event
: '';
return `
<fx-log-item short-name="SETVALUE"
short-info="${instPath}"
event-name="${listensOn}"
xpath="${xpath}"
data-path="${e.detail.path}" class="action">
<section class="details">
<span class="key">value</span>
<span class="value">${e.detail.value}</span>
</section>
</fx-log-item>
`;
// break;
default:
eventName = e.target.currentEvent
? e.target.currentEvent.type
: e.detail.event
? e.detail.event
: '';
return `
<fx-log-item event-name="${eventName}"
short-name="${e.target.nodeName}"
xpath="${xpath}"
data-path="${e.detail.path}"
class="action">
<section class="details">
</section>
</fx-log-item>
`;
}
}
_listEventDetails(e) {
if (
e.detail &&
Object.keys(e.detail).length === 0 &&
Object.getPrototypeOf(e.detail) === Object.prototype
) {
return '';
}
return `${Object.keys(e.detail).map(item => `<span>${item}</span>`)}`;
}
_listAttributes(e) {
// console.log('_listAttributes', e)
return '';
// return `${e.detail.model.id}`;
/*
if(e.detail &&
Object.keys(e.detail).length === 0 &&
Object.getPrototypeOf(e.detail) === Object.prototype){
return ``;
}else{
return `${e.detail.map((item) => `<span>${item}</span>`)}`;
}
*/
}
_highlight(element) {
const defaultBG = element.style.backgroundColor;
const defaultTransition = element.style.transition;
element.style.transition = 'background 1s';
element.style.backgroundColor = '#FFA500';
setTimeout(() => {
element.style.backgroundColor = defaultBG;
setTimeout(() => {
element.style.transition = defaultTransition;
}, 400);
}, 400);
window.document.dispatchEvent(
new CustomEvent('log-active-element', { detail: { target: element } }),
);
}
}
if (!customElements.get('fx-action-log')) {
customElements.define('fx-action-log', FxActionLog);
}