Compare commits
10 Commits
37401a85c8
...
c555f5de28
| Author | SHA1 | Date |
|---|---|---|
|
|
c555f5de28 | |
|
|
1e9bdb6910 | |
|
|
757fdf1674 | |
|
|
e32f22a387 | |
|
|
cfc41309ec | |
|
|
7b3e8498c0 | |
|
|
7be76dd2f9 | |
|
|
8f383240ce | |
|
|
4b59deb916 | |
|
|
1749249c1c |
|
|
@ -0,0 +1,20 @@
|
|||
# Prueba técnica de CGI
|
||||
|
||||
## Detalles de implementación
|
||||
|
||||
Se ha usado 100% VanillaJS, con una estructura similar a React mediante Web Components
|
||||
|
||||
## Start
|
||||
|
||||
Se puede usar cualquier web server para servir el contenido, no hay que compilar ni transpilar.
|
||||
|
||||
El script `start.sh` inicia mediante `npx` un servidor web simple en el directorio actual.
|
||||
|
||||
El contenido será servido en [http://localhost:8080](http://localhost:8080) por defecto.
|
||||
|
||||
## Tests
|
||||
|
||||
Se ha creado una test suite básica en VanillaJS
|
||||
|
||||
Para ejecutar los tests, entrar en [http://localhost:8080/test](http://localhost:8080/test) con el servidor en marcha.
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Front-end
|
||||
|
||||
Se requiere maquetar (html y estilos) y desarrollar (javascript) una
|
||||
aplicación para gestionar una lista de cadenas de texto.
|
||||
Puedes encontrar el diseño en el siguiente enlace:
|
||||
[https://xd.adobe.com/view/ea696dd0-8781-4460-8720-36deb2d19b2a-bf3a/](https://xd.adobe.com/view/ea696dd0-8781-4460-8720-36deb2d19b2a-bf3a/)
|
||||
|
||||
## Especificación Funcional
|
||||
|
||||
La aplicación debe tener una interfaz de usuario que cuente, al menos,
|
||||
con los siguientes elementos:
|
||||
|
||||
* Un contenedor donde se irán añadiendo o borrando cadenas de
|
||||
texto.
|
||||
|
||||
* Una caja de entrada de texto, donde el usuario pueda escribir los
|
||||
textos que desee añadir a la lista.
|
||||
|
||||
* Un botón para agregar nuevas entradas.
|
||||
|
||||
* Un botón para eliminar de la lista.
|
||||
|
||||
La aplicación debe:
|
||||
|
||||
* Añadir entradas de texto, permitir al usuario escribir y añadir la
|
||||
entrada de texto, a un listado. No se pueden añadir entradas
|
||||
vacías.
|
||||
|
||||
* Eliminar un elemento de la lista (los ítems de la lista deben ser
|
||||
seleccionables por el usuario). No se pueden eliminar elementos
|
||||
de la lista sin haber seleccionado uno o varios de los elementos de
|
||||
la lista. No se requiere poder seleccionar múltiples items para
|
||||
poder borrarlos a la vez, pero se tendrá en cuenta. Es deseable,
|
||||
pero no requerido, que el usuario pueda eliminar elementos de la
|
||||
lista haciendo doble click sobre el ítem que se desea eliminar.
|
||||
|
||||
* Es deseable, pero no requerido, permitir al usuario deshacer, como
|
||||
mínimo, el último cambio realizado. Para ello se deberá incluir un
|
||||
botón de deshacer.
|
||||
|
||||
## Especificación Técnica.
|
||||
|
||||
Es deseable realizar una prueba en Vanilla JS y otra utilizando React. Si
|
||||
se encuentran dificultades para realizar el desarrollo en Vanilla JS, se
|
||||
puede entregar solo la prueba con React.
|
||||
18
index.html
18
index.html
|
|
@ -8,23 +8,7 @@
|
|||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="src/components/app-base.js" defer></script>
|
||||
<script type="module" src="src/components/main-window.js" defer></script>
|
||||
<script type="module" src="src/components/custom-button.js" defer></script>
|
||||
<script type="module" src="src/components/item-list.js" defer></script>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: transparent linear-gradient(135deg, #A1C4FD 0%, #C2E9FB 100%) 0% 0% no-repeat padding-box;
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="src/app.js" defer></script>
|
||||
<app-base></app-base>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import './components/main-window.js';
|
||||
import './components/custom-button.js';
|
||||
import './components/item-list.js';
|
||||
import './components/custom-item.js';
|
||||
import './components/custom-modal.js';
|
||||
import './components/custom-text.js';
|
||||
|
||||
import Store from './modules/store.js';
|
||||
import text from './modules/text.js';
|
||||
|
||||
class AppBase extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.text = text;
|
||||
this.properties = {
|
||||
color: '#324BFF',
|
||||
};
|
||||
this.store = new Store();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.store.subscribe(this.render.bind(this));
|
||||
this.render();
|
||||
}
|
||||
|
||||
render(action = 'INIT') {
|
||||
if (![Store.actions.OPEN_MODAL, Store.actions.CLOSE_MODAL, Store.actions.ADD_LINE, Store.actions.REMOVE_LINE, Store.actions.UNDO, 'INIT'].includes(action)) {
|
||||
return; // Only render if content changed
|
||||
}
|
||||
|
||||
this.innerHTML = `
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: transparent linear-gradient(135deg, #A1C4FD 0%, #C2E9FB 100%) 0% 0% no-repeat padding-box;
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
#title {
|
||||
height: 49px;
|
||||
margin-bottom: -12px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
font-weight: inherit;
|
||||
color: #333333;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
#description {
|
||||
height: 74px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #333333;
|
||||
opacity: 1;
|
||||
}
|
||||
.button-bar {
|
||||
display: flex;
|
||||
column-gap: 35px
|
||||
}
|
||||
.button-bar > .right-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
#undo-char {
|
||||
margin-top: 3px;
|
||||
}
|
||||
</style>
|
||||
<main-window width="900px" gap="35px">
|
||||
<h1 id="title">${this.text.title}</h1>
|
||||
<p id="description">${this.text.description}</p>
|
||||
<select id="list" multiple is="item-list" action="${Store.actions.SELECT}" height="227px">
|
||||
${this.store.getRecords().map((item) => `<option is="custom-item" color="${this.properties.color}" value=${item.id}>${item.text}</option>`)}
|
||||
</select>
|
||||
<span class="button-bar">
|
||||
<button is="custom-button" action="${Store.actions.UNDO}" outline width="81px" color="${this.properties.color}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="undo-char" width="21" height="19" viewBox="0 0 5.556 5.027">
|
||||
<path d="M.681 2.22a2.38 2.38 0 0 1 4.742.294v0a2.38 2.38 0 0 1-4.526 1.03" style="fill:none;stroke:${this.properties.color};stroke-width:.264583;stroke-linecap:round;stroke-dasharray:none"/>
|
||||
<path d="m.132 1.525.437.964.964-.437" style="fill:none;stroke:${this.properties.color};stroke-width:.264583;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button is="custom-button" action="${Store.actions.REMOVE_LINE}" outline width="124px" color="${this.properties.color}">Delete</button>
|
||||
<button is="custom-button" class="right-button" action="${Store.actions.OPEN_MODAL}" width="138px" color="${this.properties.color}">Add</button>
|
||||
</span>
|
||||
</main-window>
|
||||
<custom-modal active="${this.store.state.modalActive}">
|
||||
<p>${text.modal.description}</p>
|
||||
<input is="custom-text" placeholder="${this.text.modal.placeholder}"/>
|
||||
<span class="button-bar">
|
||||
<button is="custom-button" class="right-button" action="${Store.actions.ADD_LINE}" width="138px" color="${this.properties.color}">Add</button>
|
||||
<button is="custom-button" action="${Store.actions.CLOSE_MODAL}" outline width="124px" color="${this.properties.color}">Cancel</button>
|
||||
</span>
|
||||
</custom-modal>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-base', AppBase);
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import Store from '../modules/store.js';
|
||||
import text from '../modules/text.js';
|
||||
|
||||
class AppBase extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.text = text;
|
||||
this.store = new Store();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.store.subscribe(this.render.bind(this));
|
||||
this.render();
|
||||
}
|
||||
|
||||
renderText() {
|
||||
const textList = this.store.getRecords();
|
||||
return textList.map((item) => `<option value=${item.id}>${item.text}</option>`).join('\n');
|
||||
}
|
||||
|
||||
render(action = 'INITIAL') {
|
||||
if (![Store.actions.ADD_LINE, Store.actions.REMOVE_LINE, Store.actions.UNDO, 'INITIAL'].includes(action)) {
|
||||
return; // Only render if content changed
|
||||
}
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<main-window title="${this.text.title}" description="${this.text.description}">
|
||||
<select multiple is="item-list">
|
||||
${this.renderText()}
|
||||
</select>
|
||||
<span id="button-bar">
|
||||
<custom-button action="${Store.actions.REMOVE_LINE}" color="#324bff">Delete</custom-button>
|
||||
<custom-button action="${Store.actions.ADD_LINE}" color="#324bff">Add</custom-button>
|
||||
<custom-button action="${Store.actions.UNDO}" outline color="#324bff">Undo</custom-button>
|
||||
</span>
|
||||
</main-window>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-base', AppBase);
|
||||
|
||||
|
|
@ -1,58 +1,66 @@
|
|||
import Store from '../modules/store.js';
|
||||
|
||||
class CustomButton extends HTMLElement {
|
||||
static observedAttributes = ['outline', 'color', 'action'];
|
||||
class CustomButton extends HTMLButtonElement {
|
||||
static observedAttributes = ['outline', 'color', 'width'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.publish = Store.publish;
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
this.publish(this.getAttribute('action'));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.shadowRoot.getElementById('button').addEventListener('click', this.onClick.bind(this));
|
||||
this.updateStyle();
|
||||
this.addEventListener('click', this.onClick.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.shadowRoot.getElementById('button').removeEventListener('click', this.onClick);
|
||||
this.removeEventListener('click', this.onClick);
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
let isOutline;
|
||||
let color;
|
||||
|
||||
switch (name) {
|
||||
case 'outline':
|
||||
isOutline = (newValue !== null && newValue !== 'false')
|
||||
color = this.getAttribute('color');
|
||||
this.style.backgroundColor = isOutline ? 'white' : color;
|
||||
this.style.color = isOutline ? color : 'white';
|
||||
break;
|
||||
case 'color':
|
||||
isOutline = this.getAttribute('outline') !== null && this.getAttribute('outline') !== 'false';
|
||||
this.style.backgroundColor = isOutline ? 'white' : newValue;
|
||||
this.style.color = isOutline ? newValue : 'white';
|
||||
this.style.borderColor = newValue;
|
||||
break;
|
||||
case 'width':
|
||||
this.style.width = newValue;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled attribute change: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isOutline = this.getAttribute('outline') !== null && this.getAttribute('outline') !== 'false';
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
button {
|
||||
border-radius: 50px;
|
||||
border-width: 1px;
|
||||
border-style: ${isOutline ? 'solid' : 'hidden'};
|
||||
border-color: ${this.getAttribute('color')};
|
||||
opacity: 1;
|
||||
width: 138px;
|
||||
height: 49px;
|
||||
background-color: ${isOutline ? 'white' : this.getAttribute('color')};
|
||||
color: ${isOutline ? this.getAttribute('color') : 'white'};
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: normal
|
||||
letter-spacing: 0px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<button id="button">
|
||||
<slot></slot>
|
||||
</button>
|
||||
`;
|
||||
updateStyle() {
|
||||
this.style.borderRadius = '50px';
|
||||
this.style.borderWidth = '1px';
|
||||
this.style.borderStyle = 'solid';
|
||||
this.style.opacity = '1';
|
||||
this.style.textAlign = 'center';
|
||||
this.style.fontSize = '16px';
|
||||
this.style.fontFamily = '"Montserrat", sans-serif';
|
||||
this.style.fontWeight = 'normal';
|
||||
this.style.letterSpacing = '0px';
|
||||
this.style.textTransform = 'uppercase';
|
||||
this.style.height = '49px';
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.publish(this.getAttribute('action'));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('custom-button', CustomButton);
|
||||
customElements.define('custom-button', CustomButton, { extends: 'button' });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import Store from '../modules/store.js';
|
||||
|
||||
class CustomItem extends HTMLOptionElement {
|
||||
static observedAttributes = ['color'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.publish = Store.publish;
|
||||
|
||||
this.checkedStyle = document.createElement("style");
|
||||
this.appendChild(this.checkedStyle);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateStyle();
|
||||
this.addEventListener('dblclick', this.onDoubleClick.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('dblclick', this.onDoubleClick);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
switch (name) {
|
||||
case 'color':
|
||||
this.checkedStyle.innerHTML = `
|
||||
option:checked {
|
||||
background: ${newValue} linear-gradient(0deg, ${newValue} 0%, ${newValue} 100%);
|
||||
box-shadow: 0 0 10px 100px ${newValue} inset;
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled attribute change: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
this.style.height = '40px';
|
||||
this.style.alignContent = 'center';
|
||||
this.style.paddingLeft = '15px';
|
||||
this.style.fontSize = '18px';
|
||||
}
|
||||
|
||||
onDoubleClick() {
|
||||
console.log('double click detected');
|
||||
this.publish(Store.actions.REMOVE_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('custom-item', CustomItem, { extends: 'option' });
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
class CustomModal extends HTMLElement {
|
||||
static observedAttributes = ['active'];
|
||||
|
||||
#templateHtml = `
|
||||
<div class="overlay">
|
||||
<main-window width="700px" gap="25px">
|
||||
<slot></slot>
|
||||
</main-window>
|
||||
</div>
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = this.#templateHtml;
|
||||
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
|
||||
this.overlay = this.shadowRoot.querySelector('div.overlay');
|
||||
this.mainWindow = this.shadowRoot.querySelector('main-window');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
switch (name) {
|
||||
case 'active':
|
||||
this.overlay.style.visibility = newValue === "true" ? 'visible' : 'hidden';
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled attribute change: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
this.overlay.style.position = 'fixed';
|
||||
this.overlay.style.margin = '0';
|
||||
this.overlay.style.top = '0';
|
||||
this.overlay.style.left = '0';
|
||||
this.overlay.style.right = '0';
|
||||
this.overlay.style.bottom = '0';
|
||||
this.overlay.style.display = 'flex';
|
||||
this.overlay.style.paddingTop = '50px';
|
||||
this.overlay.style.justifyContent = 'center';
|
||||
this.overlay.style.background = '#0000001A 0% 0% no-repeat padding-box';
|
||||
this.overlay.style.opacity = '1';
|
||||
this.overlay.style.transition = 'all 0.35s ease-in';
|
||||
this.mainWindow.style.top = '25px';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('custom-modal', CustomModal);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import Store from '../modules/store.js';
|
||||
|
||||
class CustomText extends HTMLInputElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'text';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.updateStyle();
|
||||
this.addEventListener('input', this.onInput.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('input', this.onInput);
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
this.style.height = '60px';
|
||||
this.style.fontSize = '18px';
|
||||
this.style.fontFamily = 'inherit';
|
||||
this.style.paddingLeft = '21px';
|
||||
}
|
||||
|
||||
onInput() {
|
||||
Store.publish(Store.actions.INPUT_TEXT, this.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('custom-text', CustomText, { extends: 'input' });
|
||||
|
|
@ -1,35 +1,47 @@
|
|||
import Store from '../modules/store.js';
|
||||
|
||||
class ItemList extends HTMLSelectElement {
|
||||
static observedAttributes = ['height'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.publish = Store.publish
|
||||
this.publish = Store.publish;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.addEventListener('click', this.onChange.bind(this));
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const selectedIds = Array.from(this.selectedOptions).map((option) => +option.value);
|
||||
this.publish(Store.actions.SELECT, selectedIds);
|
||||
this.addEventListener('click', this.onChange.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.removeEventListener('click', this.onChange.bind(this));
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
switch (name) {
|
||||
case 'height':
|
||||
this.style.height = newValue;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled attribute change: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
updateStyle() {
|
||||
this.style.width = '800px';
|
||||
this.style.height = '227px';
|
||||
this.style.background = '#F7F7F7 0% 0% no-repeat padding-box';
|
||||
this.style.border = '1px solid #CCCCCC';
|
||||
this.style.opacity = '1';
|
||||
this.style.fontSize = '18px';
|
||||
this.style.fontFamily = '"Montserrat" sans-serif';
|
||||
this.style.fontWeight = 'normal';
|
||||
this.style.letterSpacing = '0px';
|
||||
this.style.width = '100%';
|
||||
this.style.background = '#F7F7F7 0% 0% no-repeat padding-box';
|
||||
this.style.border = '1px solid #CCCCCC';
|
||||
this.style.opacity = '1';
|
||||
this.style.fontSize = '18px';
|
||||
this.style.padding = '13px';
|
||||
this.style.boxSizing = 'border-box';
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const selectedIds = Array.from(this.selectedOptions).map((option) => +option.value);
|
||||
this.publish(this.getAttribute('action'), selectedIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +1,50 @@
|
|||
class MainWindow extends HTMLElement {
|
||||
static observedAttributes = ['title', 'description'];
|
||||
static observedAttributes = ['width', 'gap'];
|
||||
|
||||
#templateHtml = `
|
||||
<div class="window">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = this.#templateHtml;
|
||||
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
|
||||
this.window = this.shadowRoot.querySelector('div.window');
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.updateStyle();
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (oldValue === newValue) return;
|
||||
|
||||
switch (name) {
|
||||
case 'width':
|
||||
this.window.style.width = newValue;
|
||||
break;
|
||||
case 'gap':
|
||||
this.window.style.rowGap = newValue;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unhandled attribute change: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const title = this.getAttribute('title') || "No title";
|
||||
const description = this.getAttribute('description') || "No description";
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
#window {
|
||||
width: 900px;
|
||||
height: 577px;
|
||||
background: #FFFFFF 0% 0% no-repeat padding-box;
|
||||
box-shadow: 0px 5px 12px #0000001F;
|
||||
border-radius: 20px;
|
||||
opacity: 1;
|
||||
}
|
||||
#content {
|
||||
width: 800px;
|
||||
margin: 0 50px 13px 50px;
|
||||
}
|
||||
h1 {
|
||||
top: 221px;
|
||||
margin: 50px 50px 13px 50px;
|
||||
height: 49px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
opacity: 1;
|
||||
}
|
||||
p {
|
||||
margin: 0 50px 0 50px;
|
||||
text-align: center;
|
||||
height: 74px;
|
||||
|
||||
/* UI Properties */
|
||||
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
<div id="window">
|
||||
<h1>${title}</h1>
|
||||
<p>${description}</p>
|
||||
<div id="content" >
|
||||
<slot></slot>
|
||||
<div/>
|
||||
</div>
|
||||
`;
|
||||
updateStyle() {
|
||||
this.window.style.boxSizing = 'border-box';
|
||||
this.window.style.padding = '50px';
|
||||
this.window.style.background = '#FFFFFF 0% 0% no-repeat padding-box';
|
||||
this.window.style.boxShadow = '0px 5px 12px #0000001F';
|
||||
this.window.style.borderRadius = '20px';
|
||||
this.window.style.opacity = '1';
|
||||
this.window.style.display = 'flex';
|
||||
this.window.style.flexDirection = 'column';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1 @@
|
|||
import './components/app-base.js';
|
||||
import './components/main-window.js';
|
||||
import './components/custom-button.js';
|
||||
import './components/item-list.js';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,21 @@ export default class Store {
|
|||
REMOVE_LINE: 'REMOVE_LINE',
|
||||
UNDO: 'UNDO',
|
||||
SELECT: 'SELECT',
|
||||
OPEN_MODAL: 'OPEN_MODAL',
|
||||
CLOSE_MODAL: 'CLOSE_MODAL',
|
||||
INPUT_TEXT: 'INPUT_TEXT',
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.state = {
|
||||
records: [],
|
||||
selected: [],
|
||||
input: "",
|
||||
modalActive: false,
|
||||
};
|
||||
this.callbacks = [() => console.log(this.state)];
|
||||
|
||||
Object.values(this.constructor.actions).forEach(action => document.addEventListener(action, this.eventHandler.bind(this), false));
|
||||
Object.values(this.constructor.actions).forEach(action => document.addEventListener(action, this.#eventHandler.bind(this), false));
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
|
|
@ -30,7 +35,7 @@ export default class Store {
|
|||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
eventHandler(event) {
|
||||
#eventHandler(event) {
|
||||
if (!Object.values(this.constructor.actions).includes(event.type)) {
|
||||
console.error(`Action "${event.type}" is not valid`);
|
||||
return;
|
||||
|
|
@ -38,24 +43,30 @@ export default class Store {
|
|||
|
||||
switch (event.type) {
|
||||
case this.constructor.actions.ADD_LINE:
|
||||
if (!this.state.input) break;
|
||||
this.state = {
|
||||
...this.state,
|
||||
records: this.state.records.concat({
|
||||
id: this.state.records.length,
|
||||
text: 'asdf',
|
||||
text: this.state.input,
|
||||
action: 'add',
|
||||
}),
|
||||
input: "",
|
||||
modalActive: false,
|
||||
};
|
||||
break;
|
||||
case this.constructor.actions.REMOVE_LINE:
|
||||
const linesToRemove = this.state.selected.map((id, index) => ({
|
||||
id: this.state.records.length + index,
|
||||
targetId: id,
|
||||
action: 'remove',
|
||||
}));
|
||||
if (this.state.selected.length === 0) {
|
||||
// Nothing to delete
|
||||
break;
|
||||
}
|
||||
this.state = {
|
||||
...this.state,
|
||||
records: [ ...this.state.records, ...linesToRemove],
|
||||
records: [ ...this.state.records, {
|
||||
id: this.state.records.length,
|
||||
targetIds: this.state.selected,
|
||||
action: 'remove',
|
||||
}],
|
||||
selected: [],
|
||||
};
|
||||
break;
|
||||
|
|
@ -71,6 +82,24 @@ export default class Store {
|
|||
selected: event.detail,
|
||||
}
|
||||
break;
|
||||
case this.constructor.actions.OPEN_MODAL:
|
||||
this.state = {
|
||||
...this.state,
|
||||
modalActive: true,
|
||||
}
|
||||
break;
|
||||
case this.constructor.actions.CLOSE_MODAL:
|
||||
this.state = {
|
||||
...this.state,
|
||||
modalActive: false,
|
||||
}
|
||||
break;
|
||||
case this.constructor.actions.INPUT_TEXT:
|
||||
this.state = {
|
||||
...this.state,
|
||||
input: event.detail,
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error(`Action "${event.type}" not implemented in eventHandler`);
|
||||
return;
|
||||
|
|
@ -85,7 +114,11 @@ export default class Store {
|
|||
return acc.concat({id: current.id, text: current.text});
|
||||
}
|
||||
if (current.action === "remove") {
|
||||
return acc.filter((item) => item.id !== current.targetId);
|
||||
let temp = acc;
|
||||
for (const id of current.targetIds) {
|
||||
temp = temp.filter((item) => item.id !== id);
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
console.error(`Action ${current.action} not valid`);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
export default {
|
||||
title: 'This is a technical proof',
|
||||
description: 'Lorem ipsum dolor sit amet consectetur adipiscing, elit mus primis nec inceptos. Lacinia habitasse arcu molestie maecenas cursus quam nunc, hendrerit posuere augue fames dictumst placerat porttitor, dis mi pharetra vestibulum venenatis phasellus.',
|
||||
modal: {
|
||||
description: 'Add item to list',
|
||||
placeholder: 'Type the text here...',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,87 +8,124 @@ export default class StoreTest {
|
|||
test() {
|
||||
(() => {
|
||||
let called = false
|
||||
const store = new Store(() => called = true);
|
||||
const callback = () => called = true;
|
||||
const store = new Store();
|
||||
|
||||
const event = new Event('addLine');
|
||||
store.eventHandler(event);
|
||||
store.subscribe(callback);
|
||||
Store.publish(Store.actions.ADD_LINE);
|
||||
|
||||
this.assert("eventHandler calls the callback after successfull event", called, true);
|
||||
this.assert("Subscribed callback is run after a successfull publish action", called, true);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
store.records = [
|
||||
const store = new Store();
|
||||
store.state = {
|
||||
records: [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
],
|
||||
selected: [0],
|
||||
};
|
||||
|
||||
Store.publish('WRONG');
|
||||
|
||||
const expected = {
|
||||
records: [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
],
|
||||
selected: [0],
|
||||
};
|
||||
|
||||
this.assert("Publish with unknown action does nothing", store.state, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store();
|
||||
|
||||
Store.publish(Store.actions.ADD_LINE);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
];
|
||||
|
||||
this.assert("Publishing action ADD_LINE adds a line to the records", store.state.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store();
|
||||
store.state = {
|
||||
records: [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
],
|
||||
selected: [],
|
||||
};
|
||||
|
||||
Store.publish(Store.actions.REMOVE_LINE);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
];
|
||||
|
||||
this.assert("Publish action REMOVE_LINE without selected in the state does nothing", store.state.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store();
|
||||
store.state = {
|
||||
records: [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
],
|
||||
selected: [0],
|
||||
};
|
||||
|
||||
Store.publish(Store.actions.REMOVE_LINE);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, targetIds: [0], action: 'remove'},
|
||||
];
|
||||
|
||||
this.assert("Publish action REMOVE_LINE with one selected id in the state creates a remove record", store.state.records, expected);
|
||||
this.assert("Selected status is reset after REMOVE_LINE action", store.state.selected, []);
|
||||
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const store = new Store();
|
||||
store.state.records = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
{id: 2, action: 'remove'},
|
||||
]
|
||||
|
||||
const event = new Event('unknown');
|
||||
store.eventHandler(event);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
{id: 2, action: 'remove'},
|
||||
];
|
||||
|
||||
this.assert("eventHandler does nothing on an unknown event type", store.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
|
||||
const event = new Event('addLine');
|
||||
store.eventHandler(event);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
];
|
||||
|
||||
this.assert("eventHandler adds the appropiate record when Event 'addLine' is passed", store.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
store.records = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
]
|
||||
|
||||
const event = new Event('removeLine');
|
||||
store.eventHandler(event);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, action: 'remove'},
|
||||
];
|
||||
|
||||
this.assert("eventHandler adds the appropiate record when Event 'removeLine' is passed", store.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
store.records = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
{id: 2, action: 'remove'},
|
||||
]
|
||||
|
||||
const event = new Event('undo');
|
||||
store.eventHandler(event);
|
||||
Store.publish(Store.actions.UNDO);
|
||||
|
||||
const expected = [
|
||||
{id: 0, text: "asdf", action: 'add'},
|
||||
{id: 1, text: "qwer", action: 'add'},
|
||||
];
|
||||
|
||||
this.assert("eventHandler deletes the appropiate record when Event 'undo' is passed", store.records, expected);
|
||||
this.assert("Publish action UNDO removes last record", store.state.records, expected);
|
||||
|
||||
})();
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
|
||||
store.records = [
|
||||
(() => {
|
||||
const store = new Store();
|
||||
store.state.selected = [0]
|
||||
|
||||
Store.publish(Store.actions.SELECTED, [1, 3]);
|
||||
|
||||
const expected = [1, 3];
|
||||
|
||||
this.assert("Publish action SELECTED updates the array of selected ids", store.state.records, expected);
|
||||
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const store = new Store();
|
||||
|
||||
store.state.records = [
|
||||
{id: 0, text: "Line 1", action: 'add'},
|
||||
{id: 1, text: "Line 2", action: 'add'},
|
||||
{id: 2, text: "Line 3", action: 'add'},
|
||||
|
|
@ -100,17 +137,17 @@ export default class StoreTest {
|
|||
{id: 2, text: "Line 3"},
|
||||
];
|
||||
|
||||
this.assert("getState returns an array of strings with 3 add records", store.getState(), expected);
|
||||
this.assert("getRecords() returns an array of strings with 3 add records", store.getRecords(), expected);
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const store = new Store(() => {});
|
||||
const store = new Store();
|
||||
|
||||
store.records = [
|
||||
store.state.records = [
|
||||
{id: 0, text: "Line 1", action: 'add'},
|
||||
{id: 1, text: "Line 2", action: 'add'},
|
||||
{id: 2, text: "Line 3", action: 'add'},
|
||||
{id: 3, targetId: 1, action: 'remove'},
|
||||
{id: 3, targetIds: [1], action: 'remove'},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
|
|
@ -118,7 +155,34 @@ export default class StoreTest {
|
|||
{id: 2, text: "Line 3"},
|
||||
];
|
||||
|
||||
this.assert("getState returns an array of strings with 3 add records and a delete record", store.getState(), expected);
|
||||
this.assert("getRecords() returns an array of 2 strings with 3 add records and a delete record with 1 id", store.getRecords(), expected);
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const store = new Store();
|
||||
|
||||
store.state.records = [
|
||||
{id: 0, text: "Line 1", action: 'add'},
|
||||
{id: 1, text: "Line 2", action: 'add'},
|
||||
{id: 2, text: "Line 3", action: 'add'},
|
||||
{id: 3, targetIds: [0, 1], action: 'remove'},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{id: 2, text: "Line 3"},
|
||||
];
|
||||
|
||||
this.assert("getRecords() returns an array of 1 string with 3 add records and a delete record with 2 ids", store.getRecords(), expected);
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const store = new Store();
|
||||
|
||||
store.state.selected = [0, 1];
|
||||
|
||||
const expected = [0, 1];
|
||||
|
||||
this.assert("getSelected() returns an array of ids representing the selected items", store.getSelected(), expected);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue