besogo
Version:
Embeddable SGF player for the game of Go (aka Weiqi, Baduk)
373 lines (333 loc) • 14 kB
JavaScript
(function() {
;
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