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 3f1214d
Show file tree
Hide file tree
Showing 20 changed files with 1,244 additions and 150 deletions.
128 changes: 128 additions & 0 deletions components/code-wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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 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;

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 slot="secondaryAction" dialogAction="close"
>Cancel</mwc-button
>
<mwc-button 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;
}
}
58 changes: 58 additions & 0 deletions open-scd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type { ActionDetail } from '@material/mwc-list';
import type { Dialog } from '@material/mwc-dialog';
import type { Drawer } from '@material/mwc-drawer';

import './components/code-wizard.js';
import './components/oscd-card.js';
import {
convertEdit,
EditV2,
Expand All @@ -25,11 +27,18 @@ import {
isRemove,
isSetAttributes,
} from '@openenergytools/xml-lib';
import type { CodeWizard } from './components/code-wizard.js';

import { allLocales, sourceLocale, targetLocales } from './locales.js';

import { cyrb64, EditEvent, OpenEvent } from './foundation.js';

import {
CloseWizardEvent,
CreateWizardEvent,
EditWizardEvent,
WizardRequest,
} from './foundation/wizard-event.js';
import { EditEventV2 } from './foundation/edit-event-v2.js';

export type LogEntry = { undo: EditV2; redo: EditV2; title?: string };
Expand Down Expand Up @@ -374,6 +383,28 @@ export class OpenSCD extends LitElement {
.filter(p => p !== undefined);
}

/** FIFO queue of [[`Wizard`]]s to display. */
@state()
workflow: WizardRequest[] = [];

@query('.wizard') wizardElement?: CodeWizard;

private closeWizard(we: CloseWizardEvent): void {
const wizard = we.detail;

this.workflow.splice(this.workflow.indexOf(wizard), 1);
this.requestUpdate();
}

private onWizard(we: EditWizardEvent | CreateWizardEvent) {
const wizard = we.detail;

if (wizard.subWizard) this.workflow.unshift(wizard);
else this.workflow.push(wizard);

this.requestUpdate();
}

private hotkeys: Partial<Record<string, () => void>> = {
m: this.controls.menu.action,
z: this.controls.undo.action,
Expand All @@ -399,6 +430,27 @@ export class OpenSCD extends LitElement {
this.addEventListener('oscd-edit-v2', event =>
this.handleEditEventV2(event)
);

this.addEventListener('oscd-edit-wizard-request', event =>
this.onWizard(event as EditWizardEvent)
);
this.addEventListener('oscd-create-wizard-request', event =>
this.onWizard(event as CreateWizardEvent)
);
this.addEventListener('oscd-close-wizard', event =>
this.closeWizard(event as CloseWizardEvent)
);
}

private renderWizard(): TemplateResult {
if (!this.workflow.length) return html``;

return html`${this.workflow.map(
(wizard, i, arr) =>
html`<oscd-card .stackLevel="${arr.length - i - 1}"
><code-wizard .wizard=${wizard}></code-wizard
></oscd-card>`
)}`;
}

private renderLogEntry(entry: LogEntry) {
Expand Down Expand Up @@ -499,6 +551,7 @@ export class OpenSCD extends LitElement {
>${msg('Close')}</mwc-button
>
</mwc-dialog>
${this.renderWizard()}
<aside>
${this.plugins.menu.map(
plugin =>
Expand Down Expand Up @@ -534,6 +587,11 @@ export class OpenSCD extends LitElement {
mwc-top-app-bar-fixed {
--mdc-theme-text-disabled-on-light: rgba(255, 255, 255, 0.38);
} /* hack to fix disabled icon buttons rendering black */
mwc-dialog {
display: flex;
flex-direction: column;
}
`;
}

Expand Down
Loading

0 comments on commit 3f1214d

Please sign in to comment.