cm-chessboard
Version:
A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.
227 lines (202 loc) • 8.47 kB
JavaScript
/**
* Extension: RightClickAnnotator
* Combines Arrows and Markers to draw/toggle arrows and circle markers with right-click + modifiers.
* Colors:
* - Green: Right-click
* - Blue: Alt + Right-click
* - Red: Shift + Right-click
* - Orange: Shift + Alt + Right-click
* Redrawing the same arrow or marker removes it.
*/
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
import {Arrows} from "../arrows/Arrows.js"
import {Markers} from "../markers/Markers.js"
import {Svg} from "../../lib/Svg.js"
export const ARROW_TYPE = {
success: { class: "arrow-success"},
warning: { class: "arrow-warning"},
info: { class: "arrow-info"},
danger: { class: "arrow-danger"}
}
export const MARKER_TYPE = {
success: {class: "marker-circle-success", slice: "markerCircle"},
warning: {class: "marker-circle-warning", slice: "markerCircle"},
info: {class: "marker-circle-info", slice: "markerCircle"},
danger: {class: "marker-circle-danger", slice: "markerCircle"},
}
export class RightClickAnnotator extends Extension {
/** @constructor */
constructor(chessboard, props = {}) {
super(chessboard)
this.props = props || {}
// Ensure Arrows and Markers extensions are available
if (!this.chessboard.getExtension(Arrows)) {
this.chessboard.addExtension(Arrows)
}
if (!this.chessboard.getExtension(Markers)) {
this.chessboard.addExtension(Markers)
}
this.onContextMenu = this.onContextMenu.bind(this)
this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseMove = this.onMouseMove.bind(this)
this.onMouseUp = this.onMouseUp.bind(this)
this.dragStart = undefined // {square, modifiers}
this.previewActiveTo = undefined // cache last to-square for preview
this.chessboard.context.addEventListener("contextmenu", this.onContextMenu)
this.chessboard.context.addEventListener("mousedown", this.onMouseDown)
this.chessboard.context.addEventListener("mousemove", this.onMouseMove)
this.chessboard.context.addEventListener("mouseup", this.onMouseUp)
this.chessboard.context.addEventListener("mouseleave", this.onMouseUp)
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
this.chessboard.context.removeEventListener("contextmenu", this.onContextMenu)
this.chessboard.context.removeEventListener("mousedown", this.onMouseDown)
this.chessboard.context.removeEventListener("mousemove", this.onMouseMove)
this.chessboard.context.removeEventListener("mouseup", this.onMouseUp)
this.chessboard.context.removeEventListener("mouseleave", this.onMouseUp)
})
// register public API
this.chessboard.getAnnotations = this.getAnnotations.bind(this.chessboard)
this.chessboard.setAnnotations = this.setAnnotations.bind(this.chessboard)
}
getAnnotations() {
return {
arrows: this.chessboard.getArrows(),
markers: this.chessboard.getMarkers()
}
}
// Remove only the arrows/markers created by this extension, leaving annotations from other sources untouched.
removeOwnArrows(from = undefined, to = undefined) {
for (const arrowType of Object.values(ARROW_TYPE)) {
this.chessboard.removeArrows(arrowType, from, to)
}
}
removeOwnMarkers(square = undefined) {
for (const markerType of Object.values(MARKER_TYPE)) {
this.chessboard.removeMarkers(markerType, square)
}
}
setAnnotations(annotations) {
this.removeOwnArrows()
this.removeOwnMarkers()
if (annotations.arrows) {
for (const arrow of annotations.arrows) {
this.chessboard.addArrow(arrow.type, arrow.from, arrow.to)
}
}
if (annotations.markers) {
for (const marker of annotations.markers) {
this.chessboard.addMarker(marker.type, marker.square)
}
}
}
onContextMenu(event) {
event.preventDefault()
}
onMouseDown(event) {
// right button only
if (event.button !== 2) {
return
}
const square = this.findSquareFromEvent(event)
if (!square) {
return
}
this.dragStart = {
square,
modifiers: {
alt: event.altKey,
shift: event.shiftKey || event.ctrlKey
}
}
}
onMouseUp(event) {
// clear preview regardless of button, but only act on right-button release
this.removePreviewArrow()
const start = this.dragStart
this.dragStart = undefined
if (!start || event.button !== 2) {
return
}
const endSquare = this.findSquareFromEvent(event) || start.square
const colorKey = this.modifiersToColorKey(start.modifiers)
const {arrowType, circleType} = this.typesForColorKey(colorKey)
if (start.square && endSquare && start.square !== endSquare) {
// toggle arrow
const existing = this.chessboard.getArrows(arrowType, start.square, endSquare)
if (existing && existing.length > 0) {
this.chessboard.removeArrows(arrowType, start.square, endSquare)
} else {
this.removeOwnArrows(start.square, endSquare)
this.chessboard.addArrow(arrowType, start.square, endSquare)
}
} else if (start.square) {
// toggle marker on start square
const existingMarkers = this.chessboard.getMarkers(circleType, start.square)
if (existingMarkers && existingMarkers.length > 0) {
this.chessboard.removeMarkers(circleType, start.square)
} else {
this.removeOwnMarkers(start.square)
this.chessboard.addMarker(circleType, start.square)
}
}
}
findSquareFromEvent(event) {
const target = /** @type {HTMLElement} */(event.target)
if (!target) return undefined
if (target.getAttribute && target.getAttribute("data-square")) {
return target.getAttribute("data-square")
}
const el = target.closest && target.closest("[data-square]")
return el ? el.getAttribute("data-square") : undefined
}
onMouseMove(event) {
if (!this.dragStart) {
return
}
// Only show preview for right-button drag if still pressed (best-effort); some browsers may not keep buttons state reliably
// We rely mainly on our dragStart flag and clear on mouseup/mouseleave
const toSquare = this.findSquareFromEvent(event)
if (!toSquare || toSquare === this.dragStart.square) {
return
}
if (this.previewActiveTo === toSquare) {
return // no change
}
this.previewActiveTo = toSquare
const colorKey = this.modifiersToColorKey(this.dragStart.modifiers)
const {arrowType} = this.typesForColorKey(colorKey)
this.drawPreviewArrow(this.dragStart.square, toSquare, arrowType)
}
drawPreviewArrow(from, to, type) {
if(!this.previewArrowType) {
this.previewArrowType = {...type}
}
this.chessboard.removeArrows(this.previewArrowType)
this.chessboard.addArrow(this.previewArrowType, from, to)
}
removePreviewArrow() {
if(this.previewArrowType) {
this.chessboard.removeArrows(this.previewArrowType)
this.previewArrowType = undefined
}
}
modifiersToColorKey(modifiers) {
if (modifiers.shift && modifiers.alt) return "warning"
if (modifiers.shift) return "danger"
if (modifiers.alt) return "info"
return "success"
}
typesForColorKey(colorKey) {
switch (colorKey) {
case "info":
return {arrowType: ARROW_TYPE.info, circleType: MARKER_TYPE.info}
case "danger":
return {arrowType: ARROW_TYPE.danger, circleType: MARKER_TYPE.danger}
case "warning":
return {arrowType: ARROW_TYPE.warning, circleType: MARKER_TYPE.warning}
case "success":
default:
return {arrowType: ARROW_TYPE.success, circleType: MARKER_TYPE.success}
}
}
}