Last active
October 24, 2020 16:10
-
-
Save mattduffield/d14741e83eb2116bd9df048ac68d12f5 to your computer and use it in GitHub Desktop.
Router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "dependencies": { | |
| "aurelia": "dev" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :host { | |
| min-height: 225px; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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++}`; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <p>Home Screen</p> | |
| <button | |
| click.trigger="launch($event)"> | |
| Launch | |
| </button> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :root { | |
| --primary-text-color: black; | |
| --primary-color: blue; | |
| --primary-offset-color: white; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- | |
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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