UNPKG

riddles

Version:

A framework for building series of puzzle challenge, like Python Challenge.

504 lines (426 loc) 18.1 kB
/** * Created by shuding on 10/11/15. * <ds303077135@gmail.com> */ // Original author: http://bl.ocks.org/rkirsling/5001347 // Modified by shuding (function (window, data) { window.keyData = {}; window.initData = {}; window.typeLock = 0; var _ = function (str) { return i18n[str] ? i18n[str] : str; }; var width = 960, height = 800; var nodeData = {}; var svg = d3.select('.flow').append('svg').attr('oncontextmenu', 'return false;').attr('width', width).attr('height', height); // set up initial nodes and links // - nodes are known by 'id', not by index in array. // - reflexive edges are indicated on the node (as a bold black circle). // - links are always source < target; edge directions are set by 'left' and 'right'. var nodes = [], links = []; // init D3 force layout var force = d3.layout.force().nodes(nodes).links(links).size([width, height]).linkDistance(150).charge(-600).on('tick', tick); // define arrow markers for graph links svg.append('svg:defs').append('svg:marker').attr('id', 'end-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 6).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M0,-5L10,0L0,5').attr('fill', '#33C3F0'); svg.append('svg:defs').append('svg:marker').attr('id', 'start-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 4).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M10,-5L0,0L10,5').attr('fill', '#33C3F0'); // line displayed when dragging new nodes var drag_line = svg.append('svg:path').attr('class', 'link dragline hidden').attr('d', 'M0,0L0,0'); // handles to link and node element groups var path = svg.append('svg:g').selectAll('path'), circle = svg.append('svg:g').selectAll('g'); // mouse event vars var selected_node = null, selected_link = null, mousedown_link = null, mousedown_node = null, mouseup_node = null; function resetMouseVars() { mousedown_node = null; mouseup_node = null; mousedown_link = null; } // update force layout (called automatically each iteration) function tick() { // draw directed edges with proper padding from node centers path.attr('d', function (d) { var deltaX = d.target.x - d.source.x, deltaY = d.target.y - d.source.y, dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), normX = deltaX / dist, normY = deltaY / dist, sourcePadding = d.left ? 27 : 22, targetPadding = d.right ? 27 : 22, sourceX = d.source.x + (sourcePadding * normX), sourceY = d.source.y + (sourcePadding * normY), targetX = d.target.x - (targetPadding * normX), targetY = d.target.y - (targetPadding * normY); return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; }); circle.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; }); } // update graph (called when needed) function restart() { // path (link) group path = path.data(links); // update existing links path.classed('selected', function (d) { return d === selected_link; }).style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; }).style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; }); // add new links path.enter().append('svg:path').attr('class', 'link').classed('selected', function (d) { return d === selected_link; }).style('marker-start', function (d) { return d.left ? 'url(#start-arrow)' : ''; }).style('marker-end', function (d) { return d.right ? 'url(#end-arrow)' : ''; }).on('mousedown', function (d) { if (d3.event.ctrlKey) { return; } // select link mousedown_link = d; if (mousedown_link === selected_link) { selected_link = null; } else { selected_link = mousedown_link; } selected_node = null; restart(); }); // remove old links path.exit().remove(); // circle (node) group // NB: the function arg is crucial here! nodes are known by id, not by index! circle = circle.data(nodes, function (d) { nodeData[d.id] = d; return d.id; }); // update existing nodes (reflexive & selected visual states) circle.selectAll('circle').style('fill', function (d) { return (d === selected_node) ? 'yellow' : '#33C3F0'; }).style('stroke', function (d) { return initData[d.id] ? '#C1F0FF' : ''; }); // add new nodes var g = circle.enter().append('svg:g'); g.append('svg:circle').attr('class', 'node').attr('r', 22).style('fill', function (d) { return (d === selected_node) ? 'yellow' : '#33C3F0'; }).style('stroke', function (d) { return initData[d.id] ? '#C1F0FF' : ''; }).on('mouseover', function (d) { d3.select(this).attr('transform', 'scale(1.2)'); }).on('mouseout', function (d) { d3.select(this).attr('transform', ''); }).on('mousedown', function (d) { if (d3.event.ctrlKey) { return; } // select node mousedown_node = d; if (mousedown_node === selected_node) { selected_node = null; } else { selected_node = mousedown_node; } selected_link = null; // reposition drag line drag_line.style('marker-end', 'url(#end-arrow)').classed('hidden', false).attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y); restart(); }).on('mouseup', function (d) { if (!mousedown_node) { return; } // needed by FF drag_line.classed('hidden', true).style('marker-end', ''); // check for drag-to-self mouseup_node = d; if (mousedown_node == mouseup_node) { return; } // unenlarge target node d3.select(this).attr('transform', ''); // add link to graph (update if exists) // NB: links are strictly source < target; arrows separately specified by booleans var source, target, direction; if (mousedown_node.id < mouseup_node.id) { source = mousedown_node; target = mouseup_node; direction = 'right'; } else { source = mouseup_node; target = mousedown_node; direction = 'left'; } var link; link = links.filter(function (l) { return (l.source === source && l.target === target); })[0]; if (link) { link[direction] = true; } else { link = { source: source, target: target, left: false, right: false }; link[direction] = true; links.push(link); } putData(); // select new link selected_link = link; selected_node = null; restart(); }); // show node IDs g.append('svg:text').attr('x', 0).attr('y', 4).attr('class', 'id').text(function (d) { return data[d.id].title; }); // remove old nodes circle.exit().remove(); // set the graph in motion force.start(); } window.restart = restart; function mousedown() { // prevent I-bar on drag //d3.event.preventDefault(); // because :active only works in WebKit? svg.classed('active', true); } function mousemove() { if (!mousedown_node) { return; } // update drag line drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]); restart(); } function mouseup() { if (mousedown_node) { // hide drag line drag_line.classed('hidden', true).style('marker-end', ''); } // because :active only works in WebKit? svg.classed('active', false); // clear mouse event vars resetMouseVars(); putData(); } // only respond once per keydown var lastKeyDown = -1; function keydown() { //d3.event.preventDefault(); if (typeLock) { return; } if (lastKeyDown !== -1) { return; } lastKeyDown = d3.event.keyCode; // ctrl if (d3.event.keyCode === 17) { circle.call(force.drag); svg.classed('ctrl', true); } if (!selected_node && !selected_link) { return; } switch (d3.event.keyCode) { case 8: // backspace case 46: // delete if (selected_link) { links.splice(links.indexOf(selected_link), 1); } selected_link = null; restart(); d3.event.preventDefault(); break; } } function keyup() { if (typeLock) { return; } lastKeyDown = -1; // ctrl if (d3.event.keyCode === 17) { circle.on('mousedown.drag', null).on('touchstart.drag', null); svg.classed('ctrl', false); } putData(); } var dataTable = window.document.getElementsByClassName('data-table')[0]; var hideForm = window.document.getElementById('hide-form'); var formData = window.document.getElementById('form-data'); var linkData; function getLinkData(id, dir) { if (!linkData[id]) { return ''; } var ret = []; if (dir == 1) { linkData[id].next.forEach(function (nid) { ret.push('<div>' + data[nid].title + '</div>'); }); } else { linkData[id].prev.forEach(function (nid) { ret.push('<div>' + data[nid].title + '</div>'); }); } return ret.join(''); } function parseLink(link) { if (!linkData[link.source.id]) { linkData[link.source.id] = { prev: [], next: [] }; } if (!linkData[link.target.id]) { linkData[link.target.id] = { prev: [], next: [] }; } if (link.right) { linkData[link.source.id].next.push(link.target.id); linkData[link.target.id].prev.push(link.source.id); } if (link.left) { linkData[link.source.id].prev.push(link.target.id); linkData[link.target.id].next.push(link.source.id); } } function putData() { linkData = {}; var html = '<table class="u-full-width"><thead><tr>'; if (selected_link) { html += '<th>From</th><th>Key</th><th>To</th></thead><tbody>'; if (selected_link.left) { if (!keyData[selected_link.target.id]) { keyData[selected_link.target.id] = {}; } if (!keyData[selected_link.target.id][selected_link.source.id]) { keyData[selected_link.target.id][selected_link.source.id] = ''; } html += '<tr><td>' + data[selected_link.target.id].title + '</td>'; html += '<td><input class="key" type="text" onfocus="typeLock++" onblur="typeLock--" value="' + keyData[selected_link.target.id][selected_link.source.id] + '" onchange="keyData[' + "'" + selected_link.target.id + "'][" + "'" + selected_link.source.id + "'] = this.value" + '"></td>'; html += '<td>' + data[selected_link.source.id].title + '</td></tr>'; } if (selected_link.right) { if (!keyData[selected_link.source.id]) { keyData[selected_link.source.id] = {}; } if (!keyData[selected_link.source.id][selected_link.target.id]) { keyData[selected_link.source.id][selected_link.target.id] = ''; } html += '<tr><td>' + data[selected_link.source.id].title + '</td>'; html += '<td><input class="key" type="text" onfocus="typeLock++" onblur="typeLock--" value="' + keyData[selected_link.source.id][selected_link.target.id] + '" onchange="keyData[' + "'" + selected_link.source.id + "'][" + "'" + selected_link.target.id + "'] = this.value" + '"></td>'; html += '<td>' + data[selected_link.target.id].title + '</td></tr>'; } html += '</tbody></table>'; } else if (selected_node) { links.forEach(parseLink); html += '<th>' + _('Quiz') + '</th><th>' + _('Previous') + '</th><th>' + _('Next') + '</th><th>' + _('Initially') + '</th></tr></thead><tbody><tr>'; html += '<td><a href="/admin/quizzes/' + selected_node.id + '">' + data[selected_node.id].title + '</a></td>'; html += '<td>' + getLinkData(selected_node.id, -1) + '</td>'; html += '<td>' + getLinkData(selected_node.id, 1) + '</td>'; html += '<td><input class="key" type="checkbox" ' + (initData[selected_node.id] ? 'checked="checked"' : '') + ' onchange="initData[' + "'" + selected_node.id + "'" + '] = this.checked; restart();"></td>'; html += '</tr></tbody></table>'; } dataTable.innerHTML = html; } document.getElementById('save').addEventListener('click', function (event) { var id; for (id in data) { if (data.hasOwnProperty(id)) { data[id].start = false; data[id].next = []; } } for (id in initData) { if (initData.hasOwnProperty(id)) { data[id].start = initData[id] == true; } } for (var i = 0; i < links.length; ++i) { if (!(function (link) { if (link.right) { if (keyData[link.source.id][link.target.id].length) { data[link.source.id].next.push({ id: link.target.id, key: keyData[link.source.id][link.target.id] }); } else { alert(_('The key from "') + data[link.source.id].title + _('" to "') + data[link.target.id].title + '" is not specified!'); event.preventDefault(); return false; } } if (link.left) { if (keyData[link.target.id][link.source.id].length) { data[link.target.id].next.push({ id: link.source.id, key: keyData[link.target.id][link.source.id] }); } else { alert(_('The key from "') + data[link.target.id].title + _('" to "') + data[link.source.id].title + '" is not specified!'); event.preventDefault(); return false; } } return true; })(links[i])) { return false; } } formData.value = JSON.stringify(data); hideForm.submit(); }, false); for (id in data) { if (data.hasOwnProperty(id)) { nodes.push({ id: id, reflexive: false }); if (data[id].start) { initData[id] = true; } } } // app starts here svg.on('mousedown', mousedown).on('mousemove', mousemove).on('mouseup', mouseup); d3.select(window).on('keydown', keydown).on('keyup', keyup); restart(); for (id in data) { if (data.hasOwnProperty(id)) { (function (id) { data[id].next.forEach(function (link) { if (!keyData[id]) { keyData[id] = {}; } keyData[id][link.id] = link.key || ''; var dir = 'right'; var newLink = links.filter(function (l) { if (l.source.id == id && l.target.id == link.id) { dir = 'right'; return 1; } if (l.source.id == link.id && l.target.id == id) { dir = 'left'; return 1; } return 0; })[0]; if (newLink) { newLink[dir] = true; } else { newLink = { source: nodeData[id], target: nodeData[link.id], left: false, right: false }; newLink[dir] = true; links.push(newLink); } }) })(id); } } restart(); })(window, data);