Skip to content

Instantly share code, notes, and snippets.

@mattduffield
Last active October 24, 2020 16:10
Show Gist options
  • Select an option

  • Save mattduffield/d14741e83eb2116bd9df048ac68d12f5 to your computer and use it in GitHub Desktop.

Select an option

Save mattduffield/d14741e83eb2116bd9df048ac68d12f5 to your computer and use it in GitHub Desktop.
Router
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Dumber Gist</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<base href="/">
</head>
<!--
Dumber Gist uses dumber bundler, the default bundle file
is /dist/entry-bundle.js.
The starting module is pointed to "main" (data-main attribute on script)
which is your src/main.js.
-->
<body>
<my-app></my-app>
<script src="/dist/entry-bundle.js" data-main="main"></script>
</body>
</html>
{
"dependencies": {
"aurelia": "dev"
}
}
/**
* Name: dialog-service.js
* Desc: This uses the native HTML5 dialog element. We can have as many dialog elements
* as necessary.
* WARNING: This element does not allow us to pass objects back as a result.
* We have to come up with a strategy to handle for this restriction.
* Usage:
* <dialog repeat.for="dlg of dlgSvc.dialogs" id="dialog_${$index}" class="dark sansserif">
* <compose view.bind="dlg.view"
* view-model.bind="dlg.viewModel"
* model.bind="dlg.model">
* </compose>
* </dialog>
*
* const options = {
* // view: `${baseUrl}src/dialogs/confirm-delete-dialog.html`,
* viewModel: `${baseUrl}src/dialogs/open-github-dialog.js`,
* isModal: true,
* model: {repos: this.repos}
* };
* const result = await this.dlgSvc.open(options);
* if (result) {
* const data = JSON.parse(result); // NEED TO PARSE THE RESULT
* for (const project of data) {
* await this.asyncInitializeAndMergeProject(project);
* }
* }
*
* References:
* https://alligator.io/html/dialog-element/
* https://keithjgrant.com/posts/2018/01/meet-the-new-dialog-element/
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
*
* https://github.com/GoogleChrome/dialog-polyfill
*/
import {isJson, moveDOM, moveDOMSync, wait} from './util';
export class DialogService {
currentDialogSelector = '';
currentIndex;
dialogs = [];
constructor() {
}
//
// The result from the dialog is always a string!
//
async open(options = {isModal: false}) {
return new Promise(async (resolve, reject) => {
const {isModal} = options;
const eventHandlerOptions = {once: true};
// if (!viewModel) throw new Error('The viewModel property must have a valid entry.');
const index = this.dialogs.length;
options.selector = `#dialog_${index}`;
options.index = index;
this.currentDialogSelector = options.selector;
this.currentIndex = index;
this.dialogs.push(options);
await wait(100);
const dlg = document.querySelector(this.currentDialogSelector);
// dlg.addEventListener('click', event => {
// if (event.target === dlg) {
// this.close();
// }
// }, eventHandlerOptions);
if (isModal) {
dlg.addEventListener('cancel', async (event) => {
if (event.target === dlg) {
event.preventDefault();
} else {
if (options.moveDOMCfg && options.moveDOMCfg.close) {
const {source, target} = options.moveDOMCfg.close;
await moveDOM(source, target);
}
}
}, eventHandlerOptions);
}
dlg.addEventListener('close', async (event) => {
const result = dlg.returnValue;
dlg.returnValue = '';
if (options.moveDOMCfg && options.moveDOMCfg.close) {
const {source, target} = options.moveDOMCfg.close;
await moveDOM(source, target);
}
this.dialogs.pop();
const index = this.dialogs.length - 1;
this.currentIndex = index;
if (index > -1) {
this.currentDialogSelector = this.dialogs[index].selector;
} else {
this.currentDialogSelector = '';
}
// console.debug('dialog-service:close - resolving result', result);
resolve(result);
}, eventHandlerOptions);
if (isModal) {
dlg.showModal();
} else {
dlg.show();
}
await wait(50);
if (options.moveDOMCfg && options.moveDOMCfg.open) {
const {source, target} = options.moveDOMCfg.open;
await moveDOM(source, target);
}
const event = new CustomEvent('ready', {bubbles: true});
dlg.dispatchEvent(event);
});
}
close(result = '') {
const dlg = document.querySelector(this.currentDialogSelector);
let resultString = '';
if (typeof result === 'object') {
resultString = JSON.stringify(result);
} else {
resultString = result;
}
dlg.close(resultString);
// console.debug('dialog-service:close - result', resultString);
}
createCloseResult(output) {
if (output && isJson(output)) {
output = JSON.parse(output);
}
const closeResult = {output, wasCancelled: output ? false : true};
return closeResult;
}
async show(options) {
options.isModal = false;
const output = await this.open(options);
const closeResult = this.createCloseResult(output);
return closeResult;
}
async showModal(options) {
options.isModal = true;
const output = await this.open(options);
const closeResult = this.createCloseResult(output);
return closeResult;
}
}
:host {
min-height: 225px;
}
<use-shadow-dom></use-shadow-dom>
<template display.style="selectedStepIndex === stepIndex && isVisible ? 'block' : 'none !important'" class="m-1 p-1 d-block border border-primary">
<slot></slot>
</template>
import { inject, customElement, bindable, BindingMode, ISignaler, EventAggregator, shadowCSS } from 'aurelia';
let counter = 0;
@inject(ISignaler, EventAggregator)
export class FormWizardItem {
@bindable title = '';
@bindable stepIndex = 0;
@bindable selectedStepIndex = 0;
@bindable isComplete = false;
@bindable disabled = false;
@bindable navClick;
@bindable isVisible = true;
constructor(signaler, messageBus) {
this.signaler = signaler;
this.messageBus = messageBus;
this.id = `form-wizard-item-${counter++}`;
}
}
:host {
position: relative;
height: calc(100% - 10px);
}
.wizard-title-container.visible
{
margin-top: 15px;
margin-bottom: 15px;
padding-bottom: 15px;
/*border-color: #e7eaec;*/
/*border-style: solid solid none;*/
border-bottom: solid 1px #e7eaec;
}
.wizard-title
{
margin-left: 15px;
margin-right: 15px;
font-size: 14px;
font-weight: 600;
}
#wizard-header > span .badge-lg
{
color: var(--primary-offset-color);
background-color: var(--primary-color);
}
#wizard-header > span .title {
font-size: 16px;
font-weight: 300;
}
#wizard-header > span.is-complete .badge-lg
{
color: var(--btn-success-color);
background-color: var(--btn-success-bg-color);
}
#wizard-header > span.active .badge-lg
{
color: var(--primary-text-color);
background-color: var(--primary-color);
}
#wizard-header > span.active .title
{
/* color: var(--wizard-header-active-color); */
}
#wizard-header > span.step:hover:not(.step-disabled)
{
/* background: #f3f3f4; */
}
#wizard-header > span.step:hover:not(.step-disabled) .title
{
/* color: var(--wizard-header-active-color); */
}
#wizard-header > span.step:hover:not(.step-disabled):not(.is-complete) .badge-lg
{
color: var(--btn-primary-color-hover);
background-color: var(--btn-primary-bg-color-hover);
}
#wizard-header > span.step.is-complete:hover:not(.active) .badge-lg
{
color: var(--btn-success-color-hover);
background-color: var(--btn-success-bg-color-hover);
}
#wizard-header > span.step.step-disabled .badge-lg
{
background-color: var(--btn-primary-bg-color-disabled);
border: var(--btn-primary-border-disabled);
}
#wizard-header > span.step.step-disabled:hover
{
cursor: initial;
}
<use-shadow-dom></use-shadow-dom>
<div id="wizard-title"
class="flex-column-none"
display.style="titleVisibility === 'visible' ? 'block' : 'none !important'">
<slot name="title"></slot>
</div>
<div id="wizard-container" class="${containerClass}">
<div id="wizard-header" class="${headerClass}">
<span repeat.for="step of steps & signal:'name-signal'"
click.trigger="navClick($event)"
class="${selectedStepIndex === step.stepIndex ? 'active' : ''} ${step.isComplete ? 'is-complete' : ''} ${step.disabled ? 'step-disabled' : ''} ${step.isVisible ? '' : 'hidden'} step"
disabled.bind="step.disabled"
data-index="${step.stepIndex & signal:'name-signal'}">
<span class="badge-lg pointer-events-none">${step.stepIndex + 1 & signal:'name-signal'}</span>
<span class="margin-left-10 title pointer-events-none"> ${step.title} </span>
</span>
</div>
<div id="wizard-content" class="${contentClass} ${contentCustomClass}" style="min-height: 0;">
<progress if.bind="progressOrientation === 'top'" id="wizard-progress" class="${progressClass} ${progressVisibility === 'visible' ? '' : 'hidden'}" max="${numberSteps & signal:'name-signal'}"
value="${progressValue}">
</progress>
<slot></slot>
<progress if.bind="progressOrientation === 'bottom'" id="wizard-progress" class="${progressClass} ${progressVisibility === 'visible' ? '' : 'hidden'}" max="${numberSteps & signal:'name-signal'}"
value="${progressValue}">
</progress>
</div>
</div>
import { inject, customElement, bindable, BindingMode, children, ISignaler, EventAggregator, shadowCSS } from 'aurelia';
import {FormWizardItem} from './form-wizard-item';
@inject(Element, ISignaler, EventAggregator)
export class FormWizard {
@children({ filter: el => el && el.nodeType === 1 && el.matches('form-wizard-item') }) steps = [];
// @children('form-wizard-item') steps = [];
@bindable titleVisibility = 'hidden';
@bindable orientation = 'top';
@bindable containerClass = '';
@bindable headerClass = '';
@bindable contentClass = '';
@bindable contentCustomClass = '';
@bindable progressVisibility = 'hidden';
@bindable progressOrientation = 'top';
@bindable progressClass = '';
@bindable numberSteps = 1;
@bindable progressValue = 0;
@bindable selectedStepIndex = 0;
@bindable computeStepsTrigger;
@bindable credential;
constructor(element, signaler, messageBus) {
this.element = element;
this.signaler = signaler;
this.messageBus = messageBus;
}
/**
* This function fires whenever the selectedStep property changes.
* It then determines the correct step and calls the firesSelectionChange
* function.
*/
selectedStepIndexChanged(newValue, oldValue) {
this.progressValue = this.selectedStepIndex + 1;
this.fireSelectionChange();
}
/**
* This function fires whenever the orientation property changes.
* It then determines the layout of the wizard.
*/
orientationChanged(newValue, oldValue) {
// console.log('orientationChanged', newValue);
if (newValue === 'top') {
this.containerClass = 'flex-column-1'
this.headerClass = 'flex-row-none order-0 header-top'
this.contentClass = 'flex-row-1 order-1'
} else if (newValue === 'bottom') {
this.containerClass = 'flex-column-1'
this.headerClass = 'flex-row-none order-1'
this.contentClass = 'flex-row-1 order-0'
} else if (newValue === 'left') {
debugger;
this.containerClass = 'grid-cols-2';
// this.containerClass = 'flex-row-1'
// this.headerClass = 'flex-column-none order-0'
// this.contentClass = 'flex-column-1 order-1'
} else if (newValue === 'right') {
this.containerClass = 'flex-row-1'
this.headerClass = 'flex-column-none order-1'
this.contentClass = 'flex-column-1 order-0'
}
}
/**
* This function fires whenever the computeStepsTrigger expression
* changes. It will then filter and compute the steps based on the
* expression.
* @param {*} newValue
*/
async computeStepsTriggerChanged(newValue) {
await wait(150);
this.steps.forEach((step, index) => {
if (!step.isVisible) {
step.stepIndex = -step.stepIndex;
}
});
const visibleSteps = this.steps.filter(s => s.isVisible);
visibleSteps.forEach((step, index) => {
step.stepIndex = index;
});
this.numberSteps = visibleSteps.length;
this.signaler.signal('name-signal');
}
/**
* This function is called when the element is attached to the DOM.
*/
afterAttach() {
this.firstStepSub = this.messageBus.subscribe('wizard:firststep', this.firstStep.bind(this));
this.nextStepSub = this.messageBus.subscribe('wizard:nextstep', this.nextStep.bind(this));
this.gotoStepSub = this.messageBus.subscribe('wizard:gotostep', this.gotoStep.bind(this));
this.prevStepSub = this.messageBus.subscribe('wizard:prevstep', this.prevStep.bind(this));
this.lastStepSub = this.messageBus.subscribe('wizard:laststep', this.lastStep.bind(this));
this.completeStepSub = this.messageBus.subscribe('wizard:complete-step', (payload) => {
const {index} = payload;
this.completeStep(index);
});
this.completeCurrentStepSub = this.messageBus.subscribe('wizard:complete-current-step', () => {
this.completeCurrentStep();
});
this.completeStepAndGoToLastStepSub = this.messageBus.subscribe('wizard:complete-current-step-and-go-to-last', () => {
this.completeStepAndGoToLastStep();
});
this.numberSteps = this.steps.length;
this.steps.forEach((item, index) => {
item.stepIndex = index;
});
this.progressValue = this.selectedStepIndex + 1;
}
/**
* This function is called when the element detached from the DOM.
*/
afterDetach() {
this.firstStepSub.dispose();
this.nextStepSub.dispose();
this.gotoStepSub.dispose();
this.prevStepSub.dispose();
this.lastStepSub.dispose();
this.completeStepSub.dispose();
this.completeCurrentStepSub.dispose();
this.completeStepAndGoToLastStepSub.dispose();
}
/**
* This function is fired when a user clicks on individual
* steps. It then navigates the user to the corresponding
* step.
*/
async navClick(e) {
e.preventDefault();
e.stopPropagation();
debugger;
let canContinue = false;
const index = Number(e.currentTarget.attributes['data-index'].value);
const isMovingForward = index > this.selectedStepIndex;
const currentStep = this.steps[this.selectedStepIndex];
const nextStep = this.steps[index];
if (nextStep.disabled) return;
if (!isMovingForward) {
canContinue = true;
} else if (nextStep.navClick) {
canContinue = await nextStep.navClick();
}
if (canContinue) {
if (isMovingForward) {
currentStep.isComplete = true;
}
this.selectedStepIndex = index;
}
}
/**
* This function fires whenever the seletectStepChange event fires.
* It dispatches events for both changing and changed events.
*/
async fireSelectionChange() {
const selectionChangingEvent = new CustomEvent('wizard-selection-changing', {bubbles: true, detail: this.selectedStepIndex});
this.element.dispatchEvent(selectionChangingEvent);
await wait(25);
this.steps.forEach((item, index) => {
item.selectedStepIndex = this.selectedStepIndex;
});
const selectionChangedEvent = new CustomEvent('wizard-selection-changed', {bubbles: true, detail: this.selectedStepIndex});
this.element.dispatchEvent(selectionChangedEvent);
}
/**
* This function navigates the wizard to the first step.
*/
firstStep() {
this.selectedStepIndex = 0;
}
/**
* This function navigates the wizard to the next step.
*/
nextStep() {
let count = this.steps.length;
if (this.selectedStepIndex < count - 1) {
this.selectedStepIndex++;
}
}
/**
* This function navigates the wizard to the index provided.
*/
gotoStep(payload) {
let count = this.steps.length;
if (payload.index > 0 && payload.index < count) {
this.selectedStepIndex = payload.index;
}
}
/**
* This function navigates the wizard to the previous step.
*/
prevStep() {
let count = this.steps.length;
if (this.selectedStepIndex > 0) {
this.selectedStepIndex--;
}
}
/**
* This function navigates the wizard to the last step.
*/
lastStep() {
this.selectedStepIndex = this.steps.length - 1;
}
completeStep(index) {
this.steps[index].isComplete = true;
}
completeStepAndGoToLastStep() {
this.steps[this.selectedStepIndex].isComplete = true;
this.selectedStepIndex = this.steps.length - 1;
}
completeCurrentStep() {
this.steps[this.selectedStepIndex].isComplete = true;
this.nextStep();
}
}
function wait(t) {
return new Promise(r => setTimeout(r, t));
}
<p>Home Screen</p>
<button
click.trigger="launch($event)">
Launch
</button>
import {inject} from 'aurelia';
import {DialogService} from './dialog-service';
import {NewClientDialog} from './new-client-dialog';
@inject(DialogService)
export class Home {
wizard = null;
constructor(dlgSvc) {
this.dlgSvc = dlgSvc;
}
async launch(e) {
e.preventDefault();
e.stopPropagation();
const model = {
header: 'Are you sure you want to unlock this record?',
prompt: 'This action unlocks the record. The user with the record locked will be navigated to the Dashboard.',
typePrompt: 'unlock',
confirmPrompt: 'I understand the consequences, unlock this record',
confirmation: ''
};
const options = {
subject: NewClientDialog,
model,
isModal: true
};
const closeResult = await this.dlgSvc.showModal(options);
if (closeResult.wasCancelled) return true;
console.log(closeResult.output);
}
}
import Aurelia, {DI, RouterConfiguration, StyleConfiguration} from 'aurelia';
import { MyApp } from './my-app';
import {FormWizard} from './form-wizard';
import {FormWizardItem} from './form-wizard-item';
import shared from './shared.css';
Aurelia
.register(RouterConfiguration)
.register(FormWizard)
.register(FormWizardItem)
.register(StyleConfiguration.shadowDOM({ sharedStyles: [shared] }))
.app(MyApp)
.start();
:root {
--primary-text-color: black;
--primary-color: blue;
--primary-offset-color: white;
}
<!--
Try to create a paired css/scss/sass/less file like my-app.scss.
It will be automatically imported based on convention.
-->
<!--
There is no bundler config you can change in Dumber Gist to
turn on shadow DOM.
But you can turn shadow DOM on by adding a meta tag in every
html template:
<use-shadow-dom>
-->
<use-shadow-dom></use-shadow-dom>
<import from="./home"></import>
<h1>${message}</h1>
<au-viewport name="main">
</au-viewport>
<dialog repeat.for="dlg of dlgSvc.dialogs"
id="dialog_${$index}"
class="drag-window">
<au-compose subject.bind="dlg.subject"></au-compose>
</dialog>
import {inject, Router} from 'aurelia';
import {DialogService} from './dialog-service';
@inject(Router, DialogService)
export class MyApp {
message = 'Hello Aurelia 2!';
dialogs = [];
constructor(router, dlgSvc) {
this.router = router;
this.dlgSvc = dlgSvc;
}
async beforeBind() {
this.router.load('home');
}
}
<form-wizard view-model.ref="wizard"
progress-class="margin-bottom-15"
progress-visibility="visible"
progress-orientation="bottom"
orientation="right"
content-custom-class="margin-20"
compute-steps-trigger.bind="project.type & throttle:500">
<form-wizard-item title="Agency Name" class="flex-row-1">
<div class="flex-column-1">
<div class="flex-column-1">
<div class="form-group">
<label>Agency Name</label>
<input class="form-control" value.bind="agency_name">
</div>
<p>Enter your agency name.</p>
</div>
<div class="flex-row-none justify-content-end">
<div class="flex-row-1 justify-content-start">
</div>
<div class="flex-row-1 justify-content-center">
</div>
<div class="flex-row-1 justify-content-end">
<button class="form-button padding-10"
click.trigger="wizard.nextStep($event)">
Next
</button>
</div>
</div>
</div>
</form-wizard-item>
<form-wizard-item title="Agency Address" class="flex-row-1">
<div class="flex-column-1">
<div class="flex-column-1">
<div class="form-group">
<label>Street</label>
<input class="form-control" value.bind="street">
</div>
<p>Enter your street address.</p>
</div>
<div class="flex-row-none justify-content-end">
<div class="flex-row-1 justify-content-start">
</div>
<div class="flex-row-1 justify-content-center">
</div>
<div class="flex-row-1 justify-content-end">
<button class="form-button transparent padding-10"
click.trigger="wizard.prevStep($event)">
Previous
</button>
<button class="form-button padding-10"
click.trigger="wizard.nextStep($event)">
Next
</button>
</div>
</div>
</div>
</form-wizard-item>
</form-wizard>
import {inject, EventAggregator} from 'aurelia';
import {DialogService} from './dialog-service';
@inject(EventAggregator, DialogService)
export class NewClientDialog {
wizard = null;
constructor(messageBus, dlgSvc) {
this.messageBus = messageBus;
this.dlgSvc = dlgSvc;
this.model = dlgSvc.dialogs[dlgSvc.currentIndex].model;
}
isStepVisible(type) {
return (type !== 'custom' && type !== 'empty' && type !== 'empty_web');
}
firstStep(e) {
e.preventDefault();
e.stopPropagation();
this.messageBus.publish('wizard:firststep', null);
}
nextStep(e) {
e.preventDefault();
e.stopPropagation();
this.messageBus.publish('wizard:nextstep', null);
}
gotoStep(e) {
e.preventDefault();
e.stopPropagation();
this.messageBus.publish('wizard:prevstep', {index: 2});
}
prevStep(e) {
e.preventDefault();
e.stopPropagation();
this.messageBus.publish('wizard:prevstep', null);
}
lastStep(e) {
e.preventDefault();
e.stopPropagation();
this.messageBus.publish('wizard:laststep', null);
}
async asyncCheckProjectName(e) {
this.isBusy = true;
const projectName = this.project.name;
const options = {repo: projectName};
const {exists} = await this.githubCtr.doesRepoExist(options);
if (!exists) {
// Not found; we can proceed.
if (e) {
this.messageBus.publish('wizard:complete-current-step');
}
this.isBusy = false;
return true;
} else {
const message = `A project already exists with the name '${projectName}'! Please enter a unique name.`;
Toast.alert(message);
this.isBusy = false;
return false;
}
}
async asyncSetProjectType(e) {
if (e) {
if (this.project.type.value === 'Custom') {
this.messageBus.publish('wizard:complete-current-step-and-go-to-last');
// this.messageBus.publish('wizard:laststep');
} else {
this.messageBus.publish('wizard:complete-current-step');
}
return true;
}
return this.asyncCheckProjectName();
}
async asyncSetProjectTranspiler(e) {
if (e) {
this.messageBus.publish('wizard:complete-current-step');
}
return true;
}
async asyncSetProjectLoader(e) {
if (e) {
this.messageBus.publish('wizard:complete-current-step');
}
return true;
}
async asyncSetProjectCssPreprocessor(e) {
if (e) {
this.messageBus.publish('wizard:complete-current-step');
}
return true;
}
async asyncCreateProject(e) {
if (e) {
// console.debug('Project options', this.project);
try {
this.isBusy = true;
// Create the project...
this.messageBus.publish('app:is-busy', {isBusy: true});
Toast.success('Creating new project.');
const source = this.getSourceRepo();
const sourceParts = source.split('/');
if (sourceParts.length !== 2) throw new Error('Source must contain both the owner and repo name!');
// 1. Create the repo
const createOptions = {name: this.project.name};
await this.githubCtr.createRepo(createOptions);
// 1a. Enable GitHub Pages, if selected...
// ...
//
// 2. Clone the source repo
const sourceOwner = sourceParts[0];
const sourceRepo = sourceParts[1];
const cloneOptions = {repo: this.project.name, sourceOwner, sourceRepo};
await this.githubCtr.cloneRepo(cloneOptions);
// this.workComplete('Project creation complete!', false, false);
this.messageBus.publish('app:load-project-by-name', {name: this.project.name});
Toast.success(`Project created successfully!`);
Toast.message(`Loading project...`);
} catch (e) {
Toast.alert(`Error encountered creating project! ${e}`);
} finally {
this.messageBus.publish('app:is-busy', {isBusy: false});
this.isBusy = false;
this.dlgSvc.close();
}
}
return true;
}
getSourceRepo() {
let source = this.sourceRepos[this.project.type.value];
if (source) return source;
const p = this.project;
const pattern = `${p.type.value}/${p.transpiler.value}/${p.loader.value}`;
source = this.sourceRepos[pattern];
if (source) return source;
if (this.project.customURL) {
return this.project.customURL;
}
return null;
}
}
.pointer-events-none {
pointer-events: none;
}
.grid {display: grid;}
.grid-cols-1 {grid-template-columns: auto;}
.grid-cols-2 {grid-template-columns: auto auto;}
.grid-cols-3 {grid-template-columns: auto auto auto;}
.grid-rows-1 {grid-template-rows: auto;}
.grid-rows-2 {grid-template-rows: auto auto;}
.grid-rows-3 {grid-template-rows: auto auto auto;}
.badge-lg
{
height: 45px;
width: 45px;
padding: 15px 20px;
border-radius: 50px !important;
font-size: 16px;
background-color: blue;
color: white;
}
progress::-webkit-progress-value
{
transition: width .6s ease;
}
progress[value]
{
-webkit-appearance: none;
appearance: none;
background-color: #f5f5f5;
border-radius: 3px;
/* width: calc(100% - 40px); */
width: calc(100%);
height: 20px;
}
progress[value]::-webkit-progress-bar
{
background-color: #f5f5f5;
border-radius: 3px;
}
progress[value]::-webkit-progress-value
{
/*background-size: 35px 35px, 100% 100%, 100% 100%;*/
border-radius:3px;
/* background-color: #0369B1; */
background-color: var(--primary-color);
}
export const wait = (time = 100) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
});
};
export const wrap = (el, htmlString) => {
let wrapper = document.createElement('div');
wrapper.innerHTML = htmlString;
wrapper = wrapper.firstChild;
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
};
export const after = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString);
export const next = (el) => el.nextElementSibling;
export const hasParentClass = (target, cls) => {
let node = target.parentElement;
while (node != null) {
if (node.classList.contains(cls)) return true;
node = node.parentElement;
}
return false;
};
export const hasClass = (target, cls) => {
if (target.classList.contains(cls)) return true;
return false;
};
export const hasClassOrParentClass = (target, cls) => hasClass(target, cls) || hasParentClass(target, cls);
/**
* This code was taken from the following blog post:
* https://blog.grossman.io/how-to-write-async-await-without-try-catch-blocks-in-javascript/
*/
export const to = (promise) => {
return promise.then(data => {
return [null, data];
}).catch(err => {
return [err];
});
};
export const toDatabaseCase = (input = '') => {
const result = input
.replace('-', '_')
.toLowerCase();
return result;
};
/**
* This function performs a string comparison for sorting arrays.
* If the object has a property isFolder, it uses upper case; otherwise,
* it uses lower case for the comparison. This ensures that folders are
* always at the beggining of the array.
*/
export const nameCompare = (a, b) => {
let nameA = a.name.toLowerCase();
let nameB = b.name.toLowerCase();
if (a.isFolder) {
nameA = `!!!${a.name.toUpperCase()}`;
}
if (b.isFolder) {
nameB = `!!!${b.name.toUpperCase()}`;
}
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
};
/**
* Usage:
* const flat = this.utilSvc.flatten(this.appService.repos);
* console.log('flat', flat);
* const found = flat.find(f => f.id === file.id);
* console.log('found', found);
* const parent = flat.find(f => f.path === file.parentFolder);
* console.log('parent', parent);
* const childFiles = flat.filter(f => f.parentFolder.includes(file.path) && f.isFile);
* console.log('childFiles', childFiles);
*
* @param {*} items
* @param {*} property
* @param {*} filterFunc
*/
export const flatten = (items, property = 'items', filterFunc = (x) => x.name) => {
const def = x => typeof x !== 'undefined';
const isArray = x => Array.isArray(x);
const reduce = ([x, ...xs], f, memo, i = 0) => def(x)
? reduce(xs, f, f(memo, x, i), i + 1) : memo;
const flatten = (xs, property = 'items') => reduce(xs, (memo, x) => x
? isArray(x[property]) ? [...memo, x, ...flatten(x[property])] : [...memo, x] : [], []);
const filter = x => x.filter(filterFunc);
const flat = filter(flatten([{items: items}]));
return flat;
}
/**
* Reference: https://gist.github.com/Integralist/749153aa53fea7168e7e
* @param {*} arr
* @param {*} property
*/
export const unflatten = (arr, property = 'items') => {
let tree = [],
mappedArr = {},
arrElem,
mappedElem;
// First map the nodes of the array to an object -> create a hash table.
for (let i = 0, len = arr.length; i < len; i++) {
arrElem = arr[i];
mappedArr[arrElem.id] = arrElem;
mappedArr[arrElem.id][property] = [];
}
for (let id in mappedArr) {
if (mappedArr.hasOwnProperty(id)) {
mappedElem = mappedArr[id];
// If the element is not at the root level, add it to its parent array of children.
if (mappedElem.parentId) {
mappedArr[mappedElem['parentId']][property].push(mappedElem);
}
// If the element is at the root level, add it to first level elements array.
else {
tree.push(mappedElem);
}
}
}
return tree;
}
/**
* This function takes in a list, function, and a property.
* It iterates over the list, executing the function passed
* and then recursively walks over all the children. Basically,
* it is the map function but recursive.
*/
export const recurseItems = (list, func, property = 'items') => {
if (list) {
list.forEach(c => {
func(c);
recurseItems(c[property], func, property);
});
}
}
export const asyncRecurseItems = async (list, func, property = 'items') => {
if (list) {
for await (const item of list) {
func(item);
await asyncRecurseItems(item[property], func, property);
// console.log('asyncRecurseItems - item.name', item.name);
}
}
}
/**
* This function takes in a list, function, and a property.
* It iterates over the list, executing the predicate function
* passed and then recursively walks over all the children
* until it finds a match.
*/
export const recurseFindItem = (list, func, property = 'items') => {
if (list && list.length > 0) {
list.forEach(c => {
if (func(c)) {
return c;
}
recurseFindItem(c[property], func, property);
});
}
}
export const stringToColor = (input) => {
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = input.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
color = (hash & 0x00FFFFFF)
.toString(16)
.toUpperCase();
return "00000".substring(0, 6 - color.length) + color;
}
export const camelCaseToProperCase = (input) => {
return input.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
}
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arays-by-string-path
// e.g. getPath('auto.0.isBusy', data)
export const getPath = (path, obj = {}, separator = '.') => {
const properties = Array.isArray(path) ? path : path.split(separator);
return properties.reduce((prev, curr) => prev && prev[curr], obj);
}
// https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-and-arays-by-string-path
// e.g. setPath('auto.0.isBusy', data, true)
export const setPath = (path, obj = {}, value = null, separator = '.') => {
const properties = Array.isArray(path) ? path : path.split(separator);
return properties.reduce((prev, curr, i) => prev[curr] = (properties.length === ++i) ? value : prev[curr] || {}, obj);
}
/**
* getProp
* Reference: https://gist.github.com/harish2704/d0ee530e6ee75bad6fd30c98e5ad9dab
* Usage:
* let key = field.name;
* const value = getProp(this.currentItem, key);
*
* "pipeline[0].$match.modified_date.$gt"
*/
export const getProp = (object, keys, defaultVal) => {
if (object) {
keys = Array.isArray(keys) ? keys : keys.replace(/(\[(\d)\])/g, '.$2').split('.');
object = object[keys[0]];
if (object && keys.length> 1) {
return getProp(object, keys.slice(1), defaultVal);
}
}
return object === undefined ? defaultVal : object;
}
/**
* pluck
* Usage:
* this.currentItem.action_ids = pluck(closeResult.output.selectedOptions, '_id');
*
* @param {*} array
* @param {...any} keys
*/
export const pluck = (array, ...keys) => {
const [first, ...rest] = keys;
let result = array.map(o => o[first]);
rest.forEach(key => {
result = result.map(o => o[key]);
});
return result;
}
/**
* pluckValue
* Desc: This function looks for '{propName}' syntax and changes it to '${propName}` so it can render as a template string
* Usage:
* <form-multi-select id="assignedDrivers_\${$index || 0}"
* instance-options.bind="parentData.household_members"
* selected-options.bind="currentItem.assigned_drivers"
* change.delegate="customerSvc.assigned_driversChanged($event, currentItem, $index, parentData)"
* display-member="{first_name} {last_name}"
* value-member="{first_name} {last_name}"
* disabled-on-empty=true>
* </form-multi-select>
*
* const value = pluckValue(found, this.valueMember)
* @param {*} obj
* @param {*} templateString
*/
export const pluckValue = (obj = null, templateString = '') => {
try {
templateString = templateString.replace(/\{/g, '${');
const func = new Function(...Object.keys(obj), "return `" + templateString + "`;");
return func(...Object.values(obj));
} catch (err) {
return 'ERROR';
}
}
export const isJson = (text) => {
if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
return true;
}
return false;
}
export const getBindingValue = (target, whitelist = ['value.bind', 'value.two-way']) => {
let binding = Array.from(target.attributes).find((item, index) => {
return whitelist.includes(item.name);
});
if (!binding) return '';
return binding.value;
}
/**
* References:
* https://stackoverflow.com/questions/25458591/iso-date-comparison-in-native-javascript
* @param {*} iso
*/
export const dateFromISO = (iso) => {
const parts = iso.match(/\d+/g);
const year = parts[0];
const month = parts[1];
const day = parts[2];
return new Date(year, month - 1, day);
// return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);
}
export const dateStringFromISO = (iso) => {
const parts = iso.match(/\d+/g);
const year = parts[0];
const month = parts[1];
const day = parts[2];
return `${month}/${day}/${year}`;
}
export const toDateFromISO = (iso) => {
if (iso) {
const date = Date.parse(iso.replace(/-/g,'\/').replace(/T.+/, ''));
if (date !== NaN) {
const dt = new Date(date);
const y = dt.getFullYear();
const m = dt.getMonth() + 1;
const d = dt.getDate();
return `${y}-${m.toString().padStart(2, '0')}-${d.toString().padStart(2, '0')}`;
}
}
return iso;
}
export const toLocalDate = () => {
const dt = new Date();
const y = dt.getFullYear();
const m = dt.getMonth() + 1;
const d = dt.getDate();
return `${y}-${m.toString().padStart(2, '0')}-${d.toString().padStart(2, '0')}`;
}
// export const nowToIso = () => {
// const now = new Date();
// const tz = now.getTimezoneOffset();
// const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// const year = now.getFullYear();
// const month = (now.getMonth() + 1).toString().padStart(2, '0');
// const day = now.getDate().toString().padStart(2, '0');
// const hours = now.getHours().toString().padStart(2, '0');
// const minutes = now.getMinutes().toString().padStart(2, '0');
// const seconds = now.getSeconds().toString().padStart(2, '0');
// const localDateTime = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
// return {utc: now.toISOString(), localDateTime, timeZoneOffset: tz, timeZone};
// }
export const dateBuilder = () => {
const now = new Date();
const dateIso = now.toISOString();
const timeZoneOffset = now.getTimezoneOffset();
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return {dateIso, timeZoneOffset, timeZone};
}
export const toLocaleDateString = (isoDate, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) => {
const options = {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false,
timeZone
// timeZone: 'America/Los_Angeles'
// timeZone: 'America/New_York'
// timeZone: 'Asia/Shanghai'
};
const localeDateString = new Date(isoDate).toLocaleDateString(undefined, options);
const parts = localeDateString.split(', ');
const dParts = parts[0].split('/');
return `${dParts[2]}-${dParts[0]}-${dParts[1]}T${parts[1]}`;
}
export const convertTimezone = (input, timeZone) => {
const srcDt = new Date(input);
const tgtDt = new Date(srcDt.toLocaleString(undefined, {timeZone}));
const diff = srcDate.getTime() - tgtDt.getTime();
return new Date(srcDt.getTime() + diff);
}
export const dateFormat = (value, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone) => {
// const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const date = Date.parse(value);
if (value && date !== NaN) {
const dt = new Date(value);
let options = {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: true,
timeZone
// timeZone: 'America/New_York'
// timeZone: 'America/Los_Angeles'
};
// Using default locale, e.g. 'en-US'
const dtString = dt.toLocaleTimeString(undefined, options);
const parts = dtString.split(',');
const datePart = parts[0];
const timePart = parts[1];
const timeParts = timePart.split(':');
const hour = timeParts[0];
const minutes = timeParts[1];
const seccondParts = timeParts[2].split(' ');
const seconds = seccondParts[0];
const daytime = seccondParts[1];
return `${datePart} ${hour}:${minutes} ${daytime}`; // 10/29/2019 10:07 AM
}
return value;
}
export const getLocalDate = (dateString) => {
const dateParts = dateString
.substring(0, dateString.indexOf('T'))
.split('-');
const [y, m, d] = dateParts;
const newDateString = `${m}/${d}/${y}`;
return newDateString;
}
export const formatDate = (value, format = '') => {
const today = new Date(value);
if (isNaN(today)) return value;
let day = today.getDate();
if (day.toString().length === 1) {
day = `0${day}`;
}
let month = today.getMonth() + 1;
if (month.toString().length === 1) {
month = `0${month}`;
}
let year = today.getFullYear();
let hours = today.getHours() + 1;
if (hours.toString().length === 1) {
hours = `0${hours}`;
}
let minutes = today.getMinutes() + 1;
if (minutes.toString().length === 1) {
minutes = `0${minutes}`;
}
const date = `${month}/${day}/${year}`;
const time = `${hours}:${minutes}`;
let result = '';
switch (format) {
case 'date':
result = `${date}`;
break;
case 'date-time':
result = `${date} ${time}`;
break;
case 'date-time-local':
result = `${date} ${((hours + 11) % 12 + 1)}:${minutes} ${hours >= 12 ? 'PM' : 'AM'}`;
break;
default:
result = `${date}`;
break;
}
return result;
}
// https://stackoverflow.com/questions/4060004/calculate-age-given-the-birth-date-in-the-format-yyyymmdd
export const getAge = (dateString) => {
const today = new Date();
const newDateString = getLocalDate(dateString);
// const birthdate = new Date(dateString);
const birthdate = new Date(newDateString);
let age = today.getFullYear() - birthdate.getFullYear();
let m = today.getMonth() - birthdate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthdate.getDate())) {
age--;
}
return age;
}
export const getDrivingAgeDetails = (dateString) => {
const newDateString = getLocalDate(dateString);
const birthdate = new Date(newDateString);
const year = birthdate.getFullYear();
const month = birthdate.getMonth() + 1;
const day = birthdate.getDate();
const c = `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year + 16}`;
const license_date = new Date(c).toISOString();
const today = new Date();
let age = today.getFullYear() - birthdate.getFullYear();
let m = today.getMonth() - birthdate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthdate.getDate())) {
age--;
}
const license_years_experience = age - 16;
return {age, license_date, license_years_experience};
}
export const getLicenseYearsExperience = (dateString) => {
const age = this.getAge(dateString);
const years = age - 16;
return years;
}
export const randomizer = (min, max) => {
return Math.floor(Math.random() * max) + min;
}
export const isEqual = (oldValue, newValue) => {
return JSON.stringify(oldValue) === JSON.stringify(newValue);
}
export const jsonReplacer = (key, value) => {
if (typeof(value) === 'function') {
return value.toString();
}
return value;
}
export const jsonReviver = (key, value) => {
try {
const regex1 = /^([a-zA-Z]\w*|\([a-zA-Z]\w*(,\s*[a-zA-Z]\w*)*\)) => /;
const regex2 = /^async ([a-zA-Z]\w*|\([a-zA-Z]\w*(,\s*[a-zA-Z]\w*)*\)) => /;
const regex3 = /^async \(\) => /;
const regex4 = /^\(\) => /;
if (key === 'actionFn' || key === 'dataContextFn') {
let functionTemplate = `async (option) => {
const {credentials, data, lookups} = option;
let {dataIndex} = option;
const {formatDate, map, takeUntil, takeAfter} = option.helperFunctions;
${value}
}`;
return eval(functionTemplate);
}
// if (typeof value === 'string' && value.includes('(ctx, parentCtx) =>')) {
// //
// // The following is necessary due to how ES6 Modules are loaded. We are using Closure to ensure these
// // functions are available for the validation templates.
// //
// debugger;
// const path = `${location.origin}/src/services/validator/validator.js`;
// const validator = await import(path);
// const {
// all, one, optional, regex, required, within, eq, notEq, lt, lte, gt, gte, maxLen, eqLen, minLen, date, email,
// url, isNumber, isString, isObject, isFunction, isNumeric, noLeadingTrailingSpaces
// } = validator;
// let functionTemplate = `(${value})`;
// return eval(functionTemplate);
// }
if (typeof value === 'string' &&
(value.indexOf('function ') === 0 || regex1.test(value) || regex2.test(value) || regex3.test(value) || regex4.test(value))
) {
let functionTemplate = `(${value})`;
return eval(functionTemplate);
}
return value;
} catch (err) {
// debugger;
throw Error(`Key: ${key} - ${value} Error: ${err}`);
}
}
export const getLSJson = (name, reviver = jsonReviver) => {
let result = null;
const item = localStorage.getItem(name);
if (item) {
result = JSON.parse(item, reviver);
}
return result;
}
export const getLSString = (name) => {
let result = null;
const item = localStorage.getItem(name);
if (item) {
result = item;
}
return result;
}
export const setLSJson = (name, item) => {
localStorage.setItem(name, JSON.stringify(item, jsonReplacer));
}
export const moveDOM = async (sourceId, targetId) => {
await wait(100);
const source = document.getElementById(sourceId)
const target = document.getElementById(targetId)
if (source && target) {
const exists = target.querySelector(`#${sourceId}`);
if (exists) return;
target.appendChild(source);
await wait(400);
}
}
export const moveDOMSync = async (sourceId, targetId) => {
const source = document.getElementById(sourceId)
const target = document.getElementById(targetId)
if (source && target) {
const exists = target.querySelector(`#${sourceId}`);
if (exists) return;
target.appendChild(source);
await wait(150);
}
}
export const setEditorValue = async (value) => {
const editorName = 'monacoEditor';
window[editorName].setValue(value);
await wait(200);
}
export const getEditorValue = async () => {
const editorName = 'monacoEditor';
return window[editorName].getValue();
await wait(200);
}
export const setEditorLanguage = async (language = 'javascript') => {
const monacoName = 'monaco';
const editorName = 'monacoEditor';
window[monacoName].editor.setModelLanguage(window[editorName].getModel(), language);
await wait(200);
}
export const setEditorTheme = async (theme = 'vs-dark') => {
const monacoName = 'monaco';
window[monacoName].editor.setTheme(theme);
await wait(200);
}
export const setEditorLayout = async () => {
const editorName = 'monacoEditor';
return window[editorName].layout();
await wait(200);
}
export const setEditorFocus = async () => {
const editorName = 'monacoEditor';
await wait(50);
window[editorName].focus();
};
/**
* This function returns a unique id.
* It was found: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*/
export const guid = () => {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
export const moveBefore = async (e, index, item, collection) => {
const swap = collection.splice(index - 1, 1);
await wait(200);
collection.splice(index, 0, swap[0]);
await wait(200);
}
export const moveAfter = async (e, index, item, collection) => {
const swap = collection.splice(index, 1);
await wait(200);
collection.splice(index + 1, 0, swap[0]);
await wait(200);
}
//
// Drag-n-Drop
// Requires .dialog-drag and .drag-handle class
//
export const enableDragElement = (windowSelector = '.drag-window', handleSelector = '.drag-handle') => {
const state = {
isDragging: false,
xDiff: 0,
yDiff: 0,
x: 0,
y: 0
};
const dragWindow = document.querySelector(windowSelector);
const dragHandle = document.querySelector(handleSelector);
dragHandle.addEventListener('mousedown', onMouseDown);
// document.addEventListener('mousemove', onMouseMove);
// document.addEventListener('mouseup', onMouseUp);
renderWindow(dragWindow);
function renderWindow(w, myState) {
w.style.transform = `translate(${state.x}px, ${state.y}px)`;
}
function clampX(n) {
return Math.min(Math.max(n, 0),
// container width - window width
500 - 400);
}
function clampY(n) {
return Math.min(Math.max(n, 0), 800);
}
function onMouseMove(e) {
if (state.isDragging) {
// state.x = clampX(e.pageX - state.xDiff);
// state.y = clampY(e.pageY - state.yDiff);
state.x = e.pageX - state.xDiff;
state.y = e.pageY - state.yDiff;
}
renderWindow(dragWindow, state);
}
function onMouseDown(e) {
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
state.isDragging = true;
state.xDiff = e.pageX - state.x;
state.yDiff = e.pageY - state.y;
}
function onMouseUp() {
state.isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
};
//
// Colors
// https://www.w3schools.com/colors/colors_picker.asp
// https://htmlcolorcodes.com/color-chart/material-design-color-chart/
// https://htmlcolorcodes.com/color-chart/
//
export const Colors = [
{backgroundColor: "#E57373", borderColor: "#F44336"}, // RED
{backgroundColor: "#FF8A65", borderColor: "#FF5722"}, // DEEP ORANGE
{backgroundColor: "#FFB74D", borderColor: "#FF9800"}, // ORANGE
{backgroundColor: "#FFD54F", borderColor: "#FFC107"}, // AMBER
{backgroundColor: "#FFF176", borderColor: "#FFEB3B"}, // YELLOW
{backgroundColor: "#DCE775", borderColor: "#CDDC39"}, // LIME
{backgroundColor: "#AED581", borderColor: "#8BC34A"}, // LIGHT GREEN
{backgroundColor: "#81C784", borderColor: "#4CAF50"}, // GREEN
{backgroundColor: "#4DB6AC", borderColor: "#009688"}, // TEAL
{backgroundColor: "#4DD0E1", borderColor: "#00BCD4"}, // CYAN
{backgroundColor: "#4FC3F7", borderColor: "#03A9F4"}, // LIGHT BLUE
{backgroundColor: "#64B5F6", borderColor: "#2196F3"}, // BLUE
{backgroundColor: "#7986CB", borderColor: "#3F51B5"}, // INDIGO
{backgroundColor: "#9575CD", borderColor: "#673AB7"}, // DEEP PURPLE
{backgroundColor: "#BA68C8", borderColor: "#9C27B0"}, // PURPLE
{backgroundColor: "#F06292", borderColor: "#E91E63"}, // PINK
// {backgroundColor: "#A1887F", borderColor: "#795548"}, // BROWN
// {backgroundColor: "#E0E0E0", borderColor: "#9E9E9E"}, // GREY
// {backgroundColor: "#90A4AE", borderColor: "#607D8B"}, // BLUE GREY
];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment