Custom Dialogs
Custom Dialogs
Complete guide to creating custom dialogs and panels.
Dialog Types
WebCAD supports two dialog modes:
| Type | Features | Suitable Scenarios |
|---|---|---|
| Modal dialog | The graphics interface cannot be operated after opening | Complex settings, data input, point/entity picking |
| Modeless panel | The graphics interface remains operable | Tool panels, find/replace, live preview |
UI Style Guidelines
WebCAD uses a dark theme style. The following color palette is recommended:
/* Primary colors */
--bg-primary: #1e2530; /* Primary background */
--bg-secondary: #0d1117; /* Secondary background (inputs, lists) */
--bg-header: #252d3a; /* Header background */
--border-color: #3d4a5c; /* Border color */
--text-primary: #e8eaed; /* Primary text */
--text-secondary: #9ca3af; /* Secondary text */
--text-muted: #6b7280; /* Muted text */
--accent-color: #58a6ff; /* Accent color */
--btn-primary: #1a56db; /* Primary button */
--btn-hover: #1e40af; /* Button hover */Common Style Template
/* Container */
.dialog-container {
background: #1e2530;
border: 1px solid #3d4a5c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #e8eaed;
}
/* Header */
.header {
padding: 12px 16px;
background: #252d3a;
border-bottom: 1px solid #3d4a5c;
border-radius: 8px 8px 0 0;
}
/* Input */
.input {
background: #0d1117;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 12px;
color: #e8eaed;
font-size: 13px;
}
.input:focus {
outline: none;
border-color: #58a6ff;
}
/* Button */
.btn {
background: #2d3748;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 16px;
color: #e8eaed;
cursor: pointer;
}
.btn:hover {
background: #3d4a5c;
}
.btn-primary {
background: #1a56db;
border-color: #1a56db;
}
.btn-primary:hover {
background: #1e40af;
}
/* Checkbox */
.checkbox input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: #58a6ff;
}Modal Dialogs
Use the base-dialog component to create modal dialogs.
Basic Structure
import { html, LitElement, Engine, type TemplateResult } from 'vjcad';
export class MyDialog extends LitElement {
private baseDialog!: any;
// Disable Shadow DOM (if third-party library CSS is needed)
createRenderRoot() {
return this;
}
static properties = {
// Declare reactive properties
myValue: { type: String }
};
declare myValue: string;
constructor() {
super();
this.myValue = '';
}
async firstUpdated(): Promise<void> {
this.baseDialog = this.querySelector('base-dialog');
}
/**
* Start the dialog
*/
async startDialog(): Promise<void> {
// Add to dialog container
Engine.dialog!.appendChild(this);
await this.updateComplete;
// Start dialog
await this.baseDialog?._startBaseDialog({
title: "My Dialog",
renderTarget: this.renderRoot
});
// Clean up after dialog closes
this.remove();
}
/**
* Close the dialog
*/
private close(): void {
this.baseDialog?.close();
}
render() {
return html`
<style>
my-dialog {
display: block;
}
my-dialog #container {
width: 400px;
background: #1e2530;
color: #e8eaed;
padding: 16px;
}
/* ... other styles ... */
</style>
<base-dialog>
<div id="container">
<div class="body">
<!-- Dialog content -->
<input type="text" .value=${this.myValue}
@input=${(e: Event) => this.myValue = (e.target as HTMLInputElement).value} />
</div>
<div class="buttons">
<button @click=${this.close}>Cancel</button>
<button class="btn-primary" @click=${this.onConfirm}>OK</button>
</div>
</div>
</base-dialog>
`;
}
private onConfirm(): void {
// Handle confirmation logic
this.close();
}
}
// Register custom element
customElements.define('my-dialog', MyDialog);Invoke the Dialog
const dialog = new MyDialog();
await dialog.startDialog();Picking Points and Entities in a Modal Dialog
When a modal dialog is open, the user cannot directly operate the CAD interface. Use suspend() and resume() to temporarily pause the dialog and allow picking in the CAD view.
Important: the
suspend()andresume()features of dialogs must be used in a command context!If you call a dialog directly instead of from the command's
main()method, clicking the CAD interface triggers default command behavior.Correct usage:
- Create a command class and start the dialog in
main()- Register the command with
CommandRegistry.regist()- Execute the command with
Engine.editor.executerWithOp('COMMAND_NAME')See the "Complete Example" section below for details.
Pick Points
import {
html, LitElement, Engine,
getPoint, getCorner,
PointInputOptions, CornerInputOptions,
InputStatusEnum, SelectionModeEnum
} from 'vjcad';
export class MyPickDialog extends LitElement {
private baseDialog!: any;
private selectedBounds: { minX: number; minY: number; maxX: number; maxY: number } | null = null;
/**
* Pick a rectangular range
*/
private async pickRange(): Promise<void> {
// 1. Pause dialog to allow CAD interaction
this.baseDialog?.suspend();
try {
// 2. Get first corner
const pointOptions = new PointInputOptions("Specify first corner of range:");
pointOptions.useOsnap = false; // Disable object snap
const point1Result = await getPoint(pointOptions);
if (point1Result.status !== InputStatusEnum.OK) {
return; // User canceled
}
const firstPoint = point1Result.value;
// 3. Get second corner (diagonal) and show selection window
const cornerOptions = new CornerInputOptions("Specify diagonal corner of range:", firstPoint);
cornerOptions.effect = SelectionModeEnum.Window; // Window selection effect
cornerOptions.useOsnap = false;
const cornerResult = await getCorner(cornerOptions);
if (cornerResult.status !== InputStatusEnum.OK) {
return; // User canceled
}
const secondPoint = cornerResult.value;
// 4. Calculate bounds
this.selectedBounds = {
minX: Math.min(firstPoint.x, secondPoint.x),
minY: Math.min(firstPoint.y, secondPoint.y),
maxX: Math.max(firstPoint.x, secondPoint.x),
maxY: Math.max(firstPoint.y, secondPoint.y)
};
} finally {
// 5. Resume dialog whether successful or canceled
this.baseDialog?.resume();
this.requestUpdate();
}
}
render() {
return html`
<base-dialog>
<div id="container">
<div class="row">
<span>Selection range:</span>
<button @click=${this.pickRange}>Select</button>
${this.selectedBounds ? html`
<span>Selected</span>
` : ''}
</div>
</div>
</base-dialog>
`;
}
}Pick Entities
import { getSelections, SelectionInputOptions, InputStatusEnum } from 'vjcad';
export class MyEntityPickDialog extends LitElement {
private baseDialog!: any;
private selectedEntities: any[] = [];
/**
* Pick entities
*/
private async pickEntities(): Promise<void> {
// Pause dialog
this.baseDialog?.suspend();
try {
const options = new SelectionInputOptions("Select entities to process:");
const result = await getSelections(options);
if (result.status === InputStatusEnum.OK) {
this.selectedEntities = result.value;
}
} finally {
// Resume dialog
this.baseDialog?.resume();
this.requestUpdate();
}
}
}Key Methods
| Method | Description |
|---|---|
baseDialog.suspend() | Pause dialog and allow CAD interaction |
baseDialog.resume() | Resume dialog |
Modeless Panels
Modeless panels do not block operations in the CAD interface and are suitable for tool panels and similar scenarios.
Basic Structure
import { LitElement, html, css, Engine, type TemplateResult } from 'vjcad';
class MyPanel extends LitElement {
// Reactive properties
static properties = {
searchText: { type: String },
results: { type: Array }
};
declare searchText: string;
declare results: any[];
// Dragging state
private isDragging = false;
private dragStartX = 0;
private dragStartY = 0;
private panelStartLeft = 0;
private panelStartTop = 0;
// Style definition
static styles = css`
:host {
position: fixed;
top: 100px;
right: 20px;
width: 360px;
background: #1e2530;
border: 1px solid #3d4a5c;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: #e8eaed;
z-index: 100000;
display: none;
flex-direction: column;
user-select: none;
}
:host([visible]) {
display: flex;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #252d3a;
border-bottom: 1px solid #3d4a5c;
border-radius: 8px 8px 0 0;
cursor: grab;
}
.header:active {
cursor: grabbing;
}
.title {
font-weight: 600;
font-size: 14px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.close-btn:hover {
color: #fff;
}
.body {
padding: 16px;
}
.input {
width: 100%;
background: #0d1117;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 8px 12px;
color: #e8eaed;
font-size: 13px;
outline: none;
}
.input:focus {
border-color: #58a6ff;
}
.btn {
background: #2d3748;
border: 1px solid #3d4a5c;
border-radius: 4px;
padding: 6px 12px;
color: #e8eaed;
font-size: 12px;
cursor: pointer;
}
.btn:hover {
background: #3d4a5c;
}
.btn-primary {
background: #1a56db;
border-color: #1a56db;
}
.btn-primary:hover {
background: #1e40af;
}
`;
constructor() {
super();
this.searchText = '';
this.results = [];
// Bind drag events
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
}
connectedCallback(): void {
super.connectedCallback();
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
}
disconnectedCallback(): void {
super.disconnectedCallback();
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
// Drag handling
private handleHeaderMouseDown(e: MouseEvent): void {
if ((e.target as HTMLElement).classList.contains('close-btn')) return;
this.isDragging = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
const rect = this.getBoundingClientRect();
this.panelStartLeft = rect.left;
this.panelStartTop = rect.top;
}
private handleMouseMove(e: MouseEvent): void {
if (!this.isDragging) return;
const dx = e.clientX - this.dragStartX;
const dy = e.clientY - this.dragStartY;
this.style.left = `${this.panelStartLeft + dx}px`;
this.style.top = `${this.panelStartTop + dy}px`;
this.style.right = 'auto';
}
private handleMouseUp(): void {
this.isDragging = false;
}
// Show/hide
show(): void {
this.setAttribute('visible', '');
// Focus input
this.updateComplete.then(() => {
const input = this.shadowRoot?.querySelector('.input') as HTMLInputElement;
input?.focus();
});
}
hide(): void {
this.removeAttribute('visible');
}
destroy(): void {
this.remove();
}
render(): TemplateResult {
return html`
<div class="header" @mousedown=${this.handleHeaderMouseDown}>
<span class="title">My Panel</span>
<button class="close-btn" title="Close" @click=${this.hide}>×</button>
</div>
<div class="body">
<input
type="text"
class="input"
placeholder="Enter content"
.value=${this.searchText}
@input=${(e: Event) => this.searchText = (e.target as HTMLInputElement).value}
>
<div class="buttons" style="margin-top: 12px; display: flex; gap: 8px;">
<button class="btn btn-primary" @click=${this.doSearch}>Search</button>
</div>
</div>
`;
}
private doSearch(): void {
// Search logic
Engine.writeMessage(`<br/>Search: ${this.searchText}`);
}
}
// Register custom element
if (!customElements.get('my-panel')) {
customElements.define('my-panel', MyPanel);
}Create and Use a Panel
export interface MyPanelInterface {
show(): void;
hide(): void;
destroy(): void;
}
export function createMyPanel(): MyPanelInterface {
const panel = document.createElement('my-panel') as MyPanel;
document.body.appendChild(panel);
return {
show: () => panel.show(),
hide: () => panel.hide(),
destroy: () => panel.destroy()
};
}
// Usage
const panel = createMyPanel();
panel.show();
// ... user interaction ...
panel.hide();
panel.destroy();Use Base Classes (Recommended)
WebCAD provides ModalDialogBase and ModelessPanelBase base classes, allowing you to quickly create dialogs and panels without manually handling styles, dragging, and related details.
ModalDialogBase - Modal Dialog Base Class
It includes built-in dark-theme styles and suspend() / resume() support. You only need to implement renderContent().
import { ModalDialogBase, html, Engine, CircleEnt } from 'vjcad';
class DrawCircleDialog extends ModalDialogBase<{ center: {x: number, y: number}, radius: number }> {
// Dialog title
static dialogTitle = "Draw Circle";
// Reactive properties
static properties = {
...ModalDialogBase.properties,
centerPoint: { type: Object },
radius: { type: Number },
};
centerPoint = null;
radius = 20;
// Pick center point
async pickCenter() {
this.suspend(); // Pause dialog
const result = await Engine.editor.getPoint(new PointInputOptions("Specify center point:"));
this.resume(); // Resume dialog
if (result.status === InputStatusEnum.OK) {
this.centerPoint = result.value;
}
}
// Required: render dialog content
renderContent() {
return html`
<div style="min-width: 300px;">
<div class="row">
<span class="label" style="width: 60px;">Center:</span>
<span class="input" style="flex: 1;">${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" style="margin-top: 12px;">
<span class="label" style="width: 60px;">Radius:</span>
<input type="number" class="input" style="flex: 1;"
.value=${String(this.radius)}
@input=${(e) => this.radius = parseFloat(e.target.value)}>
</div>
</div>
`;
}
// Override confirm() to set return result
confirm() {
if (!this.centerPoint) return;
this.result = { center: this.centerPoint, radius: this.radius };
this.close();
}
}
customElements.define('draw-circle-dialog', DrawCircleDialog);
// Use in a command
class DrawCircleCommand {
async main() {
const dialog = new DrawCircleDialog();
const result = await dialog.startDialog();
if (result) {
const circle = new CircleEnt([result.center.x, result.center.y], result.radius);
circle.setDefaults();
Engine.addEntities(circle);
}
}
}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() | Required - render dialog content |
renderFooter() | Render footer buttons, default is OK / Cancel |
confirm() | OK button callback, should set this.result |
cancel() | Cancel button callback |
close() | Close dialog |
suspend() | Pause dialog (for point picking / entity selection) |
resume() | Resume dialog |
ModelessPanelBase - Modeless Panel Base Class
It includes built-in dark-theme styles and dragging support. You only need to implement renderContent().
import { ModelessPanelBase, html, Engine, LineEnt } from 'vjcad';
class DrawToolsPanel extends ModelessPanelBase {
// Panel config
static panelTitle = "Drawing Tools";
static panelWidth = "200px";
static initialPosition = { top: '100px', right: '20px' };
drawLine() {
const line = new LineEnt([0, 0], [Math.random() * 100, Math.random() * 100]);
line.setDefaults();
Engine.addEntities(line);
Engine.zoomExtents();
}
// Required: render panel content
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();Use the createPanel Factory Function
import { createPanel } from 'vjcad';
const panelManager = createPanel(DrawToolsPanel, 'draw-tools-panel');
panelManager.show();
panelManager.hide();
panelManager.toggle();
panelManager.destroy();ModelessPanelBase API
| Property / Method | Description |
|---|---|
static panelTitle | Panel title |
static panelWidth | Panel width |
static initialPosition | Initial position, such as { top: '100px', right: '20px' } |
static maxHeight | Maximum height |
renderContent() | Required - render panel content |
getPanelTitle() | Get title (can be overridden for dynamic titles) |
show() / hide() / toggle() | Visibility control |
destroy() | Destroy panel |
isVisible | Whether visible |
onShow() / onHide() / onDestroy() | Lifecycle hooks |
Built-in CSS Classes
The base classes provide a set of built-in dark-theme CSS utility classes that can be used directly in renderContent():
| CSS Class | Description |
|---|---|
.row | Row container, 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 |
Comparison Summary
| Feature | Modal Dialog | Modeless Panel |
|---|---|---|
| Recommended base class | ModalDialogBase | ModelessPanelBase |
| Component base | base-dialog | Directly extends LitElement |
| Mount position | Engine.dialog!.appendChild() | document.body.appendChild() |
| CSS positioning | Managed by base-dialog | position: fixed |
| CAD interaction | Requires suspend() / resume() | Always interactive |
| Drag support | Built-in | Built into base class |
| Display control | startDialog() / close() | show() / hide() / toggle() |
| Suitable scenarios | Complex forms, picking required | Tool panels, real-time operations |
Complete Example: Dialog in Command Context
Shows how to correctly use suspend() / resume() in a command context for point and entity picking.
Step Overview
1. Create dialog class (extends LitElement)
2. Create command class (contains main() method)
3. Register command (CommandRegistry.regist)
4. Execute command (Engine.editor.executerWithOp)Command Class Example
import {
CommandDefinition, CommandRegistry, CommandOptions,
Engine, writeMessage
} from 'vjcad';
// Command class: start dialog in main()
class MyPickCommand {
async main() {
writeMessage("<br/>Starting pick dialog...");
// Create and start dialog
const dialog = new MyPickDialog();
const result = await dialog.startDialog();
// Handle result
if (result) {
writeMessage(`<br/>Selected ${result.entities.length} entities`);
if (result.distance !== null) {
writeMessage(`<br/>Measured distance: ${result.distance.toFixed(4)}`);
}
} else {
writeMessage("<br/>User canceled the operation");
}
}
}
// Register command
const cmdDef = new CommandDefinition(
'MYPICKDIALOG', // Command name
'Pick dialog example', // Command description
MyPickCommand, // Command class
new CommandOptions() // Command options
);
CommandRegistry.regist(cmdDef);
// Execute command (runs in command context)
await Engine.editor.executerWithOp('MYPICKDIALOG');Picking Methods Inside the Dialog Class
// Implement picking inside dialog class
async pickEntities() {
// 1. Pause dialog
this.baseDialog?.suspend();
try {
// 2. Call input API
const options = new SelectionInputOptions("Select entities:");
const result = await Engine.editor.getSelections(options);
// 3. Handle result
if (result.status === InputStatusEnum.OK) {
this.selectedEntities = result.value;
}
} finally {
// 4. Resume dialog (whether successful or canceled)
this.baseDialog?.resume();
this.requestUpdate();
}
}
async pickPoint() {
this.baseDialog?.suspend();
try {
const options = new PointInputOptions("Specify point:");
// Optional: show rubber-band line
if (this.startPoint) {
options.useBasePoint = true;
options.basePoint = new Point2D(this.startPoint.x, this.startPoint.y);
}
const result = await Engine.editor.getPoint(options);
if (result.status === InputStatusEnum.OK) {
this.pickedPoint = result.value;
}
} finally {
this.baseDialog?.resume();
this.requestUpdate();
}
}Key Points
| Key Point | Description |
|---|---|
| Must be in command context | Calling the dialog directly causes clicks to trigger the default command |
suspend() pauses the dialog | Allows the user to interact with the CAD interface |
resume() resumes the dialog | Put it in finally to ensure it always runs |
requestUpdate() | Refresh the dialog to display new data after resuming |
Engine.editor.getPoint() | Use Engine.editor methods in command context |