UNPKG

besogo

Version:

Embeddable SGF player for the game of Go (aka Weiqi, Baduk)

1,289 lines (1,145 loc) 135 kB
(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