1
0
Fork 0
mirror of synced 2024-07-07 15:25:52 +12:00

state management like designs

This commit is contained in:
Martin McKeaveney 2020-01-30 16:22:19 +00:00
parent c064f08d55
commit 61cc39fc96
12 changed files with 241 additions and 531 deletions

View file

@ -0,0 +1,30 @@
<script>
export let disabled = false;
</script>
<style>
.action-button {
color: #0055ff;
background: rgb(54, 133, 249, 0.1);
font-size: 18px;
font-weight: bold;
border-radius: 5px;
border: none;
width: 167px;
height: 64px;
}
.action-button:hover {
cursor: pointer;
}
.action-button:disabled {
color:#163057;
cursor: default;
background: transparent;
}
</style>
<button on:click class="action-button" {disabled}>
<slot />
</button>

View file

@ -0,0 +1,27 @@
<script>
export let value = "";
</script>
<style>
input {
display: block;
font-size: 14px;
font-family: sans-serif;
font-weight: 500;
color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid #ccc;
height: 50px;
}
</style>
<input type="text" on:change bind:value />

View file

@ -36,6 +36,7 @@ $: {
.uk-modal-dialog { .uk-modal-dialog {
border-radius: .3rem; border-radius: .3rem;
width: 60%;
} }
</style> </style>

View file

@ -14,21 +14,22 @@
select { select {
display: block; display: block;
font-size: 16px; font-size: 14px;
font-family: sans-serif; font-family: sans-serif;
font-weight: 700; font-weight: 500;
color: #444; color: #163057;
line-height: 1.3; line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em; padding: 1em 2.6em 0.9em 1.4em;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
border-radius: 0.5em;
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
background-color: #fafafa; background: #fff;
border: 1px solid #ccc;
height: 50px;
} }
.arrow { .arrow {

View file

@ -18,6 +18,7 @@
const onPropChanged = store.setComponentProp; const onPropChanged = store.setComponentProp;
const onStyleChanged = store.setComponentStyle; const onStyleChanged = store.setComponentStyle;
</script> </script>
<div class="root"> <div class="root">

View file

@ -1,88 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import EventSelector from "./EventSelector.svelte";
import {
filter
} from "lodash/fp";
import {EVENT_TYPE_MEMBER_NAME} from "../common/eventHandlers";
export let parentProps;
export let propDef;
export let onValueChanged;
$: events = parentProps[propDef.____name];
const addHandler = () => {
const newHandler = {parameters:{}};
newHandler[EVENT_TYPE_MEMBER_NAME] = "";
events = [...events, newHandler];
onValueChanged(events);
}
const onEventHandlerChanged = (oldEvent) => (newEvent) => {
const indexOfOldEvent = events.indexOf(oldEvent);
const newEvents = [...events];
newEvents.splice(
events.indexOf(oldEvent),
1,
newEvent);
events = newEvents;
onValueChanged(events);
}
const removeHandler = (index) => () => {
events = filter(e => e !== events[index])(events);
onValueChanged(events);
}
</script>
<div class="root">
<div class="control-container">
{#each events as ev, index}
<div class="handler-container">
<EventSelector onChanged={onEventHandlerChanged(ev)}
onRemoved={removeHandler(index)}
event={ev} />
</div>
<div class="separator"></div>
{/each}
<div class="addelement-container"
on:click={addHandler}>
<IconButton icon="plus"
size="12"/>
</div>
</div>
</div>
<style>
.addelement-container {
cursor: pointer;
padding: 3px 0px;
text-align: center;
}
.addelement-container:hover {
background-color: var(--primary25);
margin-top: 5px;
}
.control-container {
padding-left: 3px;
background: var(--secondary10);
}
.separator {
width: 60%;
margin: 10px auto;
border-style:solid;
border-width: 1px 0 0 0;
border-color: var(--primary25);
}
</style>

View file

@ -1,105 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import StateBindingControl from "./StateBindingControl.svelte";
import {
find, map, keys, reduce, keyBy
} from "lodash/fp";
import { pipe, userWithFullAccess } from "../common/core";
import { EVENT_TYPE_MEMBER_NAME, allHandlers } from "../common/eventHandlers";
import { store } from "../builderStore";
export let event;
export let onChanged;
export let onRemoved;
let eventType;
let parameters = [];
$: events = allHandlers(
{hierarchy: $store.hierarchy},
userWithFullAccess({
hierarchy: s.hierarchy,
actions: keyBy("name")($store.actions)
})
);
$: if(event) {
eventType = event[EVENT_TYPE_MEMBER_NAME];
parameters = pipe(event.parameters, [
keys,
map(k => ({name:k, value:event.parameters[k]}))
]);
} else {
eventType = "";
parameters = [];
}
const eventChanged = (type, parameters) => {
const paramsAsObject = reduce(
(obj, p) => {
obj[p.name] = p.value;
return obj;
}
, {}
)(parameters)
const ev = {};
ev[EVENT_TYPE_MEMBER_NAME]=type;
ev.parameters = paramsAsObject;
onChanged(ev);
}
const eventTypeChanged = (ev) => {
const eType = find(e => e.name === ev.target.value)(events);
const emptyParameters = map(p => ({name:p, value:""}))(eType.parameters);
eventChanged(eType.name, emptyParameters);
}
const onParameterChanged = index => val => {
const newparameters = [...parameters];
newparameters[index].value = val;
eventChanged(eventType, newparameters);
}
</script>
<div class="type-selector-container">
<select class="type-selector uk-select uk-form-small " value={eventType} on:change={eventTypeChanged}>
<option></option>
{#each events as ev}
<option value={ev.name}>{ev.name}</option>
{/each}
</select>
<IconButton icon="trash"
size="12"
on:click={onRemoved}/>
</div>
{#if parameters}
{#each parameters as p, index}
<div>
{p.name}
</div>
<StateBindingControl onChanged={onParameterChanged(index)}
value={p.value} />
{/each}
{/if}
<style>
.type-selector-container {
display: flex;
}
.type-selector {
border-color: var(--primary50);
border-radius: 2px;
width: 50px;
flex: 1 0 auto;
}
</style>

View file

@ -1,19 +1,58 @@
<script> <script>
import Modal from "../../common/Modal.svelte"; import Modal from "../../common/Modal.svelte";
import EventSelector from "../EventSelector.svelte";
import HandlerSelector from "./HandlerSelector.svelte"; import HandlerSelector from "./HandlerSelector.svelte";
import IconButton from "../../common/IconButton.svelte"; import IconButton from "../../common/IconButton.svelte";
import ActionButton from "../../common/ActionButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte"; import Select from "../../common/Select.svelte";
import Input from "../../common/Input.svelte";
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers";
export let event; export let event;
export let events; export let events;
export let open; export let open;
export let closeModal; export let onClose;
export let changeEventHandler; export let onPropChanged;
export let removeEventHandler;
export let addEventHandler;
let newEventType = "onClick"; let eventType = "onClick";
let newEventHandler = { parameters: [] };
let eventData = event || { handlers: [] };
const changeEventHandler = (updatedHandler, index) => {
eventData.handlers[index] = updatedHandler;
};
const changeNewEventHandler = updatedHandler => {
newEventHandler = updatedHandler;
};
const deleteEventHandler = index => {
eventData.handlers.splice(index, 1);
eventData = eventData;
};
const createNewEventHandler = handler => {
const newHandler = handler || {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: ""
};
eventData.handlers.push(newHandler);
eventData = eventData;
};
const deleteEvent = () => {
onPropChanged(eventType, []);
eventData = { handlers: [] };
onClose();
};
const saveEventData = () => {
onPropChanged(eventType, eventData.handlers);
eventData = { handlers: [] };
onClose();
};
</script> </script>
<style> <style>
@ -21,75 +60,63 @@
color: var(--primary100); color: var(--primary100);
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
margin-bottom: 0;
} }
h4 { h5 {
font-size: 0.7em; color: rgba(22, 48, 87, 0.6);
} font-size: 16px;
margin-bottom: 5px;
/* TODO: Should be it's own component */
input {
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.7;
padding: 5px 10px;
box-sizing: border-box;
border: 1px solid #dbdbdb;
border-radius: 2px;
outline: none;
}
.event-action {
background: #fafafa;
} }
.event-options { .event-options {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.actions {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
} }
</style> </style>
<Modal bind:isOpen={open} onClosed={closeModal}> <Modal bind:isOpen={open} onClosed={onClose}>
{#if event} <h2>{eventData.name || "Create a New Component Event"}</h2>
<h2>{event.name}</h2> <p>Click here to learn more about component events</p>
{:else}
<h2>Create a New Component Event</h2>
{/if}
<span>Click here to learn more about component events</span>
<div class="event-options"> <div class="event-options">
<div>
<h5>Event Name</h5>
<input type="text" />
</div>
<div> <div>
<h5>Event Type</h5> <h5>Event Type</h5>
<Select bind:value={newEventType}> <Select bind:value={eventType}>
{#each events as [name]} {#each events as event}
<option value={name}>{name}</option> <option value={event.name}>{event.name}</option>
{/each} {/each}
</Select> </Select>
</div> </div>
</div> </div>
<h5>Event Action(s)</h5> <h5>Event Action(s)</h5>
{#if event.handlers} <HandlerSelector
{#each event.handlers as handler, index} newHandler
onChanged={changeNewEventHandler}
onCreate={() => {
createNewEventHandler(newEventHandler)
newEventHandler = { parameters: [] };
}}
handler={newEventHandler} />
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector <HandlerSelector
{index} {index}
onChanged={changeEventHandler} onChanged={changeEventHandler}
onRemoved={() => removeEventHandler(index)} onRemoved={() => deleteEventHandler(index)}
{handler} /> {handler} />
<hr />
{/each} {/each}
{/if} {/if}
<div
class="addelement-container" <div class="actions">
on:click={() => addEventHandler(newEventType)}> <ActionButton on:click={deleteEvent}>Delete</ActionButton>
<IconButton icon="plus" size="12" /> <ActionButton disabled={eventData.handlers.length === 0} on:click={saveEventData}>Save</ActionButton>
Add Handler
</div> </div>
</Modal> </Modal>

View file

@ -31,18 +31,13 @@
let modalOpen = false; let modalOpen = false;
let events = []; let events = [];
let selectedEvent = {}; let selectedEvent = null;
let newEventType = "onClick";
// TODO: only show events that have handlers $: {
events = Object.keys(componentInfo)
$: events = .filter(key => componentInfo[key].length && findType(key) === EVENT_TYPE)
componentInfo && .map(key => ({ name: key, handlers: componentInfo[key] }));
Object.entries(componentInfo).filter( }
([name]) => findType(name) == EVENT_TYPE
);
$: action = selectedEvent ? "Edit" : "Create";
function findType(propName) { function findType(propName) {
if (!componentInfo._component) return; if (!componentInfo._component) return;
@ -56,45 +51,9 @@
}; };
const closeModal = () => { const closeModal = () => {
selectedEvent = {}; selectedEvent = null;
modalOpen = false; modalOpen = false;
}; };
const addEventHandler = eventType => {
const newEventHandler = {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: ""
};
// TODO: improve - just pass the event from props
selectedEvent = {
...selectedEvent,
handlers: [...(selectedEvent.handlers || []), newEventHandler]
};
// events = [...events, newEventHandler];
onPropChanged(newEventType, [...selectedEvent.handlers, newEventHandler]);
};
const changeEventHandler = (updatedHandler, index) => {
// TODO: Improve
const handlers = [...selectedEvent.handlers];
handlers[index] = updatedHandler;
console.log("CHANGED HANDLERS", handlers);
selectedEvent = {
...selectedEvent,
handlers
};
onPropChanged(newEventType, handlers);
};
// TODO: verify
const removeEventHandler = index => {
const handlers = [...selectedEvent.handlers];
handlers.splice(index, 1);
onPropChanged(newEventType, handlers);
};
</script> </script>
<style> <style>
@ -125,8 +84,7 @@
.handler-container { .handler-container {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
grid-gap: 10px; border: 2px solid #f9f9f9;
border: 2px solid #fafafa;
} }
.hierarchy-item { .hierarchy-item {
@ -134,52 +92,66 @@
padding: 11px 7px; padding: 11px 7px;
margin: 5px 0; margin: 5px 0;
border-radius: 5px; border-radius: 5px;
font-size: 1.5em;
width: 100%;
} }
.hierarchy-item:hover { .hierarchy-item:hover {
background: #fafafa; background: #f9f9f9;
}
.event-name {
font-size: 12px;
color: rgba(35, 65, 105, 0.4);
grid-column: 1 / span 2;
}
.event-identifier {
margin-top: 5px;
font-weight: bold;
font-size: 16px;
color: rgba(22, 48, 87, 0.6)
}
.edit-text {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
align-self: end;
justify-self: end;
font-size: 10px;
color: rgba(35, 65, 105, 0.4);
} }
.selected { .selected {
color: var(--button-text); color: var(--button-text);
background: var(--background-button) !important; background: var(--background-button) !important;
} }
.event-name {
}
.handler-identifier {
font-size: 1.5em;
}
</style> </style>
<div class="heading"> <div class="heading">
<h3>Events</h3> <h3>Events</h3>
<PlusButton on:click={() => openModal({})} /> <PlusButton on:click={() => openModal()} />
</div> </div>
<div class="root"> <div class="root">
<form class="uk-form-stacked form-root"> <form class="uk-form-stacked form-root">
{#each events as [name, handlers], index} {#each events as event, index}
{#if handlers.length > 0} {#if event.handlers.length > 0}
<div <div
class="handler-container hierarchy-item {selectedEvent.index === index ? 'selected' : ''}" class="handler-container hierarchy-item {selectedEvent && selectedEvent.index === index ? 'selected' : ''}"
on:click={() => openModal({ name, handlers, index })}> on:click={() => openModal({ ...event, index })}>
<span class="event-name">{name}</span> <span class="event-name">{event.name}</span>
<span class="event-identifier">{event.name}</span>
<span class="edit-text">EDIT</span> <span class="edit-text">EDIT</span>
<span class="handler-identifier">updateState</span>
</div> </div>
{/if} {/if}
{/each} {/each}
</form> </form>
</div> </div>
<EventEditorModal <EventEditorModal
{onPropChanged}
events={events}
open={modalOpen} open={modalOpen}
onClose={closeModal} onClose={closeModal}
event={selectedEvent} event={selectedEvent}
{events} />
{addEventHandler}
{changeEventHandler}
{removeEventHandler} />

View file

@ -1,7 +1,8 @@
<script> <script>
import IconButton from "../../common/IconButton.svelte"; import IconButton from "../../common/IconButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte"; import Select from "../../common/Select.svelte";
import StateBindingControl from "./StateBindingControl.svelte"; import StateBindingControl from "../StateBindingControl.svelte";
import { find, map, keys, reduce, keyBy } from "lodash/fp"; import { find, map, keys, reduce, keyBy } from "lodash/fp";
import { pipe, userWithFullAccess } from "../../common/core"; import { pipe, userWithFullAccess } from "../../common/core";
import { import {
@ -11,9 +12,12 @@
import { store } from "../../builderStore"; import { store } from "../../builderStore";
export let handler; export let handler;
export let onCreate;
export let onChanged; export let onChanged;
export let onRemoved; export let onRemoved;
export let index; export let index;
export let newHandler;
let eventOptions; let eventOptions;
let handlerType; let handlerType;
@ -76,44 +80,60 @@
<style> <style>
.type-selector-container { .type-selector-container {
display: grid; display: flex;
grid-template-columns: repeat(3, 1fr); justify-content: space-between;
grid-gap: 10px; align-items: center;
background: #fafafa; background: rgba(223, 223, 223, 0.5);
padding: 22px; border: 1px solid #dfdfdf;
border: 1px solid var(--primary75); margin-bottom: 18px;
}
.type-selector {
border-color: var(--primary50);
border-radius: 2px;
flex: 1 0 auto;
} }
.handler-option { .handler-option {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.handler-option > div {
margin-bottom: 5px;
}
.new-handler {
background: #fff;
}
.handler-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding: 22px;
}
</style> </style>
<div class="type-selector-container"> <div class="type-selector-container {newHandler && 'new-handler'}">
<div class="handler-option"> <div class="handler-controls">
Action <div class="handler-option">
<Select value={handlerType} on:change={handlerTypeChanged}> Action
<option /> <Select value={handlerType} on:change={handlerTypeChanged}>
{#each eventOptions as option} <option />
<option value={option.name}>{option.name}</option> {#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
{#if parameters}
{#each parameters as param, idx}
<div class="handler-option">
<div>{param.name}</div>
<StateBindingControl
onChanged={onParameterChanged(idx)}
value={param.value} />
</div>
{/each} {/each}
</Select> {/if}
</div> </div>
{#if parameters} {#if newHandler}
{#each parameters as param, idx} <PlusButton on:click={onCreate} />
<div class="handler-option"> {:else}
<div>{param.name}</div> <IconButton icon="x" on:click={onRemoved} />
<StateBindingControl
onChanged={onParameterChanged(idx)}
value={param.value} />
</div>
{/each}
{/if} {/if}
</div> </div>

View file

@ -1,177 +0,0 @@
<script>
import {
isString
} from "lodash/fp";
import IconButton from "../../common/IconButton.svelte";
import {
isBinding, getBinding, setBinding
} from "../../common/binding";
export let value="";
export let onChanged= () => {};
export let type="";
export let options=[];
let isBound=false;
let bindingPath="";
let bindingFallbackValue="";
let bindingSource="store";
let isExpanded = false;
let forceIsBound = false;
let canOnlyBind = false;
$: {
canOnlyBind = type === "state";
if(!forceIsBound && canOnlyBind)
forceIsBound = true;
isBound= forceIsBound || isBinding(value);
if(isBound) {
const binding = getBinding(value);
bindingPath= binding.path;
bindingFallbackValue= binding.fallback;
bindingSource = binding.source || "store";
} else {
bindingPath="";
bindingFallbackValue="";
bindingSource="store";
}
}
const clearBinding = () => {
forceIsBound = false;
onChanged("");
}
const bind = (path, fallback, source) => {
if(!path) {
clearBinding("");
return;
}
const binding = setBinding({path, fallback, source});
onChanged(binding);
}
const setBindingPath = ev => {
forceIsBound = canOnlyBind;
bind(ev.target.value, bindingFallbackValue, bindingSource)
}
const setBindingFallback = ev => {
bind(bindingPath, ev.target.value, bindingSource);
}
const setBindingSource = ev => {
bind(bindingPath, bindingFallbackValue, ev.target.value);
}
// const makeBinding = () => {
// forceIsBound=true;
// isExpanded=true;
// }
</script>
{#if isBound}
<div>
<div class="bound-header">
<div>{isExpanded ? "" : bindingPath}</div>
<IconButton icon={isExpanded ? "chevron-up" : "chevron-down"}
size="12"
on:click={() => isExpanded=!isExpanded}/>
{#if !canOnlyBind}
<IconButton icon="trash"
size="12"
on:click={clearBinding}/>
{/if}
</div>
{#if isExpanded}
<div>
<div class="binding-prop-label">Binding Path</div>
<input class="uk-input uk-form-small"
value={bindingPath}
on:change={setBindingPath} >
<div class="binding-prop-label">Fallback Value</div>
<input class="uk-input uk-form-small"
value={bindingFallbackValue}
on:change={setBindingFallback} >
<div class="binding-prop-label">Binding Source</div>
<select class="uk-select uk-form-small"
value={bindingSource}
on:change={setBindingSource}>
<option>store</option>
<option>context</option>
</select>
</div>
{/if}
</div>
{:else}
<div class="unbound-container">
{#if type === "bool"}
<div>
<IconButton icon={value == true ? "check-square" : "square"}
size="19"
on:click={() => onChanged(!value)} />
</div>
{:else if type === "options"}
<select class="uk-select uk-form-small"
value={value}
on:change={ev => onChanged(ev.target.value)}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else}
<input on:change={ev => onChanged(ev.target.value)}
bind:value={value}
style="flex: 1 0 auto;" />
{/if}
</div>
{/if}
<style>
.unbound-container {
display:flex;
}
.bound-header {
display: flex;
}
.bound-header > div:nth-child(1) {
flex: 1 0 auto;
width: 30px;
color: var(--secondary50);
padding-left: 5px;
}
.binding-prop-label {
color: var(--secondary50);
}
input {
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.7;
padding: 5px 10px;
box-sizing: border-box;
border: 1px solid #DBDBDB;
border-radius: 2px;
outline: none;
}
</style>

View file

@ -1,5 +1,6 @@
<script> <script>
import IconButton from "../common/IconButton.svelte"; import IconButton from "../common/IconButton.svelte";
import Input from "../common/Input.svelte";
import { import {
isBinding, getBinding, setBinding isBinding, getBinding, setBinding
} from "../common/binding"; } from "../common/binding";
@ -81,13 +82,13 @@
{#if isExpanded} {#if isExpanded}
<div> <div>
<div class="binding-prop-label">Binding Path</div> <div class="binding-prop-label">Binding Path</div>
<input class="uk-input uk-form-small" <Input
value={bindingPath} value={bindingPath}
on:change={setBindingPath} > on:change={setBindingPath} />
<div class="binding-prop-label">Fallback Value</div> <div class="binding-prop-label">Fallback Value</div>
<input class="uk-input uk-form-small" <Input
value={bindingFallbackValue} value={bindingFallbackValue}
on:change={setBindingFallback} > on:change={setBindingFallback} />
<div class="binding-prop-label">Binding Source</div> <div class="binding-prop-label">Binding Source</div>
<select class="uk-select uk-form-small" <select class="uk-select uk-form-small"
value={bindingSource} value={bindingSource}
@ -119,7 +120,7 @@
{/each} {/each}
</select> </select>
{:else} {:else}
<input on:change={ev => onChanged(ev.target.value)} <Input on:change={ev => onChanged(ev.target.value)}
bind:value={value} bind:value={value}
style="flex: 1 0 auto;" /> style="flex: 1 0 auto;" />
{/if} {/if}