import { Transition, Node, StackOperation } from '../automata'; import { html } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; import { biDatabase, biDatabaseAdd, biDatabaseDash, biDatabaseSlash, biTrash } from '../styles/icons'; import { AutomatonComponent } from 'index'; import { classMap } from 'lit/directives/class-map.js'; import { msg } from '@lit/localize'; import { Logger } from '@u/logger'; /** * Represents a context menu component that provides options for interacting with nodes and edges. */ export class ContextMenu { private selected: { data: Transition | Node; type: 'Node' | 'Transition' | undefined; updateFn: Function; deleteFn: Function; } = { data: { id: 0, label: '', initial: false, final: false }, type: undefined, updateFn: () => {}, deleteFn: () => {}, }; private position = { x: 0, y: 0 }; private translate = { x: '-100%', y: '-100%' }; private visible = false; public requestUpdate: () => void = () => {}; constructor(private parentComponent: AutomatonComponent) {} public render() { return html`
${this.selected?.type === 'Node' ? this.nodeContextMenu() : null} ${this.selected?.type === 'Transition' ? this.edgeContextMenu(!!(this.selected.data as Transition).stackOperations) : null}
`; } private nodeContextMenu() { return html`
${msg("Node")} ${this.selected?.data.label} { this.selected.deleteFn(); this.hide(); }} style=${styleMap({ display: this.parentComponent.settings.permissions.node.delete ? 'block' : 'none', })} >${biTrash}
{ this.selected.updateFn({ ...this.selected.data, label: e.target.value }); this.selected.data = { ...this.selected.data, label: e.target.value }; }} ?disabled=${!this.parentComponent.settings.permissions.node.change} autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" >
{ Logger.log('initial', e.target.checked); this.selected.updateFn({ ...this.selected.data, initial: e.target.checked }); this.selected.data = { ...this.selected.data, initial: e.target.checked }; }} name="initial" >${msg("Initial")} { this.selected.updateFn({ ...this.selected.data, final: e.target.checked }); this.selected.data = { ...this.selected.data, final: e.target.checked }; }} name="final" >${msg("Final")}
`; } private edgeContextMenu(isPda: boolean = false) { let transition = this.selected?.data as Transition; let stackOperations = transition.stackOperations as StackOperation[] || []; return html`
${msg("Edge")} ${transition.label} { this.selected.deleteFn(); this.hide(); }} style=${styleMap({ display: this.parentComponent.settings.permissions.edge.delete ? 'block' : 'none', })} >${biTrash}
${transition.symbols.map( (symbol, i) => html`
${this.parentComponent.forcedAlphabet.length > 0 ? html` { Logger.log(e.target.value); const symbols = transition.symbols; symbols[i] = e.target.value; this.selected.updateFn({ ...transition, symbols }); transition = { ...transition, symbols }; }} name="symbol" > ${this.parentComponent.forcedAlphabet.map( (symbol) => html`${symbol}` )} ` : html` { Logger.log(e.target.value); const symbols = transition.symbols; symbols[i] = e.target.value; this.selected.updateFn({ ...transition, symbols }); transition = { ...transition, symbols }; }} autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" > `} ${isPda ? html` { const operation = { operation: stackOperations[i].operation, symbol: e.target.value, condition: stackOperations[i].condition, }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" > { const operation = { operation: stackOperations[i].operation, symbol: stackOperations[i].symbol, condition: e.target.value, }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" > { const operation = { operation: 'push', symbol: stackOperations[i].symbol, condition: stackOperations[i].condition, }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} >${biDatabaseAdd} { const operation = { operation: 'pop', symbol: stackOperations[i].symbol, condition: stackOperations[i].condition, }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} >${biDatabaseDash} { const operation = { operation: 'empty', symbol: '', condition: '', }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} >${biDatabaseSlash} { const operation = { operation: 'none', symbol: '', condition: stackOperations[i].condition, }; stackOperations[i] = operation as StackOperation; this.selected.updateFn({ ...transition, stackOperations: stackOperations }); transition = { ...transition, stackOperations: stackOperations }; }} >${biDatabase} ` : ''} { const symbols = transition.symbols; symbols.splice(i, 1); this.selected.updateFn({ ...transition, symbols }); transition = { ...transition, symbols }; if (symbols.length === 0) { this.selected.deleteFn(); this.hide(); } }} style=${styleMap({ display: this.parentComponent.settings.permissions.edge.delete ? 'block' : 'none', })} >${biTrash}
` )} { const symbols = transition.symbols; symbols.push(''); if (isPda) { stackOperations.push({ operation: 'none', symbol: '', condition: '' }); this.selected.updateFn({ ...transition, stackOperations, symbols }); transition = { ...transition, stackOperations, symbols }; } else { this.selected.updateFn({ ...transition, symbols }); transition = { ...transition, symbols }; } }} style=${styleMap({ display: this.parentComponent.settings.permissions.edge.add ? 'block' : 'none', })}> ${msg("Add symbol")}
${transition.from === transition.to ? html` { const value = e.target.value; this.selected.updateFn({ ...transition, selfReference: { angle: ((450 - value) % 360) * (Math.PI / 180) }, }); }} >` : html` { const value = e.target.value; this.selected.updateFn({ ...transition, smooth: { type: value < 0 ? 'curvedCCW' : 'curvedCW', roundness: Math.abs(value / 100) }, }); }} >` } `; } public setData(data: Node | Transition, type: 'Node' | 'Transition', updateFn: Function, deleteFn: Function) { this.selected = { data, type, updateFn, deleteFn }; this.requestUpdate(); } public setPosition({ x, y }: { x: number; y: number }) { this.position = { x, y }; const totalWidth = this.parentComponent.shadowRoot?.getElementById("graphCanvas")?.clientWidth || 0; const totalHeight = this.parentComponent.shadowRoot?.getElementById("graphCanvas")?.clientHeight || 0; this.translate.x = x > totalWidth / 2 ? '-100%' : '0%'; this.translate.y = y > totalHeight / 2 ? '-100%' : '0%'; this.requestUpdate(); } public show() { this.visible = true; this.requestUpdate(); } public hide() { this.visible = false; this.requestUpdate(); } public isVisible() { return this.visible; } public toggle() { this.visible = !this.visible; this.requestUpdate(); } public blur() { this.hide(); this.requestUpdate(); } }