Dialog Components
Dialog Components
Dialog components are used for user interaction in plugins and support common scenarios such as input, confirmation, and prompts.
Online Example
Convenience Functions (Recommended)
WebCAD provides a set of convenience functions that can quickly create commonly used dialogs without manually managing component lifecycles.
Input Dialogs
import { showInputDialog, showSelectDialog, showPrompt } from 'vjcad';
// 1. Fully configured input dialog
const result = await showInputDialog({
title: 'Layer Name',
label: 'Please enter the new layer name',
placeholder: 'For example: Layer1',
defaultValue: 'NewLayer',
required: true,
validator: (value) => {
if (value.length > 31) return 'Name cannot exceed 31 characters';
if (!/^[a-zA-Z0-9_-]+$/.test(value)) return 'Only letters, numbers, underscores, and hyphens are allowed';
return null; // Return null when validation passes
}
});
if (result.confirmed) {
console.log('User input:', result.value);
}
// 2. Dropdown selection dialog
const selectResult = await showSelectDialog({
title: 'Select Linetype',
label: 'Please select a linetype',
options: [
{ value: 'Continuous', label: 'Continuous' },
{ value: 'Dashed', label: 'Dashed' },
{ value: 'Dotted', label: 'Dotted' },
{ value: 'DashDot', label: 'Dash Dot' }
],
defaultValue: 'Continuous'
});
// 3. Simple prompt dialog
const name = await showPrompt('Please enter a name', 'Default value', 'Title');
if (name !== null) {
console.log('User input:', name);
}Confirmation Dialogs
import { showConfirm, showWarningConfirm, showInfo, showError } from 'vjcad';
// 1. Regular confirmation
const confirmed = await showConfirm('Are you sure you want to delete the selected entities?', 'Delete Confirmation');
if (confirmed) {
// Perform delete action
}
// 2. Warning confirmation (yellow warning icon)
const proceed = await showWarningConfirm(
'This operation cannot be undone. Do you want to continue?',
'Warning'
);
// 3. Information dialog (OK button only)
await showInfo('Operation completed', 'Info');
// 4. Error dialog (red error icon)
await showError('Save failed, please check file permissions', 'Error');Interface Definitions
InputDialogConfig
interface InputDialogConfig {
/** Dialog title */
title: string;
/** Input label text */
label?: string;
/** Placeholder text */
placeholder?: string;
/** Default value */
defaultValue?: string;
/** Confirm button text, default "OK" */
confirmText?: string;
/** Cancel button text, default "Cancel" */
cancelText?: string;
/** Validator function, returns error message or null */
validator?: (value: string) => string | null | undefined;
/** Description text (shown below the input) */
description?: string;
/** Whether required */
required?: boolean;
/** Input type, such as 'text' or 'password' */
type?: string;
}SelectDialogConfig
interface SelectDialogConfig {
/** Dialog title */
title: string;
/** Select label text */
label?: string;
/** Option list */
options: Array<{
value: string;
label: string;
disabled?: boolean;
}>;
/** Default selected value */
defaultValue?: string;
/** Confirm button text */
confirmText?: string;
/** Cancel button text */
cancelText?: string;
/** Description text */
description?: string;
}InputDialogResult
interface InputDialogResult {
/** Whether the confirm button was clicked */
confirmed: boolean;
/** User input / selected value */
value?: string;
}YesNoDialogConfig
class YesNoDialogConfig {
/** Dialog title, default "WebCAD" */
title: string = "WebCAD";
/** Message content (HTML supported) */
message: string = "";
/** Dialog type: 'info' | 'warning' | 'error' | 'confirm' */
type: 'info' | 'warning' | 'error' | 'confirm' = 'confirm';
/** Yes button title, default "Yes(Y)" */
yesTitle: string = "Yes(Y)";
/** No button title, default "No(N)" */
noTitle: string = "No(N)";
/** Whether to show the cancel button */
showCancel: boolean = false;
/** Cancel button title */
cancelTitle: string = "Cancel";
/** Whether to show only confirm button (no No button) */
confirmOnly: boolean = false;
}Dialog Suspend / Resume (Interactive Selection)
In custom dialogs, users sometimes need to temporarily leave the dialog to select entities or pick points in the CAD canvas. BaseDialogComponent provides suspend() and resume() for this purpose.
Important
The suspend() / resume() feature of dialogs must run in command context. If you call the dialog directly rather than from a command, clicking the CAD canvas triggers default command behavior.
API Description
| Method / Property | Description |
|---|---|
suspend() | Suspend the dialog, close modal state, and allow users to operate the CAD canvas |
resume() | Resume the dialog and reopen it modally at the original position |
isSuspended | Property that checks whether the dialog is currently suspended |
Basic Usage
import {
Engine, LitElement, html, css,
PointInputOptions, SelectionInputOptions, InputStatusEnum, Point2D,
CommandRegistry, CommandDefinition, CommandOptions
} from 'vjcad';
// 1. Create dialog component
class MyInteractiveDialog extends LitElement {
baseDialog = null;
result = null;
selectedEntities = [];
pickedPoint = null;
async startDialog() {
Engine.dialog.appendChild(this);
await this.updateComplete;
this.baseDialog = this.renderRoot.querySelector('base-dialog');
await this.baseDialog?._startBaseDialog({
title: "Interactive Dialog",
renderTarget: this.renderRoot
});
this.remove();
return this.result;
}
// Select entities
async selectEntities() {
// Suspend dialog
this.baseDialog?.suspend();
// Call editor selection API
const options = new SelectionInputOptions();
const result = await Engine.editor.getSelections(options);
// Resume dialog
this.baseDialog?.resume();
if (result.status === InputStatusEnum.OK && result.value?.length > 0) {
this.selectedEntities = result.value;
this.requestUpdate();
}
}
// Pick point
async pickPoint() {
// Suspend dialog
this.baseDialog?.suspend();
// Call editor point-picking API
const options = new PointInputOptions("Specify point:");
const result = await Engine.editor.getPoint(options);
// Resume dialog
this.baseDialog?.resume();
if (result.status === InputStatusEnum.OK && result.value) {
this.pickedPoint = result.value;
this.requestUpdate();
}
}
// Pick point with rubber-band
async pickPointWithRubberband(basePoint) {
this.baseDialog?.suspend();
const options = new PointInputOptions("Specify next point:");
if (basePoint) {
options.useBasePoint = true;
options.basePoint = new Point2D(basePoint.x, basePoint.y);
}
const result = await Engine.editor.getPoint(options);
this.baseDialog?.resume();
return result;
}
okCallback() {
this.result = { entities: this.selectedEntities, point: this.pickedPoint };
this.baseDialog?.close();
}
cancelCallback() {
this.result = null;
this.baseDialog?.close();
}
render() {
return html`
<base-dialog>
<div class="container">
<div class="row">
<span>Selected entities: ${this.selectedEntities.length}</span>
<button @click=${this.selectEntities}>Select</button>
</div>
<div class="row">
<span>Coordinate: ${this.pickedPoint ?
`(${this.pickedPoint.x.toFixed(2)}, ${this.pickedPoint.y.toFixed(2)})` :
'Not specified'}</span>
<button @click=${this.pickPoint}>Pick</button>
</div>
<div class="button-bar">
<button @click=${this.cancelCallback}>Cancel</button>
<button @click=${this.okCallback}>OK</button>
</div>
</div>
</base-dialog>
`;
}
static styles = css`
/* Dark theme styles */
base-dialog {
--dialog-header-bg: #2d2d30;
--dialog-header-color: #f0f0f0;
--dialog-header-border: rgba(255,255,255,0.1);
--dialog-contents-color: #2d2d30;
}
.container { padding: 16px; background: #2d2d30; color: #f0f0f0; }
.row { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.button-bar { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
button { padding: 6px 12px; background: #0e639c; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #1177bb; }
`;
}
customElements.define('my-interactive-dialog', MyInteractiveDialog);
// 2. Create command class (important)
class MyInteractiveCommand {
async main() {
const dialog = new MyInteractiveDialog();
const result = await dialog.startDialog();
if (result) {
console.log('Selected entities:', result.entities);
console.log('Picked point:', result.point);
}
}
}
// 3. Register command
const cmdDef = new CommandDefinition(
'MYINTERACTIVE',
'Interactive Dialog Example',
MyInteractiveCommand,
new CommandOptions()
);
CommandRegistry.regist(cmdDef);
CommandRegistry.updated = true;
// 4. Execute command
await Engine.editor.executerWithOp('MYINTERACTIVE');Vue 3 Integration
You can also use Vue 3 to implement dialog content by dynamically loading the Vue 3 library:
// Dynamically load Vue 3
const loadVue3 = () => {
return new Promise((resolve, reject) => {
if (window.Vue) {
resolve(window.Vue);
return;
}
const script = document.createElement('script');
script.src = '/js/vue@3.js';
script.onload = () => resolve(window.Vue);
document.head.appendChild(script);
});
};
const Vue = await loadVue3();
const { createApp, ref } = Vue;
// Create dialog class
class VueDialog {
constructor() {
this.baseDialog = null;
this.vueApp = null;
this.result = null;
this.state = { entityCount: 0, point: null };
}
async selectEntities() {
if (this.baseDialog) this.baseDialog.suspend();
const result = await Engine.editor.getSelections(new SelectionInputOptions());
if (this.baseDialog) this.baseDialog.resume();
if (result.status === InputStatusEnum.OK && result.value) {
this.state.entityCount = result.value.length;
}
}
async startDialog() {
// Create container
const container = document.createElement('div');
container.innerHTML = '<base-dialog><div id="vue-root"></div></base-dialog>';
Engine.dialog.appendChild(container);
this.baseDialog = container.querySelector('base-dialog');
// Create Vue app
const dialogInstance = this;
this.vueApp = createApp({
setup() {
const entityCount = ref(0);
// Periodically sync state
setInterval(() => {
entityCount.value = dialogInstance.state.entityCount;
}, 100);
return { entityCount, selectEntities: () => dialogInstance.selectEntities() };
},
template: `
<div class="container">
<p>Selected entities: {{ entityCount }}</p>
<button @click="selectEntities">Select</button>
</div>
`
});
this.vueApp.mount(container.querySelector('#vue-root'));
await this.baseDialog._startBaseDialog({
title: "Vue3 Dialog",
renderTarget: container
});
this.vueApp.unmount();
container.remove();
return this.result;
}
}Custom Dialogs
If the convenience functions do not meet your needs, you can extend BaseDialogComponent to create a custom dialog.
Basic Structure
import { LitElement, html, css } from 'vjcad';
import { BaseDialogComponent } from 'vjcad';
import { Engine } from 'vjcad';
class MyCustomDialog extends BaseDialogComponent {
// Define result
result: any = undefined;
// Define properties
static properties = {
...BaseDialogComponent.properties,
myValue: { type: String }
};
declare myValue: string;
private baseDialog: any;
constructor() {
super();
this.myValue = '';
}
firstUpdated(): void {
super.firstUpdated();
this.baseDialog = this.shadowRoot?.querySelector('base-dialog');
}
// Confirm button
onConfirm(): void {
this.result = this.myValue;
this.baseDialog?.close();
}
// Cancel button
onCancel(): void {
this.result = undefined;
this.baseDialog?.close();
}
// Show dialog
async show(): Promise<any> {
Engine.dialog.appendChild(this);
await this.waitUpdated();
await this.baseDialog?._startBaseDialog({
title: 'My Dialog',
renderTarget: (this as any).renderRoot
});
this.remove();
return this.result;
}
render() {
return html`
<base-dialog>
<div id="container">
<div id="content">
<!-- Custom content -->
<input
.value=${this.myValue}
@input=${(e: Event) => this.myValue = (e.target as HTMLInputElement).value}
/>
</div>
<div id="button-bar">
<dlg-button accept="true" @click=${this.onConfirm}>
OK
</dlg-button>
<dlg-button @click=${this.onCancel}>
Cancel
</dlg-button>
</div>
</div>
</base-dialog>
`;
}
static styles = css`
#container {
padding: 16px;
background-color: var(--dialog-contents-color, #f5f7fa);
min-width: 300px;
}
#content {
margin-bottom: 16px;
}
#button-bar {
display: flex;
justify-content: flex-end;
gap: 10px;
}
`;
}
// Register custom element
customElements.define('my-custom-dialog', MyCustomDialog);Use Custom Dialog
const dialog = new MyCustomDialog();
const result = await dialog.show();
if (result !== undefined) {
console.log('User input:', result);
}DlgButtonComponent
The dialog button component is used to create standard-style buttons in custom dialogs.
HTML Tag
<dlg-button>Normal Button</dlg-button>
<dlg-button accept="true">Primary Button</dlg-button>
<dlg-button disabled>Disabled Button</dlg-button>Properties
| Property | Type | Description |
|---|---|---|
accept | boolean | Whether this is the primary button (blue background) |
disabled | boolean | Whether disabled |
CSS Variables
Dialog components support the following CSS variables for theme customization:
/* Set CSS variables on base-dialog */
base-dialog {
/* Header background color */
--dialog-header-bg: #d0d0d0;
/* Header text color */
--dialog-header-color: #000;
/* Header bottom border color */
--dialog-header-border: rgba(0,0,0,.05);
/* Content background color */
--dialog-contents-color: #f5f7fa;
/* Element border color */
--dialog-ele-border-color: #d8d8d8;
}
/* Dark theme example */
base-dialog {
--dialog-header-bg: #2d2d30;
--dialog-header-color: #f0f0f0;
--dialog-header-border: rgba(255,255,255,0.1);
--dialog-contents-color: #2d2d30;
}Keyboard Shortcuts
| Shortcut | Action |
|---|---|
Enter | Confirm / Yes |
Escape | Cancel / No |
Y | Yes (YesNoDialog) |
N | No (YesNoDialog) |
Dialog / Panel Base Classes
WebCAD provides two base classes for quickly creating custom dialogs and panels:
| Base Class | Description | Suitable Scenarios |
|---|---|---|
ModalDialogBase | Modal dialog base class | Scenarios where users must complete an action before continuing |
ModelessPanelBase | Modeless panel base class | Tool panels, property panels, and other continuously visible panels |
ModalDialogBase - Modal Dialog Base Class
Modal dialogs prevent users from operating the CAD interface until the dialog is closed. They include built-in dark-theme styles and support suspend() / resume().
Basic Usage
import { ModalDialogBase, html } from 'vjcad';
class MySettingsDialog extends ModalDialogBase<{ color: number; width: number }> {
// Set dialog title
static dialogTitle = "Settings";
// Define properties
static properties = {
...ModalDialogBase.properties,
color: { type: Number },
width: { type: Number },
};
color = 1;
width = 1;
// Implement renderContent()
renderContent() {
return html`
<div style="min-width: 300px;">
<div class="row">
<span class="label" style="width: 80px;">Color:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.color)}
@input=${(e) => this.color = parseInt(e.target.value)}>
</div>
<div class="row" style="margin-top: 12px;">
<span class="label" style="width: 80px;">Line Width:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.width)}
@input=${(e) => this.width = parseInt(e.target.value)}>
</div>
</div>
`;
}
// Override confirm() to set return result
confirm() {
this.result = { color: this.color, width: this.width };
this.close();
}
}
customElements.define('my-settings-dialog', MySettingsDialog);
// Use dialog
const dialog = new MySettingsDialog();
const result = await dialog.startDialog();
if (result) {
console.log('Settings:', result.color, result.width);
}Use suspend() / resume() to Pick Points
import {
ModalDialogBase, html,
PointInputOptions, InputStatusEnum, Point2D, Engine
} from 'vjcad';
class DrawCircleDialog extends ModalDialogBase {
static dialogTitle = "Draw Circle";
centerPoint = null;
radius = 20;
// Pick center
async pickCenter() {
this.suspend(); // Suspend dialog
const options = new PointInputOptions("Specify center point:");
const result = await Engine.editor.getPoint(options);
this.resume(); // Resume dialog
if (result.status === InputStatusEnum.OK && result.value) {
this.centerPoint = { x: result.value.x, y: result.value.y };
}
}
// Pick radius (with rubber-band)
async pickRadius() {
if (!this.centerPoint) return;
this.suspend();
const options = new PointInputOptions("Specify radius:");
options.useBasePoint = true;
options.basePoint = new Point2D(this.centerPoint.x, this.centerPoint.y);
const result = await Engine.editor.getPoint(options);
this.resume();
if (result.status === InputStatusEnum.OK && result.value) {
const dx = result.value.x - this.centerPoint.x;
const dy = result.value.y - this.centerPoint.y;
this.radius = Math.sqrt(dx * dx + dy * dy);
}
}
renderContent() {
return html`
<div>
<div class="row">
<span class="label">Center:</span>
<span>${this.centerPoint ? `(${this.centerPoint.x.toFixed(2)}, ${this.centerPoint.y.toFixed(2)})` : 'Not specified'}</span>
<button class="btn" @click=${this.pickCenter}>Pick</button>
</div>
<div class="row">
<span class="label">Radius:</span>
<input type="number" class="input" .value=${String(this.radius)}
@input=${(e) => this.radius = parseFloat(e.target.value)}>
<button class="btn" @click=${this.pickRadius}>Pick from Drawing</button>
</div>
</div>
`;
}
confirm() {
if (!this.centerPoint) return;
this.result = { center: this.centerPoint, radius: this.radius };
this.close();
}
}Disable Shadow DOM
When you need to use third-party libraries such as x-spreadsheet, you can disable Shadow DOM:
import { ModalDialogBase, createNoShadowStyles, html } from 'vjcad';
class NoShadowDialog extends ModalDialogBase {
static dialogTitle = "Use Third-Party Library";
// Disable Shadow DOM
useShadowDOM = false;
renderContent() {
return html`<div>Custom content</div>`;
}
// Styles must be injected manually
render() {
return html`
<style>${createNoShadowStyles('no-shadow-dialog')}</style>
<base-dialog>
<div class="dialog-body">${this.renderContent()}</div>
<div class="dialog-footer">${this.renderFooter()}</div>
</base-dialog>
`;
}
}ModalDialogBase API
| Property / Method | Description |
|---|---|
static dialogTitle | Dialog title |
result | Dialog return result |
useShadowDOM | Whether to use Shadow DOM, default true |
startDialog(options?) | Show dialog and wait for result |
renderContent() | Abstract method - render dialog content |
renderFooter() | Render footer buttons, defaults to OK / Cancel |
confirm() | OK button callback, should set this.result |
cancel() | Cancel button callback |
close() | Close dialog |
suspend() | Suspend dialog (for point picking / entity selection) |
resume() | Resume dialog |
isSuspended | Whether currently suspended |
ModelessPanelBase - Modeless Panel Base Class
Modeless panels do not block operation of the CAD interface, making them suitable for tool panels, property panels, and other continuously visible views. They include built-in dark-theme styles and dragging support.
Basic Usage
import { ModelessPanelBase, html, Engine, LineEnt } from 'vjcad';
class DrawToolsPanel extends ModelessPanelBase {
// Panel title
static panelTitle = "Drawing Tools";
// Panel width
static panelWidth = "200px";
// Initial position
static initialPosition = { top: '100px', right: '20px' };
drawLine() {
const line = new LineEnt([0, 0], [100, 50]);
line.setDefaults();
Engine.addEntities(line);
Engine.zoomExtents();
}
// Implement renderContent()
renderContent() {
return html`
<div style="display: flex; flex-direction: column; gap: 8px;">
<button class="btn btn-primary" @click=${this.drawLine}>Draw Line</button>
<button class="btn" @click=${() => Engine.zoomExtents()}>Zoom Extents</button>
</div>
`;
}
}
customElements.define('draw-tools-panel', DrawToolsPanel);
// Use panel
const panel = new DrawToolsPanel();
document.body.appendChild(panel);
panel.show();
// Control visibility
panel.hide();
panel.toggle();
panel.destroy();Panel with State
class PropertiesPanel extends ModelessPanelBase {
static panelTitle = "Properties";
static panelWidth = "280px";
static properties = {
...ModelessPanelBase.properties,
currentColor: { type: Number },
lineWeight: { type: Number },
};
constructor() {
super();
this.currentColor = 7;
this.lineWeight = 1;
}
renderContent() {
return html`
<div>
<div class="row">
<span class="label">Color:</span>
<input type="number" class="input"
.value=${String(this.currentColor)}
@input=${(e) => this.currentColor = parseInt(e.target.value)}>
</div>
<div class="row">
<span class="label">Line Weight:</span>
<input type="number" class="input"
.value=${String(this.lineWeight)}
@input=${(e) => this.lineWeight = parseInt(e.target.value)}>
</div>
</div>
`;
}
}Dynamic Title
class EntityInfoPanel extends ModelessPanelBase {
static panelTitle = "Entity Info";
entityCount = 0;
// Override onShow to refresh data when displayed
onShow() {
super.onShow();
this.entityCount = Engine.getEntities().length;
}
// Override getPanelTitle() for dynamic title
getPanelTitle() {
return `Entity Info (${this.entityCount})`;
}
renderContent() {
return html`<div>Total ${this.entityCount} entities</div>`;
}
}Use createPanel Factory Function
import { createPanel } from 'vjcad';
// Create panel manager
const panelManager = createPanel(PropertiesPanel, 'properties-panel');
// Control panel
panelManager.show();
panelManager.hide();
panelManager.toggle();
panelManager.destroy();
// Check state
if (panelManager.isVisible) {
console.log('Panel is visible');
}ModelessPanelBase API
| Property / Method | Description |
|---|---|
static panelTitle | Panel title |
static panelWidth | Panel width, such as "360px" |
static initialPosition | Initial position, such as { top: '100px', right: '20px' } |
static maxHeight | Maximum height, such as "80vh" |
renderContent() | Abstract method - render panel content |
getPanelTitle() | Get panel title (can be overridden for dynamic titles) |
show() | Show panel |
hide() | Hide panel |
toggle() | Toggle visibility |
destroy() | Destroy panel |
isVisible | Whether visible |
onShow() | Callback when panel is shown |
onHide() | Callback when panel is hidden |
onDestroy() | Callback when panel is destroyed |
Built-in CSS Classes
The base classes provide a set of built-in dark-theme style classes:
| CSS Class | Description |
|---|---|
.row | Row container with flex layout |
.label | Label text |
.input | Input style |
.select | Select style |
.btn | Normal button |
.btn-primary | Primary button (blue) |
.section-title | Section title |
.hint | Hint text |
.status | Status text |
Next Steps
- Picker Dialogs - color, linetype, and pattern pickers
- Icon Registration - SVG icons
- Ribbon Menu - toolbar extension