cm-chessboard 8.7.8 → 8.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,4 +28,22 @@
28
28
  stroke-linecap: round;
29
29
  opacity: 0.5; }
30
30
 
31
+ /* custom colors for RightClickAnnotator */
32
+ .cm-chessboard .arrow-green .arrow-head {
33
+ fill: #2e7d32; /* green */
34
+ fill-rule: nonzero;
35
+ fill-opacity: 1; }
36
+ .cm-chessboard .arrow-green .arrow-line {
37
+ stroke: #2e7d32;
38
+ stroke-linecap: round;
39
+ opacity: 0.5; }
40
+ .cm-chessboard .arrow-orange .arrow-head {
41
+ fill: #ff9800; /* orange */
42
+ fill-rule: nonzero;
43
+ fill-opacity: 1; }
44
+ .cm-chessboard .arrow-orange .arrow-line {
45
+ stroke: #ff9800;
46
+ stroke-linecap: round;
47
+ opacity: 0.5; }
48
+
31
49
  /*# sourceMappingURL=arrows.css.map */
@@ -34,4 +34,14 @@
34
34
  fill: black;
35
35
  opacity: 0.2; }
36
36
 
37
- /*# sourceMappingURL=markers.css.map */
37
+ /* custom colors for RightClickAnnotator */
38
+ .cm-chessboard .markers .marker.marker-circle-green {
39
+ stroke: #2e7d32; /* green */
40
+ stroke-width: 3px;
41
+ opacity: 0.4; }
42
+ .cm-chessboard .markers .marker.marker-circle-orange {
43
+ stroke: #ff9800; /* orange */
44
+ stroke-width: 3px;
45
+ opacity: 0.4; }
46
+
47
+ /*# sourceMappingURL=markers.css.map */
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>cm-chessboard</title>
6
+ <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
7
+ <link rel="stylesheet" href="../styles/examples.css"/>
8
+ <link rel="stylesheet" href="../../assets/chessboard.css"/>
9
+ <link rel="stylesheet" href="../../assets/extensions/markers/markers.css"/>
10
+ <link rel="stylesheet" href="../../assets/extensions/arrows/arrows.css"/>
11
+ </head>
12
+ <body>
13
+ <h1><a href="../..">cm-chessboard</a></h1>
14
+ <h2>Example: RightClickAnnotator extension</h2>
15
+ <p>Right-click to toggle green markers. Right-click + drag to draw green arrows. Use Alt for blue, Shift for red, Shift+Alt for orange.</p>
16
+ <div class="board" id="board"></div>
17
+ <script type="module">
18
+ import {Chessboard} from "../../src/Chessboard.js"
19
+ import {FEN} from "../../src/model/Position.js"
20
+ import {RightClickAnnotator} from "../../src/extensions/right-click-annotator/RightClickAnnotator.js"
21
+
22
+ const board = new Chessboard(document.getElementById("board"), {
23
+ position: FEN.start,
24
+ assetsUrl: "../../assets/",
25
+ extensions: [{class: RightClickAnnotator}]
26
+ })
27
+ </script>
28
+ </body>
29
+ </html>
package/index.html CHANGED
@@ -33,6 +33,7 @@ It works, it is cool and it is easy to use. 👍</p>
33
33
  <li><a href="examples/extensions/html-layer-extension.html">HTML Layer extension</a></li>
34
34
  <li><a href="examples/extensions/promotion-dialog-extension.html">PromotionDialog extension</a></li>
35
35
  <li><a href="examples/extensions/accessibility-extension.html">Accessibility extension</a></li>
36
+ <li><a href="examples/extensions/right-click-annotator.html">RightClickAnnotator extension</a></li>
36
37
  </ul>
37
38
  </div>
38
39
  <h2>Chessboard</h2>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-chessboard",
3
- "version": "8.7.8",
3
+ "version": "8.7.10",
4
4
  "description": "A JavaScript chessboard which is lightweight, ES6 module based, responsive, SVG rendered and without dependencies.",
5
5
  "keywords": [
6
6
  "chess",
@@ -23,7 +23,13 @@ export class Arrows extends Extension {
23
23
  this.onRedrawBoard()
24
24
  })
25
25
  this.props = {
26
- sprite: "extensions/arrows/arrows.svg"
26
+ sprite: "extensions/arrows/arrows.svg",
27
+ // offset factors relative to half the square size (0.0 .. 1.0)
28
+ // 0.0 = start/end in the center; 1.0 = at the outer edge of the inscribed circle (half the square)
29
+ // default chosen to preserve previous appearance where radius = 0.36 * min(squareWidth, squareHeight)
30
+ // which corresponds to 0.72 of half the square size (0.36 / 0.5 = 0.72)
31
+ offsetFrom: 0,
32
+ offsetTo: 0.72
27
33
  }
28
34
  Object.assign(this.props, props)
29
35
  if (this.chessboard.props.assetsCache) {
@@ -70,10 +76,27 @@ export class Arrows extends Extension {
70
76
  href: `${spriteUrl}#${arrow.type.slice}`,
71
77
  })
72
78
 
73
- const x1 = ptFrom.x + view.squareWidth / 2
74
- const x2 = ptTo.x + view.squareHeight / 2
75
- const y1 = ptFrom.y + view.squareWidth / 2
76
- const y2 = ptTo.y + view.squareHeight / 2
79
+ // Compute centers of start and end squares
80
+ const cx1 = ptFrom.x + view.squareWidth / 2
81
+ const cy1 = ptFrom.y + view.squareHeight / 2
82
+ const cx2 = ptTo.x + view.squareWidth / 2
83
+ const cy2 = ptTo.y + view.squareHeight / 2
84
+
85
+ // Offset the line so it starts/ends at the edge of an invisible circle inside each square
86
+ const dx = cx2 - cx1
87
+ const dy = cy2 - cy1
88
+ const len = Math.hypot(dx, dy) || 1
89
+ const ux = dx / len
90
+ const uy = dy / len
91
+ // compute radii from props: factor relative to half of the square size
92
+ const halfMin = Math.min(view.squareWidth, view.squareHeight) / 2
93
+ const clamp01 = (v) => Math.max(0, Math.min(1, v))
94
+ const rFrom = halfMin * clamp01(this.props.offsetFrom)
95
+ const rTo = halfMin * clamp01(this.props.offsetTo)
96
+ const x1 = cx1 + ux * rFrom
97
+ const y1 = cy1 + uy * rFrom
98
+ const x2 = cx2 - ux * rTo
99
+ const y2 = cy2 - uy * rTo
77
100
 
78
101
  const width = ((view.scalingX + view.scalingY) / 2) * 4
79
102
  let lineFill = Svg.addElement(arrowsGroup, "line")
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Extension: RightClickAnnotator
3
+ * Combines Arrows and Markers to draw/toggle arrows and circle markers with right-click + modifiers.
4
+ * Colors:
5
+ * - Green: Right-click
6
+ * - Blue: Alt + Right-click
7
+ * - Red: Shift + Right-click
8
+ * - Orange: Shift + Alt + Right-click
9
+ * Redrawing the same arrow or marker removes it.
10
+ */
11
+ import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
12
+ import {Arrows, ARROW_TYPE} from "../arrows/Arrows.js"
13
+ import {Markers, MARKER_TYPE} from "../markers/Markers.js"
14
+ import {Svg} from "../../lib/Svg.js"
15
+
16
+ // Define persistent type objects for matching (strict equality used in core extensions)
17
+ const ARROW_GREEN = {class: "arrow-green", slice: "arrowDefault", headSize: 7}
18
+ const ARROW_ORANGE = {class: "arrow-orange", slice: "arrowDefault", headSize: 7}
19
+
20
+ const CIRCLE_GREEN = {class: "marker-circle-green", slice: "markerCircle"}
21
+ const CIRCLE_ORANGE = {class: "marker-circle-orange", slice: "markerCircle"}
22
+
23
+ export class RightClickAnnotator extends Extension {
24
+ constructor(chessboard, props = {}) {
25
+ super(chessboard)
26
+ this.props = props || {}
27
+
28
+ // Ensure Arrows and Markers extensions are available
29
+ if (!this.chessboard.getExtension(Arrows)) {
30
+ this.chessboard.addExtension(Arrows)
31
+ }
32
+ if (!this.chessboard.getExtension(Markers)) {
33
+ this.chessboard.addExtension(Markers)
34
+ }
35
+
36
+ this.onContextMenu = this.onContextMenu.bind(this)
37
+ this.onMouseDown = this.onMouseDown.bind(this)
38
+ this.onMouseMove = this.onMouseMove.bind(this)
39
+ this.onMouseUp = this.onMouseUp.bind(this)
40
+
41
+ this.dragStart = undefined // {square, modifiers}
42
+ this.previewGroup = undefined
43
+ this.previewActiveTo = undefined // cache last to-square for preview
44
+
45
+ this.chessboard.context.addEventListener("contextmenu", this.onContextMenu)
46
+ this.chessboard.context.addEventListener("mousedown", this.onMouseDown)
47
+ this.chessboard.context.addEventListener("mousemove", this.onMouseMove)
48
+ this.chessboard.context.addEventListener("mouseup", this.onMouseUp)
49
+ this.chessboard.context.addEventListener("mouseleave", this.onMouseUp)
50
+
51
+ // ensure preview group exists after redraws
52
+ this.registerExtensionPoint(EXTENSION_POINT.afterRedrawBoard, () => {
53
+ this.ensurePreviewGroup()
54
+ })
55
+
56
+ this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
57
+ this.chessboard.context.removeEventListener("contextmenu", this.onContextMenu)
58
+ this.chessboard.context.removeEventListener("mousedown", this.onMouseDown)
59
+ this.chessboard.context.removeEventListener("mousemove", this.onMouseMove)
60
+ this.chessboard.context.removeEventListener("mouseup", this.onMouseUp)
61
+ this.chessboard.context.removeEventListener("mouseleave", this.onMouseUp)
62
+ })
63
+ // create preview group now
64
+ this.ensurePreviewGroup()
65
+ }
66
+
67
+ onContextMenu(event) {
68
+ // prevent default context menu on chessboard
69
+ event.preventDefault()
70
+ }
71
+
72
+ onMouseDown(event) {
73
+ // right button only
74
+ if (event.button !== 2) {
75
+ return
76
+ }
77
+ const square = this.findSquareFromEvent(event)
78
+ if (!square) {
79
+ return
80
+ }
81
+ this.dragStart = {
82
+ square,
83
+ modifiers: {
84
+ alt: event.altKey,
85
+ shift: event.shiftKey
86
+ }
87
+ }
88
+ }
89
+
90
+ onMouseUp(event) {
91
+ // clear preview regardless of button, but only act on right-button release
92
+ const start = this.dragStart
93
+ this.dragStart = undefined
94
+ this.clearPreview()
95
+ if (!start || event.button !== 2) {
96
+ return
97
+ }
98
+ const endSquare = this.findSquareFromEvent(event) || start.square
99
+ const colorKey = this.modifiersToColorKey(start.modifiers)
100
+ const {arrowType, circleType} = this.typesForColorKey(colorKey)
101
+
102
+ if (start.square && endSquare && start.square !== endSquare) {
103
+ // toggle arrow
104
+ const existing = this.chessboard.getArrows(arrowType, start.square, endSquare)
105
+ if (existing && existing.length > 0) {
106
+ this.chessboard.removeArrows(arrowType, start.square, endSquare)
107
+ } else {
108
+ this.chessboard.removeArrows(undefined, start.square, endSquare)
109
+ this.chessboard.addArrow(arrowType, start.square, endSquare)
110
+ }
111
+ } else if (start.square) {
112
+ // toggle marker on start square
113
+ const existingMarkers = this.chessboard.getMarkers(circleType, start.square)
114
+ if (existingMarkers && existingMarkers.length > 0) {
115
+ this.chessboard.removeMarkers(circleType, start.square)
116
+ } else {
117
+ this.chessboard.removeMarkers(undefined, start.square)
118
+ this.chessboard.addMarker(circleType, start.square)
119
+ }
120
+ }
121
+ }
122
+
123
+ findSquareFromEvent(event) {
124
+ const target = /** @type {HTMLElement} */(event.target)
125
+ if (!target) return undefined
126
+ if (target.getAttribute && target.getAttribute("data-square")) {
127
+ return target.getAttribute("data-square")
128
+ }
129
+ const el = target.closest && target.closest("[data-square]")
130
+ return el ? el.getAttribute("data-square") : undefined
131
+ }
132
+
133
+ ensurePreviewGroup() {
134
+ // create or reset preview group used for transient arrow drawing
135
+ if (!this.chessboard?.view?.markersTopLayer) {
136
+ return
137
+ }
138
+ if (this.previewGroup && this.previewGroup.parentNode) {
139
+ // keep and clear existing
140
+ this.clearPreview()
141
+ return
142
+ }
143
+ this.previewGroup = Svg.addElement(this.chessboard.view.markersTopLayer, "g", {class: "right-click-annotator-preview"})
144
+ }
145
+
146
+ clearPreview() {
147
+ if (!this.previewGroup) return
148
+ while (this.previewGroup.firstChild) {
149
+ this.previewGroup.removeChild(this.previewGroup.firstChild)
150
+ }
151
+ this.previewActiveTo = undefined
152
+ }
153
+
154
+ onMouseMove(event) {
155
+ if (!this.dragStart) {
156
+ return
157
+ }
158
+ // Only show preview for right-button drag if still pressed (best-effort); some browsers may not keep buttons state reliably
159
+ // We rely mainly on our dragStart flag and clear on mouseup/mouseleave
160
+ const toSquare = this.findSquareFromEvent(event)
161
+ if (!toSquare || toSquare === this.dragStart.square) {
162
+ this.clearPreview()
163
+ return
164
+ }
165
+ if (this.previewActiveTo === toSquare) {
166
+ return // no change
167
+ }
168
+ this.previewActiveTo = toSquare
169
+ const colorKey = this.modifiersToColorKey(this.dragStart.modifiers)
170
+ const {arrowType} = this.typesForColorKey(colorKey)
171
+ this.drawPreviewArrow(this.dragStart.square, toSquare, arrowType)
172
+ }
173
+
174
+ drawPreviewArrow(from, to, type) {
175
+ this.ensurePreviewGroup()
176
+ this.clearPreview()
177
+ const view = this.chessboard.view
178
+ const arrowsGroup = Svg.addElement(this.previewGroup, "g", {class: "arrow " + type.class})
179
+ const ptFrom = view.squareToPoint(from)
180
+ const ptTo = view.squareToPoint(to)
181
+ const defs = Svg.addElement(arrowsGroup, "defs")
182
+ const id = "arrow-preview-" + from + to
183
+ const marker = Svg.addElement(defs, "marker", {
184
+ id: id,
185
+ markerWidth: type.headSize,
186
+ markerHeight: type.headSize,
187
+ refX: 20,
188
+ refY: 20,
189
+ viewBox: "0 0 40 40",
190
+ orient: "auto",
191
+ class: "arrow-head",
192
+ })
193
+ const spriteUrl = this.chessboard.props.assetsCache ? "" : this.chessboard.getExtension(Arrows)?.getSpriteUrl?.() || this.chessboard.props.assetsUrl + "extensions/arrows/arrows.svg"
194
+ Svg.addElement(marker, "use", {href: `${spriteUrl}#${type.slice}`})
195
+ // Compute centers of start and end squares
196
+ const cx1 = ptFrom.x + view.squareWidth / 2
197
+ const cy1 = ptFrom.y + view.squareHeight / 2
198
+ const cx2 = ptTo.x + view.squareWidth / 2
199
+ const cy2 = ptTo.y + view.squareHeight / 2
200
+
201
+ // Offset the line so it starts/ends at the edge of an invisible circle inside each square
202
+ const dx = cx2 - cx1
203
+ const dy = cy2 - cy1
204
+ const len = Math.hypot(dx, dy) || 1
205
+ const ux = dx / len
206
+ const uy = dy / len
207
+ // get offsets from Arrows extension props to match final arrow rendering
208
+ const arrowsExt = this.chessboard.getExtension(Arrows)
209
+ const offsetFrom = arrowsExt?.props?.offsetFrom ?? 0.72
210
+ const offsetTo = arrowsExt?.props?.offsetTo ?? 0.72
211
+ const halfMin = Math.min(view.squareWidth, view.squareHeight) / 2
212
+ const clamp01 = (v) => Math.max(0, Math.min(1, v))
213
+ const rFrom = halfMin * clamp01(offsetFrom)
214
+ const rTo = halfMin * clamp01(offsetTo)
215
+ const x1 = cx1 + ux * rFrom
216
+ const y1 = cy1 + uy * rFrom
217
+ const x2 = cx2 - ux * rTo
218
+ const y2 = cy2 - uy * rTo
219
+
220
+ const width = ((view.scalingX + view.scalingY) / 2) * 4
221
+ let lineFill = Svg.addElement(arrowsGroup, "line")
222
+ lineFill.setAttribute('x1', x1.toString())
223
+ lineFill.setAttribute('x2', x2.toString())
224
+ lineFill.setAttribute('y1', y1.toString())
225
+ lineFill.setAttribute('y2', y2.toString())
226
+ lineFill.setAttribute('class', 'arrow-line')
227
+ lineFill.setAttribute("marker-end", "url(#" + id + ")")
228
+ lineFill.setAttribute('stroke-width', width + "px")
229
+ }
230
+
231
+ modifiersToColorKey(modifiers) {
232
+ if (modifiers.shift && modifiers.alt) return "orange"
233
+ if (modifiers.shift) return "red"
234
+ if (modifiers.alt) return "blue"
235
+ return "green"
236
+ }
237
+
238
+ typesForColorKey(colorKey) {
239
+ switch (colorKey) {
240
+ case "blue":
241
+ return {arrowType: ARROW_TYPE.default, circleType: MARKER_TYPE.circlePrimary}
242
+ case "red":
243
+ return {arrowType: ARROW_TYPE.danger, circleType: MARKER_TYPE.circleDanger}
244
+ case "orange":
245
+ return {arrowType: ARROW_ORANGE, circleType: CIRCLE_ORANGE}
246
+ case "green":
247
+ default:
248
+ return {arrowType: ARROW_GREEN, circleType: CIRCLE_GREEN}
249
+ }
250
+ }
251
+ }