最近更新: 2007-08-28

WebFlow UserInterface

流程編輯器。使用 JavaScript 實作的使用者介面,未附伺服端儲存與載入功能源碼。

使用 wz_jsgraphics.js 繪製線條。當時曾試過 SVG ,但效果與瀏覽器相容性皆不理想,所以還是用 wz_jsgraphics.js 。它是以 1px 大小的 div node 為畫素,構成圖形。

操作圖例: WebFlow操作圖例
FlowNode.js

Flow object。資料結構。

/*
Copyright (c) 2006 Shih Yuncheng. All rights reserved.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License (LGPL) as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
*/

function NodeIndex(l, n) {
	this.level = l;
	this.node = n;
	return this;
}
NodeIndex.prototype.toString = function() {
	return "(" + this.level + "," + this.node + ")";
}

function FlowNode(name) {
	this.name = name;

	this.items = new Array();
	//this.items.push(null); //每一新節點的會辦者為 END
	//this.backs = new Array();
	return this;
}
FlowNode.prototype.toString = function() {
	return this.name;
}

//每一層級至少有一節點。
function FlowLevel(name) {
	this.nodes = new Array();
	this.nodes.push(new FlowNode(name));
	return this;
}
FlowLevel.prototype.addNode = function(name) {
	var nodes = this.nodes;
	//nodes[nodes.length] = new FlowNode(name);
	for(var i = 0; i < nodes.length; i++) {
	    if(nodes[i].name == name)
	        return nodes[i];
	}
	var node = new FlowNode(name);
	nodes.push(node);
	return node;
}

/*
  Flow
*/
function Flow() {
	this.levels = new Array();
	this.levels.push(new FlowLevel("")); // Node END, Flow.levels[0]
	return this;
}

Flow.prototype.getNodeIndex = function(node) {
	var levels = this.levels;
	if(node == null || node.name == "")
	    return new NodeIndex(0,0);
	var nodes;
	for(var i=1; i < levels.length; i++) {
	    nodes = levels[i].nodes;
	    var j = this.arrayHasItem(nodes, node);
	    if( j >=0 ) {
	        return new NodeIndex(i,j);
		}
	}
	return null;
}

Flow.prototype.getNodeByName = function(nodeName) {
	var levels = this.levels;
	if(nodeName == null || nodeName == "")
	    return levels[0].nodes[0]/*null*/;
	var nodes;
	for(var i=0; i < levels.length; i++) {
	    nodes = levels[i].nodes;
	    for(var j = 0; j < nodes.length; j++) {
	        if(nodes[j].name == nodeName) {
	            return nodes[j];
			}
		}
	}
	return null;
}
/*
Flow.prototype.removeNodeFromItem = function(node) {
	if(node == null)
	    return;
	var levels = this.levels;
	var nodes;
	for(var i=1; i < levels.length; i++) {
	    nodes = levels[i].nodes;

	    for(var j = 0; j < nodes.length; j++) {
			this.removeItem(i, j, node);
		}
	}
}
*/
Flow.prototype.replaceNodeInItems = function(node, replaceWithItems) {
	if(node == null)
	    return;
	var levels = this.levels;
	var nodes;
	for(var i=1; i < levels.length; i++) {
	    nodes = levels[i].nodes;

	    for(var j = 0; j < nodes.length; j++) {
			for(var k = 0; k < replaceWithItems.length; k++) {
			    if(this.hasItem(i,j,node) >= 0) {
		        	this.addItem(i, j, replaceWithItems[k]);
				}
		    }
			this.removeItem(i, j, node);
		}
	}
}

Flow.prototype.insertLevel = function(indexOfLevel, name, fromNode) {
	var levels = this.levels;
	if(indexOfLevel > levels.length)
		return null;
	if(this.getNodeByName(name) != null)
	    return null; //已存在 (可能在不同層級)

	levels.splice(indexOfLevel, 0, new FlowLevel(name));

	var node = levels[indexOfLevel].nodes[0];

	if(fromNode != null) {
		//node.items.pop();
		// remove the Node END; 新層級會包含一個新節點,而新節點的會辦者預設為 END
		while(fromNode.items.length >0) {
			node.items.push(fromNode.items.pop());
			//新層級之新節點之會辦者,為處理者的會辦者。
		}
		fromNode.items.push(node);
		//將新節點加入處理者的會辦者中
	}
	return levels[indexOfLevel];
}

Flow.prototype.removeLevel = function(indexOfLevel) {
	var levels = this.levels;
	if(indexOfLevel != levels.length - 1)
		return false;
	// NOTE: 層級移除策略未定。暫定只能移除最後一個層級。

	if(indexOfLevel > levels.length)
	    return false;
	var level = levels[indexOfLevel];
	if(level.nodes.length > 1)
	    return false;
	//只有一個參與者時,才可移除層級
	var node = level.nodes[0];

	this.replaceNodeInItems(node, node.items);

	this.levels.splice(indexOfLevel, 1);

	return true;
}

Flow.prototype.addNode = function(indexOfLevel, name, fromNode) {
	var levels = this.levels;
	if(indexOfLevel > levels.length)
		return null;
	if(this.getNodeByName(name) != null) {
	    return null; //已存在 (可能在不同層級)
	}
	else {
		var node = this.levels[indexOfLevel].addNode(name);
		if(fromNode) {
			fromNode.items.push(node);
		}
		//node.items.push(new NodeIndex(0,0));
		return node;
	}
}

Flow.prototype.removeNode = function(indexOfLevel, indexOfNode) {
	if(indexOfLevel > this.levels.length || indexOfNode > this.levels[indexOfLevel].nodes.length)
		return false;

	if(this.levels[indexOfLevel].nodes.length <= 1)
		return false; //一個層級至少要有一個節點。

	var node = this.levels[indexOfLevel].nodes[indexOfNode];
	//只有一個會辦者,且其為 END ,才可移除
	if(node.items.length > 1) {
	    return false;
	}
	else {//if(node.items.length <= 1 /*&& node.items[0] == null*/) {
		this.replaceNodeInItems(node, node.items);

		this.levels[indexOfLevel].nodes.splice(indexOfNode, 1);
	}
	return true;
}

Flow.prototype.updateNode = function(indexOfLevel, indexOfNode, nodeData) {
	if(indexOfLevel > this.levels.length || indexOfNode > this.levels[indexOfLevel].nodes.length)
		return false;

	if(this.getNodeByName(name) == null) {
	    return false; //不存在
	}

	var node = this.levels[indexOfLevel].nodes[indexOfNode];
	var oldNode = new FlowNode(node.name);
	if(nodeData['name']) {
	    node.name = nodeData['name'];
	}
	this.replaceNodeInItems(oldNode, [node]);

	return true;
}

Flow.prototype.arrayHasItem = function(ar, item) {
	for(var i = 0; i < ar.length; i++) {
		if(ar[i] == null && (item == null || item.name == ""))
			return i;
		else if(ar[i] != null && item != null) {
			if(ar[i].name == item.name)
				return i;
		}
	}
	return -1;
}

Flow.prototype.hasItem = function(indexOfLevel, indexOfNode, item) {
	if(indexOfLevel > this.levels.length || indexOfNode > this.levels[indexOfLevel].nodes.length)
		return -1;
	return this.arrayHasItem(this.levels[indexOfLevel].nodes[indexOfNode].items, item);
}

Flow.prototype.addItem = function(indexOfLevel, indexOfNode, item) {
	if(indexOfLevel > this.levels.length || indexOfNode > this.levels[indexOfLevel].nodes.length)
		return false;
    if(this.levels[indexOfLevel].nodes[indexOfNode] == null)
		return false;
	var items = this.levels[indexOfLevel].nodes[indexOfNode].items;
	if(this.hasItem(indexOfLevel, indexOfNode, item) <= -1) {
		items.push(item);
	}
	return true;
}

Flow.prototype.removeItem = function(indexOfLevel, indexOfNode, item) {
	if(indexOfLevel > this.levels.length || indexOfNode > this.levels[indexOfLevel].nodes.length)
		return false;

	var items = this.levels[indexOfLevel].nodes[indexOfNode].items;
	if(items.length <= 1)
		return false;
	if(items.length <= 1 && items[0] && items[0].name == "")
		return false;
	var i = this.hasItem(indexOfLevel, indexOfNode, item);
	if(i >= 0) {
		items.splice(i, 1);
	}
	return true;
}
/*
Flow.prototype.nodeEnd = function() {
	//return this.levels[0].nodes[0];
	return null;
}
*/
WebFlow.js

流程編輯功能。源碼不展開。

/*
Copyright (c) 2006 Shih Yuncheng. All rights reserved.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License (LGPL) as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
*/

function hiddenFlowMenu1() {
	document.getElementById("FlowMenu1").style.visibility = "hidden";
}

function mouseOverMenuItem(menuItem) {
	menuItem.className = "MenuItemMouseOver";
}

function mouseOutMenuItem(menuItem) {
	menuItem.className = "MenuItem";
}

function showFlowMenu1(obj, l, n) {
	var menu = document.getElementById("FlowMenu1");
	if(currentNodeIndex.level != l || currentNodeIndex.node != n || menu.style.visibility == 'hidden') {
		setCurrentNode(l,n);
		menu.innerHTML = "關卡: " + flow.levels[l].nodes[n]; //+ "(" + l + "," + n + ")";
		menu.innerHTML += "<ul>";
		menu.innerHTML += "<li onClick='showAddItem();' onMouseOver='mouseOverMenuItem(this);' onMouseOut='mouseOutMenuItem(this);'>新增關卡</li>";
		menu.innerHTML += "<li onClick='showUpdateNode();' onMouseOver='mouseOverMenuItem(this);' onMouseOut='mouseOutMenuItem(this);'>修改關卡</li>";
		menu.innerHTML += "<li onClick='showRemoveNode();' onMouseOver='mouseOverMenuItem(this);' onMouseOut='mouseOutMenuItem(this);'>刪除關卡</li>";
		menu.innerHTML += "<li onClick='showRemoveItem();' onMouseOver='mouseOverMenuItem(this);' onMouseOut='mouseOutMenuItem(this);'>移除會辦者</li>";
		menu.innerHTML += "</ul>";
		layoutToElement("FlowMenu1", obj);
	    menu.style.visibility = 'visible';
	}
	else {
	    menu.style.visibility = 'hidden';
	}
}

function layoutToElement(objId, layoutToObj) {
	var obj = document.getElementById(objId);

	if(obj) {
		var cs = obj.style;

		//if(cs.visibility == 'hidden') {
			var o = layoutToObj;
			var x=0;
			var y=0;
			while(o.offsetParent) {
				x+=o.offsetLeft;
				y+=o.offsetTop;
				o=o.offsetParent;
			}
			//cs.top=(y+layoutToObj.offsetHeight)+'px';
			//cs.left=x+'px';
			cs.top=y+'px';
			cs.left=(x+layoutToObj.offsetWidth)+'px';
		//	cs.visibility='visible';
		//}
		//else {
		//	cs.visibility='hidden';
		//}
	}
}

function setCurrentNode(l, n) {
	currentNodeIndex.level = l;
	currentNodeIndex.node = n;
	currentNode = flow.levels[l].nodes[n];
}

function getNewLevelName() {
	var textbox = document.getElementById("FlowMenuAddNewLevel").getElementsByName("nodeName")[0];
	return textbox.value;
}

function cancleAddNewLevel() {
	var obj = document.getElementById("FlowMenuAddNewLevel");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
	hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitAddNewLevel() {
	var obj = document.getElementById("FlowMenuAddNewLevel");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
	if(textbox.value != "") {
	    flow.insertLevel(currentNodeIndex.level+1, textbox.value, currentNode);
	    renderFlowTable();
	}
}

function showAddNewLevel() {
	var obj = document.getElementById("FlowMenuAddNewLevel");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
    layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
	textbox.focus();
}

function getNewNodeName() {
	var textbox = document.getElementById("FlowMenuAddNewNode").getElementsByName("nodeName")[0];
	return textbox.value;
}

function cancleAddNewNode() {
	var obj = document.getElementById("FlowMenuAddNewNode");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
	hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitAddNewNode() {
	var obj = document.getElementById("FlowMenuAddNewNode");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
	if(textbox.value != "") {
	    var rc;
	    if(currentNodeIndex.level >= flow.levels.length -1 ) {
		    rc = flow.insertLevel(currentNodeIndex.level+1, textbox.value, currentNode);
		}
		else {
		    rc = flow.addNode(currentNodeIndex.level+1, textbox.value, currentNode);
		}
		if(rc == null) {
			alert("不允許新增,可能已存在。");
		}
		else {
			renderFlowTable();
		}
	}
}

function hideAddNewNode() {
	var obj = document.getElementById("FlowMenuAddNewNode");
	obj.style.visibility = "hidden";
	bodyControler.removeClickHandler(hideAddNewNode);
}

function showAddNewNode() {
	var obj = document.getElementById("FlowMenuAddNewNode");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
    layoutToElement("FlowMenuAddNewNode", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
	textbox.focus();
}

function cancleUpdateNode() {
	var obj = document.getElementById("FlowMenuUpdateNode");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
	hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitUpdateNode() {
	var obj = document.getElementById("FlowMenuUpdateNode");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
	if(textbox.value != "" && textbox.value != currentNode.name) {
	    var rc;
	    rc = flow.updateNode(currentNodeIndex.level, currentNodeIndex.node, {name:textbox.value});
		renderFlowTable();
	}
}

function showUpdateNode() {
	var obj = document.getElementById("FlowMenuUpdateNode");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = currentNode.name;
    layoutToElement("FlowMenuUpdateNode", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
	textbox.focus();
}

function getItemName() {
	var textbox = document.getElementById("FlowMenuAddItem").getElementsByName("nodeName")[0];
	return textbox.value;
}

function cancleAddItem() {
	var obj = document.getElementById("FlowMenuAddItem");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitAddItem() {
	var obj = document.getElementById("FlowMenuAddItem");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
    var rc;
    if(flow.getNodeByName(textbox.value) == null) {
    	if(currentNodeIndex.level >= flow.levels.length -1 ) {
	    	rc = flow.insertLevel(currentNodeIndex.level+1, textbox.value, currentNode);
		}
		else {
	    	rc = flow.addNode(currentNodeIndex.level+1, textbox.value, currentNode);
		}
	}
	else {
	    flow.addItem(currentNodeIndex.level, currentNodeIndex.node, flow.getNodeByName(textbox.value));
	}
	renderFlowTable();
}

function showAddItem() {
	var obj = document.getElementById("FlowMenuAddItem");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";

	var nodeList = obj.getElementsByTagName("select")[0];
	while(nodeList.options.length > 0) {
		nodeList.remove(0);
	}
	nodeList.options[0] = new Option("請選擇", "0");
	for(var i = 1; i < flow.levels.length; i++) {
	    for(var j = 0; j < flow.levels[i].nodes.length; j++)  {
	        if(flow.levels[currentNodeIndex.level].nodes[currentNodeIndex.node].name
			  != flow.levels[i].nodes[j].name
			  && flow.hasItem(currentNodeIndex.level,currentNodeIndex.node,flow.levels[i].nodes[j]) <= -1) {
				nodeList.options[nodeList.options.length] = new Option(flow.levels[i].nodes[j].name, flow.levels[i].nodes[j].name);
	        }
		}
	}
	nodeList.options[nodeList.options.length] = new Option("結案", "");

    layoutToElement("FlowMenuAddItem", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
	textbox.focus();
}

function cancleRemoveItem() {
	var obj = document.getElementById("FlowMenuRemoveItem");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";
	hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitRemoveItem() {
	var obj = document.getElementById("FlowMenuRemoveItem");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
    hiddenFlowMenu1();
	obj.style.visibility = "hidden";
	//if(textbox.value != "") {
	//window.alert(flow.getNodeByName(textbox.value));
	    flow.removeItem(currentNodeIndex.level, currentNodeIndex.node, flow.getNodeByName(textbox.value));
	    renderFlowTable();
	//}
}

function showRemoveItem() {
	var obj = document.getElementById("FlowMenuRemoveItem");
	var textbox = obj.getElementsByTagName("input")[0];
	textbox.value = "";

	var nodeList = obj.getElementsByTagName("select")[0];
	while(nodeList.options.length > 0) {
		nodeList.remove(0);
	}
	nodeList.options[0] = new Option("請選擇", "0");
	for(var i = 0; i < currentNode.items.length; i++) {
		if(currentNode.items[i] == null || currentNode.items[i].name == "") {
			nodeList.options[nodeList.options.length] = new Option("結案", "");
		}
		else {
			nodeList.options[nodeList.options.length] = new Option(currentNode.items[i].name, currentNode.items[i].name);
		}
	}

    layoutToElement("FlowMenuRemoveItem", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
	textbox.focus();
}

function cancleRemoveNode() {
	var obj = document.getElementById("FlowMenuRemoveNode");
	//var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
	hiddenFlowMenu1();
	obj.style.visibility = "hidden";
}

function submitRemoveNode() {
	var obj = document.getElementById("FlowMenuRemoveNode");
	var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    //layoutToElement("FlowMenuAddNewLevel", document.getElementById("FlowMenu1"));
	obj.style.visibility = "hidden";
	hiddenFlowMenu1();
	if(currentNodeIndex.level <= 1)
	    return;
	if(flow.levels[currentNodeIndex.level].nodes.length <= 1) {
    	flow.removeLevel(currentNodeIndex.level);
	}
	else {
    	flow.removeNode(currentNodeIndex.level, currentNodeIndex.node);
	}
    renderFlowTable();
}

function showRemoveNode() {
	var obj = document.getElementById("FlowMenuRemoveNode");
	//var textbox = obj.getElementsByTagName("input")[0];
	//textbox.value = "";
    layoutToElement("FlowMenuRemoveNode", document.getElementById("FlowMenu1"));
	obj.style.visibility = "visible";
}

function renderFlowTable() {
	var div = document.getElementById("div1");

	var offsetX=0;
	var offsetY=0;
	var o = div;
	while(o.offsetParent) {
		offsetX += o.offsetLeft;
		offsetY += o.offsetTop;
		o = o.offsetParent;
	}

	var nodes = new Array( flow.levels.length);
	var maxNodes = 1;
	var maxWidth = (20+80) * maxNodes - 20;
	for(var i=0; i < flow.levels.length; i++) {
	    nodes[i] = new Array(flow.levels[i].nodes.length);
		for(var j=0; j < flow.levels[i].nodes.length; j++) {
			nodes[i][j] = document.createElement("div");
			nodes[i][j].className = "FlowNode";
		}
		if(maxNodes < flow.levels[i].nodes.length) {
		    maxNodes = flow.levels[i].nodes.length;
			maxWidth = (20+80) * maxNodes - 20;
		}
	}
	for(var i=0; i < flow.levels.length; i++) {
		nodes[i].offsetLeft = offsetX +((maxWidth - ((20+80)*flow.levels[i].nodes.length - 20)) / 2);
	}


	jg_doc.clear();
	/*for(var i=0; i < div.childNodes.length; i++) {
	    div.removeChild(div.childNodes[i]);
	}*/

	nodes[0].offsetLeft = offsetX + ((maxWidth - ((20+80)*flow.levels[0].nodes.length - 20)) / 2);
	nodes[0][0].innerHTML = "<input type='button' value='結案'/>";
   	nodes[0][0].style.top = offsetY + ((flow.levels.length-1) * (50 + 40)) + 'px';
   	nodes[0][0].style.left = offsetX + (nodes[0].offsetLeft + (20 + 80)) + 'px';
   	nodes[0][0].getElementsByTagName("input")[0].style['width'] = 80 + 'px';
   	nodes[0][0].getElementsByTagName("input")[0].style['height'] = 40 + 'px';
   	div.appendChild(nodes[0][0]);


	for(var i = 1; i < flow.levels.length; i++) {
		for(var j = 0; j < flow.levels[i].nodes.length; j++) {
			nodes[i][j].innerHTML = "<input type='button' value='"
				+ flow.levels[i].nodes[j] + "' onClick='showFlowMenu1(this,"+i+","+j+");'/>";//+ flow.levels[i].nodes[j].items.join(",");
			if(nodes[i][j].addEventListener) {
				//nodes[i][j].addEventListener("mouseover", function(event){this.className = 'FlowMenuItemMouseOver';}, false);
				//nodes[i][j].addEventListener("mouseout", function(event){this.className = 'FlowMenuItem';}, false);
			}
			else { // M$IE
				//nodes[i][j].onmouseover = function(){this.className = 'FlowMenuItemMouseOver';};
				//nodes[i][j].onmouseout = function(){this.className = 'FlowMenuItem';};
			}
	    	nodes[i][j].style.top = offsetY +((i-1) * (50 + 40)) + 'px';
	    	nodes[i][j].style.left = offsetX +(nodes[i].offsetLeft + (j+1) * (20 + 80)) + 'px';
	    	nodes[i][j].getElementsByTagName("input")[0].style['width'] = 80 + 'px';
	    	nodes[i][j].getElementsByTagName("input")[0].style['height'] = 40 + 'px';

	    	div.appendChild(nodes[i][j]);
		}
	}

	//var EndNodeIndex = new NodeIndex(flow.levels.length,0);

	jg_doc.setColor("#0000ff"); // green
	//jg_doc.drawLine(100, 50, 200, 400);
	for(var i = 1; i < flow.levels.length; i++) {
	    //var currentLevelLeft = (maxWidth - ((20+80)*flow.levels[i].nodes.length - 20)) / 2;
		for(var j = 0; j < flow.levels[i].nodes.length; j++) {
			var item;
			var nodeIndex;
		    for(var k = 0; k < flow.levels[i].nodes[j].items.length; k++) {
		        item = flow.levels[i].nodes[j].items[k];
				if(item == null || item.name=="") {
				    //nodeIndex = EndNodeIndex;
					jg_doc.drawLine(
						offsetX+(nodes[i].offsetLeft+(j+1) * (20 + 80) + 40),
						offsetY+((i-1)*(50 + 40)+40),
						offsetX+(nodes[0].offsetLeft+(0+1)*(20 + 80)+40),
						offsetY+((flow.levels.length-1) * (50+40)));
				}
				else {
					nodeIndex = flow.getNodeIndex(item);
					if(nodeIndex.level > i) {
						jg_doc.drawLine(
							offsetX+(nodes[i].offsetLeft+(j+1) * (20 + 80) + 40),
							offsetY+((i-1)*(50 + 40)+40),
							offsetX+(nodes[nodeIndex.level].offsetLeft+(nodeIndex.node+1)*(20 + 80)+40),
							offsetY+((nodeIndex.level-1) * (50+40)));
					}
					else {
						jg_doc.drawLine(
							offsetX+(nodes[i].offsetLeft+(j+1) * (20 + 80) + 40),
							offsetY+((i-1)*(50 + 40)),
							offsetX+(nodes[nodeIndex.level].offsetLeft+(nodeIndex.node+1)*(20 + 80)+40),
							offsetY+((nodeIndex.level-1) * (50+40)+40));
					}
				}
				//window.alert(item + nodeIndex);
		    }
		}
	}
	jg_doc.paint(); // draws, in this case, directly into the document

}
FlowTableSubmit.js

送出編輯內容。此處僅將結果顯示於網頁上,並未送回伺服端。請依實作規格自行補上伺服端的部份。

我當時是把內容轉成 JSON 格式後送回伺服端儲存。伺服端是以 C#/ASP.Net 實作。我並未擁有伺服端的著作權,故不發佈伺服端源碼。

/*
Copyright (c) 2006 Shih Yuncheng. All rights reserved.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License (LGPL) as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
*/

function openString() {
	document.getElementById("div1").innerHTML = "<ol>";
}
function closeString() {
	document.getElementById("div1").innerHTML += "</ol>";
}
function addString(v) {
	var div1 = document.getElementById("div1");
	div1.innerHTML += "<li>" + v.name + "<br/> Items: ";
	for(var i = 0; i < v.items.length; i++) {
		div1.innerHTML += v.items[i] + ",";
	}
	div1.innerHTML += "<br/> Backs: ";
	for(var i = 0; i < v.backs.length; i++) {
		div1.innerHTML += v.backs[i] + ",";
	}

	div1.innerHTML += "</li>";
}

function addRow(tableId, level, n, node) {
	var table1 = document.getElementById(tableId).getElementsByTagName("tbody")[0];

	var tdLevel = document.createElement("td");
	tdLevel.innerHTML = level;

	var tdName = document.createElement("td");
	tdName.innerHTML = node;

	var tdItems = document.createElement("td");
	for(var i = 0; i < node.items.length; i++) {
	    if(i>0)
            tdItems.innerHTML += ",";
		if(node.items[i] != null)
	    	tdItems.innerHTML += node.items[i];
		else
            tdItems.innerHTML += "結案";
	}

	var tr = document.createElement("tr");
	tr.appendChild(tdLevel);
	tr.appendChild(tdName);
	tr.appendChild(tdItems);
//	tr.appendChild(tdBacks);

	table1.appendChild(tr);
}
function X_renderFlowTable() {
	document.getElementById("div1").style.visibility = 'hidden';
	document.getElementById("table2").style.visibility='visible';
	var table1 = document.getElementById("table2").getElementsByTagName("tbody")[0];
	var trs = table1.getElementsByTagName("tr");
    while(trs.length > 1) {
		table1.deleteRow(1); //using Table Object Model;
	}

	for(var l = 1; l < flow.levels.length; l++) {
		for(var n = 0; n < flow.levels[l].nodes.length; n++) {
	    	addRow("table2", l, n, flow.levels[l].nodes[n]);
		}
	}
}
WebFlow.sql

WebFlow SQL schema.

-- when insert, the column which IDENTITY could be ignored.
-- Note: IDENTITY only in M$ SQL Server, not SQL-92 Standard.
create table WebFlowMaster (
	WebFlowID int IDENTITY(1,1) not null primary key,
	Description text not null default '',
	ModifyEmployee varchar(50) not null,
	ModifyMemo text not null default '',
	ModifyTime datetime not null
);

create table WebFlowDetail (
	WebFlowID int not null,
	FlowLevel int not null,
	NodeID varchar(50) not null,
	ItemIDs varchar(1000) not null
	-- text will not allow to insert with select union in SQLServer.
	--SQL Sever線上說明:
    -- SQL Server 版本 7.0 execute_statement 無法包含會
    --傳回 text  image 資料行的延伸預存程序
    --此行為是對先前 SQL Server 版本的一項變更
);

--select WFM.WebFlowID.*, N.FlowLevel, N.NodeID, I.ItemID
--from WebFlowMaster as WFM
--inner join (WebFlowNode as N inner join WebFlowItem as I
--  on N.WebFlowID = I.WebFlowID and N.NodeID = I.NodeID)
--on WFM.WebFlowID = N.WebFlowID

create procedure dt_WebFlowMasterInsert (
	@WebFlowID int output,
	@Description text,
	@ModifyEmployee varchar(50),
	@ModifyMemo text,
	@ModifyTime datetime
)
AS
BEGIN
Insert Into WebFlowMaster (
	Description,
	ModifyEmployee,
	ModifyMemo,
	ModifyTime
)
Values (
	@Description,
	@ModifyEmployee,
	@ModifyMemo,
	@ModifyTime
);

Set @WebFlowID = SCOPE_IDENTITY();

END
GO

create procedure dt_WebFlowMasterUpdate (
	@WebFlowID int,
	@Description text,
	@ModifyEmployee varchar(50),
	@ModifyMemo text,
	@ModifyTime datetime
)
AS
BEGIN
Update WebFlowMaster set
	Description = @Description,
	ModifyEmployee = @ModifyEmployee,
	ModifyMemo = @ModifyMemo,
	ModifyTime = @ModifyTime
where WebFlowID = @WebFlowID;

END
GO
WebFlow.css

視覺元件外觀。

.FlowNode {
	position: absolute;
	width: 80px;
	height: 40px;
	border: 1px solid black;
	z-index: 100;
}

.FlowMenu1 {
	position:absolute;
	background-color: lightgrey;
	border: 1px solid black;
	visibility: hidden;
	z-index: 500;
	padding: 10px;
	width: 120px;
}
.FlowSubMenu1 {
	position:absolute;
	background-color: lightgrey;
	border: 1px solid black;
	visibility: hidden;
	z-index: 500;
	padding: 10px;
	width: 360px;
	text-align: center;
}
.MenuItemMouseOver {
	background-color: #fcfcfc;
}

.MenuItem {
	background-color: lightgrey;
}

#div1 {
	position:relative;
	text-align: center;
}
#div2 {
	position:absolute;
	left: 200px;
	top: 20px;
}
WebFlow.html

主程式。

<html>
<head>
<link rel="stylesheet" href="WebFlow.css" type="text/css" />
<script type="text/javascript" src="wz_jsgraphics.js"></script>
<script type="text/javascript" src="FlowNode.js"></script>
<script type="text/javascript" src="WebFlow.js"></script>
<script type="text/javascript" src="FlowTableSubmit.js"></script>
</head>

<body onClick="bodyControler.clickHandler();">

<span id="FlowMenu1" class="FlowMenu1"></span>

<span id="FlowMenuAddNewLevel" class="FlowSubMenu1">
    <label>關卡名稱: <input type="text" name="nodeName" /></label><br/>
    <input type="button" value="確定" onClick="submitAddNewLevel();"/>
    <input type="button" value="取消" onClick="cancleAddNewLevel();" />
</span>

<span id="FlowMenuAddNewNode" class="FlowSubMenu1">
    <fieldset>
    <legend>新增關卡</legend>
    <label>關卡名稱: <input type="text" name="nodeName" /></label><br/>
    </fieldset>
    <p align="center">
    <input type="button" value="確定" onClick="submitAddNewNode();"/>
    <input type="button" value="取消" onClick="cancleAddNewNode();" />
    </p>
</span>

<span id="FlowMenuUpdateNode" class="FlowSubMenu1">
    <fieldset>
    <legend>修改關卡</legend>
    <label>關卡名稱: <input type="text" name="nodeName" /></label><br/>
    </fieldset>
    <p align="center">
    <input type="button" value="確定" onClick="submitUpdateNode();"/>
    <input type="button" value="取消" onClick="cancleUpdateNode();" />
    </p>
</span>

<span id="FlowMenuAddItem" class="FlowSubMenu1">
    <fieldset>
    <legend>新增會辦者</legend>
    <label>關卡名稱: <input type="text" name="nodeName" />
    <select name="nodeList" onChange="if(this.selectedIndex > 0){document.getElementById('FlowMenuAddItem').getElementsByTagName('input')[0].value = this.options[this.selectedIndex].value};"></select></label><br/>
    </fieldset>
    <p align="center">
    <input type="button" value="確定" onClick="submitAddItem();"/>
    <input type="button" value="取消" onClick="cancleAddItem();" />
    </p>
</span>

<span id="FlowMenuRemoveItem" class="FlowSubMenu1">
    <fieldset>
    <legend>移除會辦者</legend>
    <label>關卡名稱: <input type="text" name="nodeName" />
    <select name="nodeList" onChange="if(this.selectedIndex > 0){document.getElementById('FlowMenuRemoveItem').getElementsByTagName('input')[0].value = this.options[this.selectedIndex].value};"></select></label></label><br/>
    </fieldset>
    <p align="center">
    <input type="button" value="確定" onClick="submitRemoveItem();"/>
    <input type="button" value="取消" onClick="cancleRemoveItem();" />
    </p>
</span>

<span id="FlowMenuRemoveNode" class="FlowSubMenu1">
    <label>確定刪除?</label><br/>
    <input type="button" value="確定" onClick="submitRemoveNode();"/>
    <input type="button" value="取消" onClick="cancleRemoveNode();" />
</span>

<table id="table2" border="1" style='visibility:hidden'>
<tboby>
<tr>
	<td>Level</td>
	<td>Node</td>
	<td>Items</td>
</tr>
</tbody>
</table>

<div id="div1" class="div1">
</div>

<canvas id="div2">
</canvas>

<form>
    <input type='button' value='送出表格' onClick='X_renderFlowTable();'/>
</form>

<script type="text/javascript">
var currentNode;
var currentNodeIndex = new NodeIndex(0,0);
var jg_doc = new jsGraphics("div1");

var canvas = document.getElementById('div2');
if (canvas.getContext) {
	var ctx = canvas.getContext('2d');
}

var bodyControler = {
	currentNode: null,
	currentNodeIndex: new NodeIndex(0,0),
	jg: new jsGraphics("div1"),

	currentObject: null,

	_clickHandlers: new Array(),

	addClickHandler: function(func) {
		this._clickHandlers.push(func);
	},

	removeClickHandler: function(func) {
	    for(var i = 0; i < this._clickHandlers.length; i++) {
	        if(this._clickHandlers[i] === func) {
	            this._clickHandlers.splice(i, 1);
	            break;
			}
		}
	},

	clickHandler: function() {
		for(var i = 0; i < this._clickHandlers.length; i++) {
		    this._clickHandlers[i]();
		}
	}
};

/*=============================================*/

var flow = new Flow();

flow.insertLevel(1, "申請人", null);
flow.addItem(1,0,null);
//flow.insertLevel(2, "A1", flow.levels[1].nodes[0]);
//flow.addNode(2,"A2",flow.levels[1].nodes[0])

renderFlowTable();

</script>
</body>
</html>

這是我一年前在某公司任職時寫的,算是程式雛型。當時最主要的困難點是在瀏覽器頁面上「畫圖/線」。試了包括 SVG 的多種方式後皆不可行,最後還是用 wz_jsgraphics.js 解決。日後待網頁向量圖形規格確認與普及後,再改寫吧。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/4036079.html