@webwriter/geometry-cloze
Version:
Create and view geometry exercises with coloring, styling and labeling options.
212 lines (194 loc) • 6.33 kB
text/typescript
import '@webcomponents/scoped-custom-element-registry';
import { LitElementWw } from '@webwriter/lit';
import { PropertyValueMap, css, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { WwGeomContextMenu } from './components/context-menu/ww-geom-context-menu';
import { WwGeomToolbar } from './components/toolbar/ww-geom-toolbar';
import Shape from './data/elements/Shape';
import CanvasManager, { CanvasData } from './data/CanvasManager/CanvasManager';
import Objects from './data/helper/Objects';
import { LitElement } from 'lit';
import '@shoelace-style/shoelace/dist/themes/light.css';
import { WwGeomOptions } from './components/options/ww-geom-options';
/**
* A widget to create and view geometry exercises.
*/
export class WwGeometryCloze extends LitElementWw {
accessor canvas!: HTMLCanvasElement;
accessor contextMenu!: WwGeomContextMenu;
manager: CanvasManager | null = null;
accessor elements: CanvasData['children'];
accessor mode: CanvasData['mode'] = 'select';
accessor abstractRightAngle: CanvasData['abstractRightAngle'] = false;
accessor showGrid: CanvasData['showGrid'] = true;
accessor snap: CanvasData['snapping'] = true;
render() {
return html`<div class="wrapper">
${
this.isContentEditable
? html`<ww-geom-toolbar
mode=${this.mode}
-change=${(e: CustomEvent<{ mode: InteractionMode }>) => {
this.mode = e.detail.mode;
if (!this.manager) return;
this.manager.mode = e.detail.mode;
}}></ww-geom-toolbar>`
: ''
}
<canvas width="1000" height="700" =${()=>{this.dispatchEvent(new Event("focus"))}}></canvas>
<ww-geom-context-menu></ww-geom-context-menu>
</div>
</div>
<ww-geom-options part="options" .manager=${
this.manager
}></ww-geom-options>`;
}
private onBlur() {
this.contextMenu?.close();
}
private onClick() {
this.contextMenu?.close();
}
protected updated(
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
if (changedProperties.has('elements')) {
if (!this.manager || !this.elements) return;
const exportData = this.manager.export();
if (!Objects.deepEqual(exportData.children, this.elements))
this.manager.import({
children: this.elements
});
} else if (changedProperties.has('mode')) {
if (this.manager) this.manager.mode = this.mode;
} else if (changedProperties.has('abstractRightAngle'.toLowerCase())) {
if (this.manager)
this.manager.abstractRightAngle = this.abstractRightAngle;
} else if (changedProperties.has('showGrid'.toLowerCase())) {
if (this.manager) this.manager.toggleGrid(this.showGrid);
} else if (changedProperties.has('snap')) {
if (this.manager) this.manager.toggleSnapping(this.snap);
}
}
private onCanvasValueChange(value: CanvasData) {
this.elements = value.children;
this.mode = value.mode;
this.abstractRightAngle = value.abstractRightAngle;
this.showGrid = value.showGrid;
this.snap = value.snapping;
}
firstUpdated() {
this.addEventListener('blur', this.onBlur.bind(this));
this.addEventListener('click', this.onClick.bind(this));
if (this.canvas) {
if (this.manager) {
console.warn('Prevented creating multiple CanvasManager');
return;
}
this.manager = new CanvasManager(
this.canvas,
this.renderRoot as HTMLElement,
this.contextMenu
);
this.manager.addUpdateListener(this.onCanvasValueChange.bind(this));
if (this.elements) {
this.manager.import({
children: this.elements,
mode: this.mode
});
} else {
const polygon = Shape.createPolygon(this.manager, [
{ x: 200, y: 200, name: 'top left' },
{ x: 500, y: 200, name: 'top right' },
{
x: 600,
y: 300,
name: 'middle right'
},
{ x: 500, y: 500, name: 'bottom right' },
{ x: 200, y: 500, name: 'bottom left' }
]);
this.manager.addChild(polygon);
}
} else console.warn('No canvas context');
}
disconnectedCallback(): void {
this.removeEventListener('blur', this.onBlur);
this.removeEventListener('click', this.onClick);
if (this.manager) {
this.manager.removeUpdateListener(this.onCanvasValueChange);
this.manager.unmount();
}
super.disconnectedCallback();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true
};
public static get scopedElements() {
return {
'ww-geom-toolbar': WwGeomToolbar,
'ww-geom-context-menu': WwGeomContextMenu,
'ww-geom-options': WwGeomOptions
};
}
static styles = css`
:host {
outline: none;
z-index: 10000000;
position: relative;
border-width: 2px;
border-style: solid;
border-radius: 5px;
border-color: #6a6a6a;
}
.wrapper {
margin-top: 0;
position: relative;
outline: none;
}
canvas {
aspect-ratio: 10 / 7;
width: calc(100% - 2px);
box-sizing: border-box;
}
:host(:not([contenteditable='true']):not([contenteditable=''])) canvas {
pointer-events: none;
}
:host(:not([contenteditable='true']):not([contenteditable='']))
ww-geom-options {
display: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
'ww-geometry-cloze': WwGeometryCloze;
}
}