Skip to content

Commit

Permalink
feat: add simple code wizard
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobVogelsang committed Nov 13, 2024
1 parent 2fa12a8 commit 5ea3716
Show file tree
Hide file tree
Showing 20 changed files with 1,245 additions and 150 deletions.
140 changes: 140 additions & 0 deletions components/code-wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { css, html, LitElement, nothing, TemplateResult } from 'lit';

import { customElement, property, query, state } from 'lit/decorators.js';

import 'ace-custom-element';
import '@material/mwc-dialog';

import type AceEditor from 'ace-custom-element';
import { Button } from '@material/mwc-button';
import type { Dialog } from '@material/mwc-dialog';

import { Insert, newEditEvent, Remove } from '../foundation.js';
import {
isCreateRequest,
newCloseWizardEvent,
WizardRequest,
} from '../foundation/wizard-event.js';

function formatXml(xml: string, tab: string = '\t'): string {
let formatted = '';
let indent = '';

xml.split(/>\s*</).forEach(node => {
if (node.match(/^\/\w/)) indent = indent.substring(tab!.length);
formatted += `${indent}<${node}>\r\n`;
if (node.match(/^<?\w[^>]*[^/]$/)) indent += tab;
});
return formatted.substring(1, formatted.length - 3);
}

function codeEdits(
oldElement: Element,
newElementText: string
): (Remove | Insert)[] {
const parent = oldElement.parentElement;
if (!parent) return [];

const remove: Remove = { node: oldElement };
const insert: Insert = {
parent: oldElement.parentElement,
node: new DOMParser().parseFromString(newElementText, 'application/xml')
.documentElement,
reference: oldElement.nextSibling,
};

return [remove, insert];
}

@customElement('code-wizard')
export class CodeWizard extends LitElement {
@property({ attribute: false })
wizard?: WizardRequest;

@state()
get value(): string {
return this.editor?.value ?? '';
}

set value(val: string) {
if (this.editor) this.editor.value = val;

this.requestUpdate();
}

@state()
get element(): Element | null {
if (!this.wizard) return null;

return isCreateRequest(this.wizard)
? this.wizard.parent
: this.wizard.element;
}

@query('ace-editor') editor!: AceEditor;

@query('mwc-dialog') dialog!: Dialog;

@query('.button.close') closeBtn!: Button;

@query('.button.save') saveBtn!: Button;

save(element: Element) {
const text = this.editor.value;
if (!text) return;

const edits = codeEdits(element, text);
if (!edits.length) return;

this.dispatchEvent(newEditEvent(edits));
}

onClosed(ae: CustomEvent<{ action: string }>): void {
if (ae.detail.action === 'save') this.save(this.element!);

this.dispatchEvent(newCloseWizardEvent(this.wizard!));
}

updated(): void {
this.editor.basePath = '';
this.editor.mode = 'ace/mode/xml';
this.dialog.show();
}

render(): TemplateResult {
if (!this.element) return html`${nothing}`;

return html`<mwc-dialog
heading="Edit ${this.element.tagName}"
defaultAction=""
@closed=${this.onClosed}
>
<ace-editor
wrap
soft-tabs
value="${formatXml(
new XMLSerializer().serializeToString(this.element)
)}"
></ace-editor>
<mwc-button
class="button close"
slot="secondaryAction"
dialogAction="close"
>Cancel</mwc-button
>
<mwc-button
class="button save"
slot="primaryAction"
icon="save"
dialogAction="save"
>Save</mwc-button
>
</mwc-dialog>`;
}

static styles = css`
ace-editor {
width: 60vw;
}
`;
}
79 changes: 79 additions & 0 deletions components/oscd-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { css, html, LitElement } from 'lit';

import { customElement, property } from 'lit/decorators.js';

@customElement('oscd-card')
export class Card extends LitElement {
@property({ type: Number }) stackLevel = 0;

render() {
const scale = `${1 - this.stackLevel * 0.05}`;
const translateY = `${-this.stackLevel * 40}px`;
const translateX = `0px`;

const style = `
--scale: ${scale};
--translate-x: ${translateX};
--translate-y: ${translateY};
`;

return html`
<div class="container" style=${style}>
<div class="surface">
<div class="content">
<slot></slot>
</div>
</div>
</div>
`;
}

static styles = css`
:host {
position: fixed;
top: 0px;
left: 0px;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
box-sizing: border-box;
height: 100%;
pointer-events: none;
transform: scale(var(--scale))
translate(var(--translate-x), var(--translate-y));
transition: all 0.5s ease-in-out;
}
.surface {
max-height: var(--mdc-dialog-max-height, calc(100% - 32px));
min-width: var(--mdc-dialog-min-width, 280px);
border-radius: var(--mdc-shape-medium, 4px);
background-color: var(--mdc-theme-surface, #fff);
box-shadow: var(
--mdc-dialog-box-shadow,
0px 11px 15px -7px rgba(0, 0, 0, 0.2),
0px 24px 38px 3px rgba(0, 0, 0, 0.14),
0px 9px 46px 8px rgba(0, 0, 0, 0.12)
);
position: relative;
display: flex;
flex-direction: column;
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
max-width: 100%;
max-height: 100%;
pointer-events: auto;
overflow-y: auto;
}
`;
}
78 changes: 78 additions & 0 deletions foundation/wizard-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
interface WizardRequestBase {
subWizard?: boolean;
}

export interface EditWizardRequest extends WizardRequestBase {
element: Element;
}

export interface CreateWizardRequest extends WizardRequestBase {
parent: Element;
tagName: string;
}

export type WizardRequest = EditWizardRequest | CreateWizardRequest;

export type EditWizardEvent = CustomEvent<EditWizardRequest>;
export type CreateWizardEvent = CustomEvent<CreateWizardRequest>;
export type CloseWizardEvent = CustomEvent<WizardRequest>;

export type WizardEvent = EditWizardEvent | CreateWizardEvent;

export function isCreateRequest(
wizard: CreateWizardRequest | EditWizardRequest
): wizard is CreateWizardRequest {
return (wizard as CreateWizardRequest)?.parent !== undefined;
}

export function newEditWizardEvent(
element: Element,
subWizard?: boolean,
eventInitDict?: CustomEventInit<Partial<EditWizardRequest>>
): EditWizardEvent {
return new CustomEvent<EditWizardRequest>('oscd-edit-wizard-request', {
bubbles: true,
composed: true,
...eventInitDict,
detail: { element, subWizard, ...eventInitDict?.detail },
});
}

export function newCreateWizardEvent(
parent: Element,
tagName: string,
subWizard?: boolean,
eventInitDict?: CustomEventInit<Partial<CreateWizardRequest>>
): CreateWizardEvent {
return new CustomEvent<CreateWizardRequest>('oscd-create-wizard-request', {
bubbles: true,
composed: true,
...eventInitDict,
detail: {
parent,
tagName,
subWizard,
...eventInitDict?.detail,
},
});
}

export function newCloseWizardEvent(
wizard: WizardRequest,
eventInitDict?: CustomEventInit<Partial<WizardRequest>>
): CloseWizardEvent {
return new CustomEvent<WizardRequest>('oscd-close-wizard', {
bubbles: true,
composed: true,
...eventInitDict,
detail: wizard,
});
}

declare global {
interface ElementEventMap {
['oscd-edit-wizard-request']: EditWizardEvent;
['oscd-create-wizard-request']: CreateWizardEvent;
['oscd-close-wizard']: WizardEvent;
}
}
Loading

0 comments on commit 5ea3716

Please sign in to comment.