besogo
Version:
Embeddable SGF player for the game of Go (aka Weiqi, Baduk)
1,289 lines (1,145 loc) • 135 kB
JavaScript
(function() {
'use strict';
var besogo = window.besogo = window.besogo || {}; // Establish our namespace
besogo.VERSION = '0.0.2-alpha';
besogo.create = function(container, options) {
var editor, // Core editor object
resizer, // Auto-resizing function
boardDiv, // Board display container
panelsDiv, // Parent container of panel divs
makers = { // Map to panel creators
control: besogo.makeControlPanel,
names: besogo.makeNamesPanel,
comment: besogo.makeCommentPanel,
tool: besogo.makeToolPanel,
tree: besogo.makeTreePanel,
file: besogo.makeFilePanel
},
insideText = container.textContent || container.innerText || '',
i, panelName; // Scratch iteration variables
container.className += ' besogo-container'; // Marks this div as initialized
// Process options and set defaults
options = options || {}; // Makes option checking simpler
options.size = besogo.parseSize(options.size || 19);
options.coord = options.coord || 'none';
options.tool = options.tool || 'auto';
if (options.panels === '') {
options.panels = [];
}
options.panels = options.panels || 'control+names+comment+tool+tree+file';
if (typeof options.panels === 'string') {
options.panels = options.panels.split('+');
}
options.path = options.path || '';
if (options.shadows === undefined) {
options.shadows = 'auto';
} else if (options.shadows === 'off') {
options.shadows = false;
}
// Make the core editor object
editor = besogo.makeEditor(options.size.x, options.size.y);
editor.setTool(options.tool);
editor.setCoordStyle(options.coord);
if (options.realstones) { // Using realistic stones
editor.REAL_STONES = true;
editor.SHADOWS = options.shadows;
} else { // SVG stones
editor.SHADOWS = (options.shadows && options.shadows !== 'auto');
}
if (!options.nokeys) { // Add keypress handler unless nokeys option is truthy
addKeypressHandler(container, editor);
}
if (options.sgf) { // Load SGF file from URL
try {
fetchParseLoad(options.sgf, editor, options.path);
} catch(e) {
// Silently fail on network error
}
} else if (insideText.match(/\s*\(\s*;/)) { // Text content looks like an SGF file
parseAndLoad(insideText, editor);
navigatePath(editor, options.path); // Navigate editor along path
}
if (typeof options.variants === 'number' || typeof options.variants === 'string') {
editor.setVariantStyle(+options.variants); // Converts to number
}
while (container.firstChild) { // Remove all children of container
container.removeChild(container.firstChild);
}
boardDiv = makeDiv('besogo-board'); // Create div for board display
besogo.makeBoardDisplay(boardDiv, editor); // Create board display
if (!options.nowheel) { // Add mousewheel handler unless nowheel option is truthy
addWheelHandler(boardDiv, editor);
}
if (options.panels.length > 0) { // Only create if there are panels to add
panelsDiv = makeDiv('besogo-panels');
for (i = 0; i < options.panels.length; i++) {
panelName = options.panels[i];
if (makers[panelName]) { // Only add if creator function exists
makers[panelName](makeDiv('besogo-' + panelName, panelsDiv), editor);
}
}
if (!panelsDiv.firstChild) { // If no panels were added
container.removeChild(panelsDiv); // Remove the panels div
panelsDiv = false; // Flags panels div as removed
}
}
options.resize = options.resize || 'auto';
if (options.resize === 'auto') { // Add auto-resizing unless resize option is truthy
resizer = function() {
var windowHeight = window.innerHeight, // Viewport height
// Calculated width of parent element
parentWidth = parseFloat(getComputedStyle(container.parentElement).width),
maxWidth = +(options.maxwidth || -1),
orientation = options.orient || 'auto',
portraitRatio = +(options.portratio || 200) / 100,
landscapeRatio = +(options.landratio || 200) / 100,
minPanelsWidth = +(options.minpanelswidth || 350),
minPanelsHeight = +(options.minpanelsheight || 400),
minLandscapeWidth = +(options.transwidth || 600),
// Initial width parent
width = (maxWidth > 0 && maxWidth < parentWidth) ? maxWidth : parentWidth,
height; // Initial height is undefined
// Determine orientation if 'auto' or 'view'
if (orientation !== 'portrait' && orientation !== 'landscape') {
if (width < minLandscapeWidth || (orientation === 'view' && width < windowHeight)) {
orientation = 'portrait';
} else {
orientation = 'landscape';
}
}
if (orientation === 'portrait') { // Portrait mode
if (!isNaN(portraitRatio)) {
height = portraitRatio * width;
if (panelsDiv) {
height = (height - width < minPanelsHeight) ? width + minPanelsHeight : height;
}
} // Otherwise, leave height undefined
} else if (orientation === 'landscape') { // Landscape mode
if (!panelsDiv) { // No panels div
height = width; // Square overall
} else if (isNaN(landscapeRatio)) {
height = windowHeight;
} else { // Otherwise use ratio
height = width / landscapeRatio;
}
if (panelsDiv) {
// Reduce height to ensure minimum width of panels div
height = (width < height + minPanelsWidth) ? (width - minPanelsWidth) : height;
}
}
setDimensions(width, height);
container.style.width = width + 'px';
};
window.addEventListener("resize", resizer);
resizer(); // Initial div sizing
} else if (options.resize === 'fixed') {
setDimensions(container.clientWidth, container.clientHeight);
}
// Sets dimensions with optional height param
function setDimensions(width, height) {
if (height && width > height) { // Landscape mode
container.style['flex-direction'] = 'row';
boardDiv.style.height = height + 'px';
boardDiv.style.width = height + 'px';
if (panelsDiv) {
panelsDiv.style.height = height + 'px';
panelsDiv.style.width = (width - height) + 'px';
}
} else { // Portrait mode (implied if height is missing)
container.style['flex-direction'] = 'column';
boardDiv.style.height = width + 'px';
boardDiv.style.width = width + 'px';
if (panelsDiv) {
if (height) { // Only set height if param present
panelsDiv.style.height = (height - width) + 'px';
}
panelsDiv.style.width = width + 'px';
}
}
}
// Creates and adds divs to specified parent or container
function makeDiv(className, parent) {
var div = document.createElement("div");
if (className) {
div.className = className;
}
parent = parent || container;
parent.appendChild(div);
return div;
}
}; // END function besogo.create
// Parses size parameter from SGF format
besogo.parseSize = function(input) {
var matches,
sizeX,
sizeY;
input = (input + '').replace(/\s/g, ''); // Convert to string and remove whitespace
matches = input.match(/^(\d+):(\d+)$/); // Check for #:# pattern
if (matches) { // Composed value pattern found
sizeX = +matches[1]; // Convert to numbers
sizeY = +matches[2];
} else if (input.match(/^\d+$/)) { // Check for # pattern
sizeX = +input; // Convert to numbers
sizeY = +input; // Implied square
} else { // Invalid input format
sizeX = sizeY = 19; // Default size value
}
if (sizeX > 52 || sizeX < 1 || sizeY > 52 || sizeY < 1) {
sizeX = sizeY = 19; // Out of range, set to default
}
return { x: sizeX, y: sizeY };
};
// Automatically converts document elements into besogo instances
besogo.autoInit = function() {
var allDivs = document.getElementsByTagName('div'), // Live collection of divs
targetDivs = [], // List of divs to auto-initialize
options, // Structure to hold options
i, j, attrs; // Scratch iteration variables
for (i = 0; i < allDivs.length; i++) { // Iterate over all divs
if ( (hasClass(allDivs[i], 'besogo-editor') || // Has an auto-init class
hasClass(allDivs[i], 'besogo-viewer') ||
hasClass(allDivs[i], 'besogo-diagram')) &&
!hasClass(allDivs[i], 'besogo-container') ) { // Not already initialized
targetDivs.push(allDivs[i]);
}
}
for (i = 0; i < targetDivs.length; i++) { // Iterate over target divs
options = {}; // Clear the options struct
if (hasClass(targetDivs[i], 'besogo-editor')) {
options.panels = ['control', 'names', 'comment', 'tool', 'tree', 'file'];
options.tool = 'auto';
} else if (hasClass(targetDivs[i], 'besogo-viewer')) {
options.panels = ['control', 'names', 'comment'];
options.tool = 'navOnly';
} else if (hasClass(targetDivs[i], 'besogo-diagram')) {
options.panels = [];
options.tool = 'navOnly';
}
attrs = targetDivs[i].attributes;
for (j = 0; j < attrs.length; j++) { // Load attributes as options
options[attrs[j].name] = attrs[j].value;
}
besogo.create(targetDivs[i], options);
}
function hasClass(element, str) {
return (element.className.split(' ').indexOf(str) !== -1);
}
};
// Sets up keypress handling
function addKeypressHandler(container, editor) {
if (!container.getAttribute('tabindex')) {
container.setAttribute('tabindex', '0'); // Set tabindex to allow div focusing
}
container.addEventListener('keydown', function(evt) {
evt = evt || window.event;
switch (evt.keyCode) {
case 33: // page up
editor.prevNode(10);
break;
case 34: // page down
editor.nextNode(10);
break;
case 35: // end
editor.nextNode(-1);
break;
case 36: // home
editor.prevNode(-1);
break;
case 37: // left
editor.prevNode(1);
break;
case 38: // up
editor.nextSibling(-1);
break;
case 39: // right
editor.nextNode(1);
break;
case 40: // down
editor.nextSibling(1);
break;
case 46: // delete
editor.cutCurrent();
break;
} // END switch (evt.keyCode)
if (evt.keyCode >= 33 && evt.keyCode <= 40) {
evt.preventDefault(); // Suppress page nav controls
}
}); // END func() and addEventListener
} // END function addKeypressHandler
// Sets up mousewheel handling
function addWheelHandler(boardDiv, editor) {
boardDiv.addEventListener('wheel', function(evt) {
evt = evt || window.event;
if (evt.deltaY > 0) {
editor.nextNode(1);
evt.preventDefault();
} else if (evt.deltaY < 0) {
editor.prevNode(1);
evt.preventDefault();
}
});
}
// Parses SGF string and loads into editor
function parseAndLoad(text, editor) {
var sgf;
try {
sgf = besogo.parseSgf(text);
} catch (error) {
return; // Silently fail on parse error
}
besogo.loadSgf(sgf, editor);
}
// Fetches text file at url from same domain
function fetchParseLoad(url, editor, path) {
var http = new XMLHttpRequest();
http.onreadystatechange = function() {
if (http.readyState === 4 && http.status === 200) { // Successful fetch
parseAndLoad(http.responseText, editor);
navigatePath(editor, path);
}
};
http.overrideMimeType('text/plain'); // Prevents XML parsing and warnings
http.open("GET", url, true); // Asynchronous load
http.send();
}
function navigatePath(editor, path) {
var subPaths,
i, j; // Scratch iteration variables
path = path.split(/[Nn]+/); // Split into parts that start in next mode
for (i = 0; i < path.length; i++) {
subPaths = path[i].split(/[Bb]+/); // Split on switches into branch mode
executeMoves(subPaths[0], false); // Next mode moves
for (j = 1; j < subPaths.length; j++) { // Intentionally starting at 1
executeMoves(subPaths[j], true); // Branch mode moves
}
}
function executeMoves(part, branch) {
var i;
part = part.split(/\D+/); // Split on non-digits
for (i = 0; i < part.length; i++) {
if (part[i]) { // Skip empty strings
if (branch) { // Branch mode
if (editor.getCurrent().children.length) {
editor.nextNode(1);
editor.nextSibling(part[i] - 1);
}
} else { // Next mode
editor.nextNode(+part[i]); // Converts to number
}
}
}
}
}
})(); // END closure
besogo.makeBoardDisplay = function(container, editor) {
'use strict';
var CELL_SIZE = 88, // Including line width
COORD_MARGIN = 75, // Margin for coordinate labels
EXTRA_MARGIN = 6, // Extra margin on the edge of board
BOARD_MARGIN, // Total board margin
// Board size parameters
sizeX = editor.getCurrent().getSize().x,
sizeY = editor.getCurrent().getSize().y,
svg, // Holds the overall board display SVG element
stoneGroup, // Group for stones
markupGroup, // Group for markup
hoverGroup, // Group for hover layer
markupLayer, // Array of markup layer elements
hoverLayer, // Array of hover layer elements
randIndex, // Random index for stone images
TOUCH_FLAG = false; // Flag for touch interfaces
initializeBoard(editor.getCoordStyle()); // Initialize SVG element and draw the board
container.appendChild(svg); // Add the SVG element to the document
editor.addListener(update); // Register listener to handle editor/game state updates
redrawAll(editor.getCurrent()); // Draw stones, markup and hover layer
// Set listener to detect touch interfaces
container.addEventListener('touchstart', setTouchFlag);
// Function for setting the flag for touch interfaces
function setTouchFlag () {
TOUCH_FLAG = true; // Set flag to prevent needless function calls
hoverLayer = []; // Drop hover layer references, kills events
svg.removeChild(hoverGroup); // Remove hover group from SVG
// Remove self when done
container.removeEventListener('touchstart', setTouchFlag);
}
// Initializes the SVG and draws the board
function initializeBoard(coord) {
drawBoard(coord); // Initialize the SVG element and draw the board
stoneGroup = besogo.svgEl("g");
markupGroup = besogo.svgEl("g");
svg.appendChild(stoneGroup); // Add placeholder group for stone layer
svg.appendChild(markupGroup); // Add placeholder group for markup layer
if (!TOUCH_FLAG) {
hoverGroup = besogo.svgEl("g");
svg.appendChild(hoverGroup);
}
addEventTargets(); // Add mouse event listener layer
if (editor.REAL_STONES) { // Generate index for realistic stone images
randomizeIndex();
}
}
// Callback for board display redraws
function update(msg) {
var current = editor.getCurrent(),
currentSize = current.getSize(),
reinit = false, // Board redraw flag
oldSvg = svg;
// Check if board size has changed
if (currentSize.x !== sizeX || currentSize.y !== sizeY || msg.coord) {
sizeX = currentSize.x;
sizeY = currentSize.y;
initializeBoard(msg.coord || editor.getCoordStyle()); // Reinitialize board
container.replaceChild(svg, oldSvg);
reinit = true; // Flag board redrawn
}
// Redraw stones only if needed
if (reinit || msg.navChange || msg.stoneChange) {
redrawStones(current);
redrawMarkup(current);
redrawHover(current);
} else if (msg.markupChange || msg.treeChange) {
redrawMarkup(current);
redrawHover(current);
} else if (msg.tool || msg.label) {
redrawHover(current);
}
}
function redrawAll(current) {
redrawStones(current);
redrawMarkup(current);
redrawHover(current);
}
// Initializes the SVG element and draws the board
function drawBoard(coord) {
var boardWidth,
boardHeight,
string = "", // Path string for inner board lines
i; // Scratch iteration variable
BOARD_MARGIN = (coord === 'none' ? 0 : COORD_MARGIN) + EXTRA_MARGIN;
boardWidth = 2*BOARD_MARGIN + sizeX*CELL_SIZE;
boardHeight = 2*BOARD_MARGIN + sizeY*CELL_SIZE;
svg = besogo.svgEl("svg", { // Initialize the SVG element
width: "100%",
height: "100%",
viewBox: "0 0 " + boardWidth + " " + boardHeight
});
svg.appendChild(besogo.svgEl("rect", { // Fill background color
width: boardWidth,
height: boardHeight,
'class': 'besogo-svg-board'
}) );
svg.appendChild(besogo.svgEl("rect", { // Draw outer square of board
width: CELL_SIZE*(sizeX - 1),
height: CELL_SIZE*(sizeY - 1),
x: svgPos(1),
y: svgPos(1),
'class': 'besogo-svg-lines'
}) );
for (i = 2; i <= (sizeY - 1); i++) { // Horizontal inner lines
string += "M" + svgPos(1) + "," + svgPos(i) + "h" + CELL_SIZE*(sizeX - 1);
}
for (i = 2; i <= (sizeX - 1); i++) { // Vertical inner lines
string += "M" + svgPos(i) + "," + svgPos(1) + "v" + CELL_SIZE*(sizeY - 1);
}
svg.appendChild( besogo.svgEl("path", { // Draw inner lines of board
d: string,
'class': 'besogo-svg-lines'
}) );
drawHoshi(); // Draw the hoshi points
if (coord !== 'none') {
drawCoords(coord); // Draw the coordinate labels
}
}
// Draws coordinate labels on the board
function drawCoords(coord) {
var labels = besogo.coord[coord](sizeX, sizeY),
labelXa = labels.x, // Top edge labels
labelXb = labels.xb || labels.x, // Bottom edge
labelYa = labels.y, // Left edge
labelYb = labels.yb || labels.y, // Right edge
shift = COORD_MARGIN + 10,
i, x, y; // Scratch iteration variable
for (i = 1; i <= sizeX; i++) { // Draw column coordinate labels
x = svgPos(i);
drawCoordLabel(x, svgPos(1) - shift, labelXa[i]);
drawCoordLabel(x, svgPos(sizeY) + shift, labelXb[i]);
}
for (i = 1; i <= sizeY; i++) { // Draw row coordinate labels
y = svgPos(i);
drawCoordLabel(svgPos(1) - shift, y, labelYa[i]);
drawCoordLabel(svgPos(sizeX) + shift, y, labelYb[i]);
}
function drawCoordLabel(x, y, label) {
var element = besogo.svgEl("text", {
x: x,
y: y,
dy: ".65ex", // Seems to work for vertically centering these fonts
"font-size": 32,
"text-anchor": "middle", // Horizontal centering
"font-family": "Helvetica, Arial, sans-serif",
fill: 'black'
});
element.appendChild( document.createTextNode(label) );
svg.appendChild(element);
}
}
// Draws hoshi onto the board at procedurally generated locations
function drawHoshi() {
var cx, cy, // Center point calculation
pathStr = ""; // Path string for drawing star points
if (sizeX % 2 && sizeY % 2) { // Draw center hoshi if both dimensions are odd
cx = (sizeX - 1)/2 + 1; // Calculate the center of the board
cy = (sizeY - 1)/2 + 1;
drawStar(cx, cy);
if (sizeX >= 17 && sizeY >= 17) { // Draw side hoshi if at least 17x17 and odd
drawStar(4, cy);
drawStar(sizeX - 3, cy);
drawStar(cx, 4);
drawStar(cx, sizeY - 3);
}
}
if (sizeX >= 11 && sizeY >= 11) { // Corner hoshi at (4, 4) for larger sizes
drawStar(4, 4);
drawStar(4, sizeY - 3);
drawStar(sizeX - 3, 4);
drawStar(sizeX - 3, sizeY - 3);
} else if (sizeX >= 8 && sizeY >= 8) { // Corner hoshi at (3, 3) for medium sizes
drawStar(3, 3);
drawStar(3, sizeY - 2);
drawStar(sizeX - 2, 3);
drawStar(sizeX - 2, sizeY - 2);
} // No corner hoshi for smaller sizes
if (pathStr) { // Only need to add if hoshi drawn
svg.appendChild( besogo.svgEl('path', { // Drawing circles via path points
d: pathStr, // Hack to allow radius adjustment via stroke-width
'stroke-linecap': 'round', // Makes the points round
'class': 'besogo-svg-hoshi'
}) );
}
function drawStar(i, j) { // Extend path string to draw star point
pathStr += "M" + svgPos(i) + ',' + svgPos(j) + 'l0,0'; // Draws a point
}
}
// Remakes the randomized index for stone images
function randomizeIndex() {
var maxIndex = besogo.BLACK_STONES * besogo.WHITE_STONES,
i, j;
randIndex = [];
for (i = 1; i <= sizeX; i++) {
for (j = 1; j <= sizeY; j++) {
randIndex[fromXY(i, j)] = Math.floor(Math.random() * maxIndex);
}
}
}
// Adds a grid of squares to register mouse events
function addEventTargets() {
var element,
i, j;
for (i = 1; i <= sizeX; i++) {
for (j = 1; j <= sizeY; j++) {
element = besogo.svgEl("rect", { // Make a transparent event target
x: svgPos(i) - CELL_SIZE/2,
y: svgPos(j) - CELL_SIZE/2,
width: CELL_SIZE,
height: CELL_SIZE,
opacity: 0
});
// Add event listeners, using closures to decouple (i, j)
element.addEventListener("click", handleClick(i, j));
if (!TOUCH_FLAG) { // Skip hover listeners for touch interfaces
element.addEventListener("mouseover", handleOver(i, j));
element.addEventListener("mouseout", handleOut(i, j));
}
svg.appendChild(element);
}
}
}
function handleClick(i, j) { // Returns function for click handling
return function(event) {
// Call click handler in editor
editor.click(i, j, event.ctrlKey, event.shiftKey);
if(!TOUCH_FLAG) {
(handleOver(i, j))(); // Ensures that any updated tool is visible
}
};
}
function handleOver(i, j) { // Returns function for mouse over
return function() {
var element = hoverLayer[ fromXY(i, j) ];
if (element) { // Make tool action visible on hover over
element.setAttribute('visibility', 'visible');
}
};
}
function handleOut(i, j) { // Returns function for mouse off
return function() {
var element = hoverLayer[ fromXY(i, j) ];
if (element) { // Make tool action invisible on hover off
element.setAttribute('visibility', 'hidden');
}
};
}
// Redraws the stones
function redrawStones(current) {
var group = besogo.svgEl("g"), // New stone layer group
shadowGroup, // Group for shadow layer
i, j, x, y, color; // Scratch iteration variables
if (editor.SHADOWS) { // Add group for shawdows
shadowGroup = besogo.svgShadowGroup();
group.appendChild(shadowGroup);
}
for (i = 1; i <= sizeX; i++) {
for (j = 1; j <= sizeY; j++) {
color = current.getStone(i, j);
if (color) {
x = svgPos(i);
y = svgPos(j);
if (editor.REAL_STONES) { // Realistic stone
group.appendChild(besogo.realStone(x, y, color, randIndex[fromXY(i, j)]));
} else { // SVG stone
group.appendChild(besogo.svgStone(x, y, color));
}
if (editor.SHADOWS) { // Draw shadows
shadowGroup.appendChild(besogo.svgShadow(x - 2, y - 4));
shadowGroup.appendChild(besogo.svgShadow(x + 2, y + 4));
}
}
}
}
svg.replaceChild(group, stoneGroup); // Replace the stone group
stoneGroup = group;
}
// Redraws the markup
function redrawMarkup(current) {
var element, i, j, x, y, // Scratch iteration variables
group = besogo.svgEl("g"), // Group holding markup layer elements
lastMove = current.move,
variants = editor.getVariants(),
mark, // Scratch mark state {0, 1, 2, 3, 4, 5}
stone, // Scratch stone state {0, -1, 1}
color; // Scratch color string
markupLayer = []; // Clear the references to the old layer
for (i = 1; i <= sizeX; i++) {
for (j = 1; j <= sizeY; j++) {
mark = current.getMarkup(i, j);
if (mark) {
x = svgPos(i);
y = svgPos(j);
stone = current.getStone(i, j);
color = (stone === -1) ? "white" : "black"; // White on black
if (lastMove && lastMove.x === i && lastMove.y === j) {
// Mark last move blue or violet if also a variant
color = checkVariants(variants, current, i, j) ?
besogo.PURP : besogo.BLUE;
} else if (checkVariants(variants, current, i, j)) {
color = besogo.RED; // Natural variant marks are red
}
if (typeof mark === 'number') { // Markup is a basic shape
switch(mark) {
case 1:
element = besogo.svgCircle(x, y, color);
break;
case 2:
element = besogo.svgSquare(x, y, color);
break;
case 3:
element = besogo.svgTriangle(x, y, color);
break;
case 4:
element = besogo.svgCross(x, y, color);
break;
case 5:
element = besogo.svgBlock(x, y, color);
break;
}
} else { // Markup is a label
if (!stone) { // If placing label on empty spot
element = makeBacker(x, y);
group.appendChild(element);
}
element = besogo.svgLabel(x, y, color, mark);
}
group.appendChild(element);
markupLayer[ fromXY(i, j) ] = element;
} // END if (mark)
} // END for j
} // END for i
// Mark last move with plus if not already marked
if (lastMove && lastMove.x !== 0 && lastMove.y !== 0) {
i = lastMove.x;
j = lastMove.y;
if (!markupLayer[ fromXY(i, j) ]) { // Last move not marked
color = checkVariants(variants, current, i, j) ? besogo.PURP : besogo.BLUE;
element = besogo.svgPlus(svgPos(i), svgPos(j), color);
group.appendChild(element);
markupLayer[ fromXY(i, j) ] = element;
}
}
// Mark variants that have not already been marked above
markRemainingVariants(variants, current, group);
svg.replaceChild(group, markupGroup); // Replace the markup group
markupGroup = group;
} // END function redrawMarkup
function makeBacker(x, y) { // Makes a label markup backer at (x, y)
return besogo.svgEl("rect", {
x: x - CELL_SIZE/2,
y: y - CELL_SIZE/2,
height: CELL_SIZE,
width: CELL_SIZE,
opacity: 0.85,
stroke: "none",
'class': 'besogo-svg-board besogo-svg-backer'
});
}
// Checks if (x, y) is in variants
function checkVariants(variants, current, x, y) {
var i, move;
for (i = 0; i < variants.length; i++) {
if (variants[i] !== current) { // Skip current (within siblings)
move = variants[i].move;
if (move && move.x === x && move.y === y) {
return true;
}
}
}
return false;
}
// Marks variants that have not already been marked
function markRemainingVariants(variants, current, group) {
var element,
move, // Variant move
label, // Variant label
stone, // Stone state
i, x, y; // Scratch iteration variables
for (i = 0; i < variants.length; i++) {
if (variants[i] !== current) { // Skip current (within siblings)
move = variants[i].move;
// Check if move, not a pass, and no mark yet
if (move && move.x !== 0 && !markupLayer[ fromXY(move.x, move.y) ]) {
stone = current.getStone(move.x, move.y);
x = svgPos(move.x); // Get SVG positions
y = svgPos(move.y);
if (!stone) { // If placing label on empty spot
element = makeBacker(x, y);
group.appendChild(element);
}
// Label variants with letters A-Z cyclically
label = String.fromCharCode('A'.charCodeAt(0) + (i % 26));
element = besogo.svgLabel(x, y, besogo.LRED, label);
group.appendChild(element);
markupLayer[ fromXY(move.x, move.y) ] = element;
}
}
}
} // END function markRemainingVariants
// Redraws the hover layer
function redrawHover(current) {
if (TOUCH_FLAG) {
return; // Do nothing for touch interfaces
}
var element, i, j, x, y, // Scratch iteration variables
group = besogo.svgEl("g"), // Group holding hover layer elements
tool = editor.getTool(),
children,
stone, // Scratch stone state {0, -1, 1} or move
color; // Scratch color string
hoverLayer = []; // Clear the references to the old layer
group.setAttribute('opacity', '0.35');
if (tool === 'navOnly') { // Render navOnly hover by iterating over children
children = current.children;
for (i = 0; i < children.length; i++) {
stone = children[i].move;
if (stone && stone.x !== 0) { // Child node is move and not a pass
x = svgPos(stone.x);
y = svgPos(stone.y);
element = besogo.svgStone(x, y, stone.color);
element.setAttribute('visibility', 'hidden');
group.appendChild(element);
hoverLayer[ fromXY(stone.x, stone.y) ] = element;
}
}
} else { // Render hover for other tools by iterating over grid
for (i = 1; i <= sizeX; i++) {
for (j = 1; j <= sizeY; j++) {
element = null;
x = svgPos(i);
y = svgPos(j);
stone = current.getStone(i, j);
color = (stone === -1) ? "white" : "black"; // White on black
switch(tool) {
case 'auto':
element = besogo.svgStone(x, y, current.nextMove());
break;
case 'playB':
element = besogo.svgStone(x, y, -1);
break;
case 'playW':
element = besogo.svgStone(x, y, 1);
break;
case 'addB':
if (stone === -1) {
element = besogo.svgCross(x, y, besogo.RED);
} else {
element = besogo.svgEl('g');
element.appendChild(besogo.svgStone(x, y, -1));
element.appendChild(besogo.svgPlus(x, y, besogo.RED));
}
break;
case 'addW':
if (stone === 1) {
element = besogo.svgCross(x, y, besogo.RED);
} else {
element = besogo.svgEl('g');
element.appendChild(besogo.svgStone(x, y, 1));
element.appendChild(besogo.svgPlus(x, y, besogo.RED));
}
break;
case 'addE':
if (stone) {
element = besogo.svgCross(x, y, besogo.RED);
}
break;
case 'clrMark':
break; // Nothing
case 'circle':
element = besogo.svgCircle(x, y, color);
break;
case 'square':
element = besogo.svgSquare(x, y, color);
break;
case 'triangle':
element = besogo.svgTriangle(x, y, color);
break;
case 'cross':
element = besogo.svgCross(x, y, color);
break;
case 'block':
element = besogo.svgBlock(x, y, color);
break;
case 'label':
element = besogo.svgLabel(x, y, color, editor.getLabel());
break;
} // END switch (tool)
if (element) {
element.setAttribute('visibility', 'hidden');
group.appendChild(element);
hoverLayer[ fromXY(i, j) ] = element;
}
} // END for j
} // END for i
} // END else
svg.replaceChild(group, hoverGroup); // Replace the hover layer group
hoverGroup = group;
} // END function redrawHover
function svgPos(x) { // Converts (x, y) coordinates to SVG position
return BOARD_MARGIN + CELL_SIZE/2 + (x-1) * CELL_SIZE;
}
function fromXY(x, y) { // Converts (x, y) coordinates to linear index
return (x - 1)*sizeY + (y - 1);
}
};
besogo.makeCommentPanel = function(container, editor) {
'use strict';
var infoTexts = {}, // Holds text nodes for game info properties
gameInfoTable = document.createElement('table'),
gameInfoEdit = document.createElement('table'),
commentBox = document.createElement('div'),
commentEdit = document.createElement('textarea'),
playerInfoOrder = 'PW WR WT PB BR BT'.split(' '),
infoOrder = 'HA KM RU TM OT GN EV PC RO DT RE ON GC AN US SO CP'.split(' '),
infoIds = {
PW: 'White Player',
WR: 'White Rank',
WT: 'White Team',
PB: 'Black Player',
BR: 'Black Rank',
BT: 'Black Team',
HA: 'Handicap',
KM: 'Komi',
RU: 'Rules',
TM: 'Timing',
OT: 'Overtime',
GN: 'Game Name',
EV: 'Event',
PC: 'Place',
RO: 'Round',
DT: 'Date',
RE: 'Result',
ON: 'Opening',
GC: 'Comments',
AN: 'Annotator',
US: 'Recorder',
SO: 'Source',
CP: 'Copyright'
};
container.appendChild(makeInfoButton());
container.appendChild(makeInfoEditButton());
container.appendChild(makeCommentButton());
container.appendChild(gameInfoTable);
container.appendChild(gameInfoEdit);
infoTexts.C = document.createTextNode('');
container.appendChild(commentBox);
commentBox.appendChild(infoTexts.C);
container.appendChild(commentEdit);
commentEdit.onblur = function() {
editor.setComment(commentEdit.value);
};
commentEdit.addEventListener('keydown', function(evt) {
evt = evt || window.event;
evt.stopPropagation(); // Stop keydown propagation when in focus
});
editor.addListener(update);
update({ navChange: true, gameInfo: editor.getGameInfo() });
gameInfoEdit.style.display = 'none'; // Hide game info editting table initially
function update(msg) {
var temp; // Scratch for strings
if (msg.navChange) {
temp = editor.getCurrent().comment || '';
updateText(commentBox, temp, 'C');
if (editor.getCurrent() === editor.getRoot() &&
gameInfoTable.firstChild &&
gameInfoEdit.style.display === 'none') {
gameInfoTable.style.display = 'table';
} else {
gameInfoTable.style.display = 'none';
}
commentEdit.style.display = 'none';
commentBox.style.display = 'block';
} else if (msg.comment !== undefined) {
updateText(commentBox, msg.comment, 'C');
commentEdit.value = msg.comment;
}
if (msg.gameInfo) { // Update game info
updateGameInfoTable(msg.gameInfo);
updateGameInfoEdit(msg.gameInfo);
}
} // END function update
function updateGameInfoTable(gameInfo) {
var table = document.createElement('table'),
i, id, row, cell, text; // Scratch iteration variable
table.className = 'besogo-gameInfo';
for (i = 0; i < infoOrder.length ; i++) { // Iterate in specified order
id = infoOrder[i];
if (gameInfo[id]) { // Only add row if property exists
row = document.createElement('tr');
table.appendChild(row);
cell = document.createElement('td');
cell.appendChild(document.createTextNode(infoIds[id]));
row.appendChild(cell);
cell = document.createElement('td');
text = document.createTextNode(gameInfo[id]);
cell.appendChild(text);
row.appendChild(cell);
}
}
if (!table.firstChild || gameInfoTable.style.display === 'none') {
table.style.display = 'none'; // Do not display empty table or if already hidden
}
container.replaceChild(table, gameInfoTable);
gameInfoTable = table;
}
function updateGameInfoEdit(gameInfo) {
var table = document.createElement('table'),
infoTableOrder = playerInfoOrder.concat(infoOrder),
i, id, row, cell, text;
table.className = 'besogo-gameInfo';
for (i = 0; i < infoTableOrder.length ; i++) { // Iterate in specified order
id = infoTableOrder[i];
row = document.createElement('tr');
table.appendChild(row);
cell = document.createElement('td');
cell.appendChild(document.createTextNode(infoIds[id]));
row.appendChild(cell);
cell = document.createElement('td');
text = document.createElement('input');
if (gameInfo[id]) {
text.value = gameInfo[id];
}
text.onblur = function(t, id) {
return function() { // Commit change on blur
editor.setGameInfo(t.value, id);
};
}(text, id);
text.addEventListener('keydown', function(evt) {
evt = evt || window.event;
evt.stopPropagation(); // Stop keydown propagation when in focus
});
cell.appendChild(text);
row.appendChild(cell);
}
if (gameInfoEdit.style.display === 'none') {
table.style.display = 'none'; // Hide if already hidden
}
container.replaceChild(table, gameInfoEdit);
gameInfoEdit = table;
}
function updateText(parent, text, id) {
var textNode = document.createTextNode(text);
parent.replaceChild(textNode, infoTexts[id]);
infoTexts[id] = textNode;
}
function makeInfoButton() {
var button = document.createElement('input');
button.type = 'button';
button.value = 'Info';
button.title = 'Show/hide game info';
button.onclick = function() {
if (gameInfoTable.style.display === 'none' && gameInfoTable.firstChild) {
gameInfoTable.style.display = 'table';
} else {
gameInfoTable.style.display = 'none';
}
gameInfoEdit.style.display = 'none';
};
return button;
}
function makeInfoEditButton() {
var button = document.createElement('input');
button.type = 'button';
button.value = 'Edit Info';
button.title = 'Edit game info';
button.onclick = function() {
if (gameInfoEdit.style.display === 'none') {
gameInfoEdit.style.display = 'table';
} else {
gameInfoEdit.style.display = 'none';
}
gameInfoTable.style.display = 'none';
};
return button;
}
function makeCommentButton() {
var button = document.createElement('input');
button.type = 'button';
button.value = 'Comment';
button.title = 'Edit comment';
button.onclick = function() {
if (commentEdit.style.display === 'none') { // Comment edit box hidden
commentBox.style.display = 'none'; // Hide static comment display
gameInfoTable.style.display = 'none'; // Hide game info table
commentEdit.value = editor.getCurrent().comment;
commentEdit.style.display = 'block'; // Show comment edit box
} else { // Comment edit box open
commentEdit.style.display = 'none'; // Hide comment edit box
commentBox.style.display = 'block'; // Show static comment display
}
};
return button;
}
};
besogo.makeControlPanel = function(container, editor) {
'use strict';
var leftElements = [], // SVG elements for previous node buttons
rightElements = [], // SVG elements for next node buttons
siblingElements = [], // SVG elements for sibling buttons
variantStyleButton, // Button for changing variant style
hideVariantButton, // Button for toggling show/hide variants
childVariantElement, // SVG element for child style variants
siblingVariantElement, // SVG element for sibling style variants
hideVariantElement; // SVG element for hiding variants
drawNavButtons();
drawStyleButtons();
editor.addListener(update);
update({ navChange: true, variantStyle: editor.getVariantStyle() }); // Initialize
// Callback for variant style and nav state changes
function update(msg) {
var current;
if (msg.variantStyle !== undefined) {
updateStyleButtons(msg.variantStyle);
}
if (msg.navChange || msg.treeChange) { // Update the navigation buttons
current = editor.getCurrent();
if (current.parent) { // Has parent
arraySetColor(leftElements, 'black');
if (current.parent.children.length > 1) { // Has siblings
arraySetColor(siblingElements, 'black');
} else { // No siblings
arraySetColor(siblingElements, besogo.GREY);
}
} else { // No parent
arraySetColor(leftElements, besogo.GREY);
arraySetColor(siblingElements, besogo.GREY);
}
if (current.children.length) { // Has children
arraySetColor(rightElements, 'black');
} else { // No children
arraySetColor(rightElements, besogo.GREY);
}
}
function updateStyleButtons(style) { // Updates the variant style buttons
if (style % 2) { // Sibling style variants
childVariantElement.setAttribute('fill', 'black');
siblingVariantElement.setAttribute('fill', besogo.BLUE);
variantStyleButton.title = 'Variants: child/[sibling]';
} else { // Child style variants
childVariantElement.setAttribute('fill', besogo.BLUE);
siblingVariantElement.setAttribute('fill', besogo.RED);
variantStyleButton.title = 'Variants: [child]/sibling';
}
if (style >= 2) { // Hide auto-markup for variants
hideVariantElement.setAttribute('visibility', 'visible');
hideVariantButton.title = 'Variants: show/[hide]';
} else { // Show auto-markup for variants
hideVariantElement.setAttribute('visibility', 'hidden');
hideVariantButton.title = 'Variants: [show]/hide';
}
}
function arraySetColor(list, color) { // Changes fill color of list of svg elements
var i;
for (i = 0; i < list.length; i++) {
list[i].setAttribute('fill', color);
}
}
} // END function update
// Draws the navigation buttons
function drawNavButtons() {
leftElements.push(makeNavButton('First node',
'5,10 5,90 25,90 25,50 95,90 95,10 25,50 25,10',
function() {
editor.prevNode(-1);
})
);
leftElements.push(makeNavButton('Jump back',
'95,10 50,50 50,10 5,50 50,90 50,50 95,90',
function() {
editor.prevNode(10);
})
);
leftElements.push(makeNavButton('Previous node', '85,10 85,90 15,50', function() {
editor.prevNode(1);
}));
rightElements.push(makeNavButton('Next node', '15,10 15,90 85,50', function() {
editor.nextNode(1);
}));
rightElements.push(makeNavButton('Jump forward',
'5,10 50,50 50,10 95,50 50,90 50,50 5,90',
function() {
editor.nextNode(10);
})
);
rightElements.push(makeNavButton('Last node',
'95,10 95,90 75,90 75,50 5,90 5,10 75,50 75,10',
function() {
editor.nextNode(-1);
})
);
siblingElements.push(makeNavButton('Previous sibling', '10,85 90,85 50,15', function() {
editor.nextSibling(-1);
}));
siblingElements.push(makeNavButton('Next sibling', '10,15 90,15 50,85', function() {
editor.nextSibling(1);
}));
function makeNavButton(tooltip, pointString, action) { // Creates a navigation button
var button = document.createElement('button'),
svg = makeButtonContainer(),
element = besogo.svgEl("polygon", {
points: pointString,
stroke: 'none',
fill: 'black'
});
button.title = tooltip;
button.onclick = action;
button.appendChild(svg);
svg.appendChild(element);
container.appendChild(button);
return el