agentscape
Version:
Agentscape is a library for creating agent-based simulations. It provides a simple API for defining agents and their behavior, and for defining the environment in which the agents interact. Agentscape is designed to be flexible and extensible, allowing
312 lines (264 loc) • 9.91 kB
text/typescript
const DRAG_HANDLE_ICON = `
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4.5" cy="2.5" r=".6" fill="#000000" />
<circle cx="4.5" cy="4.5" r=".6" fill="#000000" />
<circle cx="4.5" cy="6.499" r=".6" fill="#000000" />
<circle cx="4.5" cy="8.499" r=".6" fill="#000000" />
<circle cx="4.5" cy="10.498" r=".6" fill="#000000" />
<circle cx="4.5" cy="12.498" r=".6" fill="#000000" />
<circle cx="6.5" cy="2.5" r=".6" fill="#000000" />
<circle cx="6.5" cy="4.5" r=".6" fill="#000000" />
<circle cx="6.5" cy="6.499" r=".6" fill="#000000" />
<circle cx="6.5" cy="8.499" r=".6" fill="#000000" />
<circle cx="6.5" cy="10.498" r=".6" fill="#000000" />
<circle cx="6.5" cy="12.498" r=".6" fill="#000000" />
<circle cx="8.499" cy="2.5" r=".6" fill="#000000" />
<circle cx="8.499" cy="4.5" r=".6" fill="#000000" />
<circle cx="8.499" cy="6.499" r=".6" fill="#000000" />
<circle cx="8.499" cy="8.499" r=".6" fill="#000000" />
<circle cx="8.499" cy="10.498" r=".6" fill="#000000" />
<circle cx="8.499" cy="12.498" r=".6" fill="#000000" />
<circle cx="10.499" cy="2.5" r=".6" fill="#000000" />
<circle cx="10.499" cy="4.5" r=".6" fill="#000000" />
<circle cx="10.499" cy="6.499" r=".6" fill="#000000" />
<circle cx="10.499" cy="8.499" r=".6" fill="#000000" />
<circle cx="10.499" cy="10.498" r=".6" fill="#000000" />
<circle cx="10.499" cy="12.498" r=".6" fill="#000000" />
</svg>
`
class DragPage extends HTMLElement {
public headerRef: HTMLDivElement
public titleRef: HTMLDivElement
public dragHandleRef: HTMLDivElement
private initWidth: number = 400
static get observedAttributes() {
return ['heading', 'key', 'minimized', 'color']
}
constructor() {
super()
}
public connectedCallback() {
const style = `
:root {
width: auto;
}
:host {
position: absolute;
z-index: 9;
background-color: #ffffff;
text-align: center;
border: 1px solid #000000;
width: auto;
}
:host([active]) {
z-index: 999;
}
:host([disabled]) #header {
background-color: #9E9E9E;
cursor: unset;
}
#header {
display: flex;
justify-content: space-between;
padding: 10px;
z-index: 10;
background-color: #2196F3;
color: #fff;
}
#drag-handle {
width: 20px;
height: 20px;
cursor: move;
}
#drag-handle svg {
width: 20px;
height: 20px;
}
#heading {
margin: 0 auto;
}
#controls {
display: inline-flex;
margin-left: 10px;
}
.btn {
line-height: 9px;
width: 10px;
height: 10px;
border: 1px solid black;
border-radius: 3px;
box-shadow: 1px 1px black;
cursor: pointer;
color: white;
}
.btn:not(:last-of-type) {
margin: 0 5px 0 0;
}
.btn:hover {
box-shadow: -1px -1px black;
border: 1px solid black;
color: white;
}
:host([disabled]) .btn, :host([disabled]) .btn:hover {
border: 1px solid #616161;
box-shadow: inset 1px 1px black;
color: #424242;
cursor: unset;
}
.btn:active {
box-shadow: inset 1px 1px black;
}
`
const template = document.createElement('template')
template.innerHTML = `
<style>${style}</style>
<div id="header">
<div id="drag-handle">${DRAG_HANDLE_ICON}</div>
<div id="heading">${this.heading}</div>
<div id="controls">
<div id="minimize" class="btn">-</div>
</div>
</div>
<slot id="content"></slot>
`
const shadowRoot = this.attachShadow({mode: 'open'})
shadowRoot.appendChild(template.content.cloneNode(true))
this.headerRef = this.shadowRoot!.querySelector<HTMLDivElement>('#header')!
this.titleRef = this.shadowRoot!.querySelector<HTMLDivElement>('#heading')!
this.dragHandleRef = this.shadowRoot!.querySelector<HTMLDivElement>('#drag-handle')!
this.restorePosition()
this.dragHandleRef.addEventListener('mousedown', this.beginElementDrag)
this.dragHandleRef.addEventListener('dblclick', this.handleDblCLick)
this.headerRef.querySelectorAll<HTMLDivElement>('.btn').forEach( (btnEl) => btnEl.addEventListener('click', this.handleBtnClick))
// set the initial width to the width of the content
this.initWidth = this.getBoundingClientRect().width
// update the z-index when the element is clicked
this.addEventListener('click', () => {
if (this.id !== 'canvas_main') {
this.style.zIndex = '3'
const otherPanes = Array.from(document.getElementsByTagName('drag-pane')).filter( (el: HTMLElement) => el !== this && el.id !== 'canvas_main')
otherPanes.forEach( (el: HTMLElement) => el.style.zIndex = '2')
}
})
}
public get heading(): string {
return this.getAttribute('heading') || ''
}
public set heading(newState: string) {
this.setAttribute('heading', newState)
}
public get minimized(): boolean {
return this.getAttribute('minimized') === 'true' || this.hasAttribute('minimized') && this.getAttribute('minimized') === ''
}
public set minimized(newState: boolean) {
if (newState) {
this.setAttribute('minimized', 'true')
} else {
this.removeAttribute('minimized')
}
}
public set color(newColor: string) {
this.setAttribute('color', newColor)
}
public get color(): string {
return this.getAttribute('color') || this.defaultHeaderColor
}
public get key(): string|undefined {
return this.getAttribute('key') || undefined
}
public set key(newValue: string|undefined){
if (newValue) {
this.setAttribute('key', newValue)
} else {
this.removeAttribute('key')
}
}
attributeChangedCallback(_name: string) {
if (_name === 'minimized') {
if (this.minimized) {
// this.style.height = '30px'
this.shadowRoot!.querySelector<HTMLDivElement>('#content')!.style.display = 'none'
this.style.width = this.initWidth + 'px'
} else {
// this.style.height = 'auto'
this.shadowRoot!.querySelector<HTMLDivElement>('#content')!.style.display = 'block'
this.style.width = this.initWidth + 'px'
}
}
}
private defaultHeaderColor: string = '#2196F3'
private handleBtnClick = (event: MouseEvent) => {
const el = event!.target as HTMLDivElement
switch (el.id) {
case 'close':
this.dispatchEvent(new Event('remove'))
this.remove()
break
case 'minimize':
this.minimized = !this.minimized
this.dispatchEvent(new Event('toggleminimize', {bubbles: true, composed: true}))
break
default:
break
}
}
private handleDblCLick = () => {
this.minimized = !this.minimized
this.dispatchEvent(new Event('toggleminimize', {bubbles: true, composed: true}))
}
private pos: {x: number, y: number} = {x:0, y:0}
private storePosition(){
if (this.key) {
window.localStorage.setItem(`drag-pane-${this.key}`, JSON.stringify(this.pos))
}
}
private restorePosition(){
if (this.key) {
const storeValue = window.localStorage.getItem(`drag-pane-${this.key}`)
if (storeValue) {
this.pos = JSON.parse(storeValue)
this.style.top = this.pos.y + 'px'
this.style.left = this.pos.x + 'px'
}
}
}
private didMove: boolean = false
// private moveThreshold: number = 5;
private beginElementDrag = (event: MouseEvent) => {
event.preventDefault()
this.setAttribute('active', 'true')
// get the mouse cursor position at startup:
this.pos.x = event.clientX
this.pos.y = event.clientY
document.onmouseup = this.endElementDrag
// call a function whenever the cursor moves:
document.onmousemove = this.elementDrag
}
private elementDrag = (event: MouseEvent) => {
event.preventDefault()
// calculate the new cursor position:
const pos1 = this.pos.x - event.clientX
const pos2 = this.pos.y - event.clientY
this.pos.x = event.clientX
this.pos.y = event.clientY
this.didMove = true
this.dispatchEvent(new Event('dragstart', {bubbles: true, composed: true}))
// set the element's new position:
const newPosition = {top: this.offsetTop - pos2, left: this.offsetLeft - pos1}
this.style.top = newPosition.top + 'px'
this.style.left = newPosition.left + 'px'
}
private endElementDrag = () => {
// release element
document.onmouseup = null
document.onmousemove = null
this.setAttribute('active', 'false')
if (this.didMove) {
this.storePosition()
this.dispatchEvent(new Event('dragend', {bubbles: true, composed: true}))
this.didMove = false
}
}
}
window.customElements.define('drag-pane', DragPage)
export default DragPage