1// Dokuwiki table editing plugin
2// Author: Bohumir Zamecnik <bohumir|zamecnik|org>
3// Last modified: 22.11.2008
4// License: GNU GPL 2
5// Website: http://zamecnik.org/projekty/dokuwiki_tableedit
6//
7// Purpose:
8// The goal of this plugin is to simplify handling Dokuwiki table columns.
9// It's not easy to move some columns around, add them or remove them by hand.
10// This plugin will offer a friendly user iterface to underlying manipulation
11// with the table markup using regular expression.
12
13//function replaceColumns(inputText, replaceSpec, columnChar, defaultContent) {
14function replaceColumns(inputText, replaceSpec) {
15	var headChar = "^";
16	var headDefault = "";
17	var normalChar = "|";
18	var normalDefault = " ";
19	var nColumns = countColumns(inputText);
20
21	for (var i = 0; i < replaceSpec.length; i++) {
22		if ((replaceSpec[i] <= 0) || (replaceSpec[i] > nColumns)) {
23			replaceSpec[i] = "&"; // will be replaced by a default char
24		}
25		replaceSpec[i] = "$" + replaceSpec[i];
26	}
27
28	var pattern = "^" + multiple("#", nColumns)
29		.join("([^#]*)") + "[ \t]*$";
30
31	var replacePattern = replaceSpec.join("#");
32	if (replaceSpec.length > 0) {
33		replacePattern = "#" + replacePattern + "#";
34	}
35
36	var currentPattern = new RegExp();
37
38	currentPattern.compile(pattern.replace(/#/g, "\\"+headChar), "gm");
39	currentReplacePattern = replacePattern.replace(/#/g, headChar).replace(/\$&/g, headDefault);
40	var text = inputText.replace(currentPattern, currentReplacePattern);
41
42	currentPattern.compile(pattern.replace(/#/g, "\\"+normalChar),"gm");
43	currentReplacePattern = replacePattern.replace(/#/g, normalChar).replace(/\$&/g, normalDefault)
44	return text.replace(currentPattern, currentReplacePattern);
45}
46
47// n times text into an array
48function multiple(text, n) {
49	if (n <= 0) { return new Array(); }
50	var tmp = new Array();
51	for (var i = 0; i < n; i++) {
52		tmp[i] = text;
53	}
54	return tmp;
55}
56
57// maximum number of columns in a table
58function countColumns(table) {
59	var maxColumns = 0;
60	var lines = table.split("\n");
61	var pattern = new RegExp("[\\^\\|]","g");
62
63	for (var i = 0; i < lines.length; i++) {
64		var result = lines[i].match(pattern);
65		var columns = (result != null ? result.length : 0);
66		maxColumns = (columns > maxColumns ? columns : maxColumns);
67	}
68	return maxColumns;
69}
70
71// add ^ or | symbols up to maxColumns
72function normalizeTable(table) {
73	var maxColumns = countColumns(table);
74	var lines = table.split("\n");
75	var headPattern = new RegExp("\\^","g");
76	var normalPattern = new RegExp("\\|","g");
77	for (var i = 0; i < lines.length; i++) {
78		// resolve which character to add
79		var c = lines[i].charAt(0); // ...using the first character
80		// count column count on current line
81		var columns = 0;
82		if (c == "^") {
83			result = lines[i].match(headPattern);
84			columns = (result != null ? result.length : 0);
85		} else if (c == "|") {
86			result = lines[i].match(normalPattern);
87			columns = (result != null ? result.length : 0);
88		} else {
89			break;
90		}
91
92		//for (k = 0; k < lines[i].length; k++) {
93		//	if (lines[i].charAt(k) == c) {
94		//		columns++;
95		//	}
96		//}
97		// add up to maxColumns count
98		for (var j = 0; j < maxColumns - columns; j++) {
99			lines[i] += " " + c;
100		}
101	}
102	return lines.join("\n");
103}
104
105function parseColumnOrder(orderString) {
106	return (orderString != "" ? orderString.split(",") : new Array());
107}
108
109// Get position of the caret (cursor) in a field with given id
110function caretPosition(field) {
111	//var field = document.getElementById(editId);
112
113	if (field != null) {
114		field.focus();
115	  if (document.selection) {
116	  	// IE
117			return getCaretPositionIE(field);
118	  } else if (field.selectionStart || field.selectionStart == '0') {
119	  	// Mozilla / Netscape
120	    return field.selectionStart;
121	  }
122  }
123  return 0;
124}
125
126function getCaretPositionIE(textarea) {
127	// Source: http://linebyline.blogspot.com/2006/11/textarea-cursor-position-in-internet.html
128	// TODO:
129	// - simplify a bit, we need only to compute startPoint
130	// - find out what about the license...
131
132
133	//var textarea = document.getElementById("myTextArea");
134	textarea.focus();
135	var selection_range = document.selection.createRange().duplicate();
136
137	if (selection_range.parentElement() != textarea) {
138		// Check that the selection is actually in our textarea
139		return 0;
140	}
141	// Create three ranges, one containing all the text before the selection,
142	// one containing all the text in the selection (this already exists), and one containing all
143	// the text after the selection.
144	var before_range = document.body.createTextRange();
145	before_range.moveToElementText(textarea);                    // Selects all the text
146	before_range.setEndPoint("EndToStart", selection_range);     // Moves the end where we need it
147
148	var after_range = document.body.createTextRange();
149	after_range.moveToElementText(textarea);                     // Selects all the text
150	after_range.setEndPoint("StartToEnd", selection_range);      // Moves the start where we need it
151
152	var before_finished = false, selection_finished = false, after_finished = false;
153	var before_text, untrimmed_before_text, selection_text, untrimmed_selection_text, after_text, untrimmed_after_text;
154
155	// Load the text values we need to compare
156	before_text = untrimmed_before_text = before_range.text;
157	selection_text = untrimmed_selection_text = selection_range.text;
158	after_text = untrimmed_after_text = after_range.text;
159
160	// Check each range for trimmed newlines by shrinking the range by 1 character and seeing
161	// if the text property has changed.  If it has not changed then we know that IE has trimmed
162	// a \r\n from the end.
163	do {
164	  if (!before_finished) {
165	      if (before_range.compareEndPoints("StartToEnd", before_range) == 0) {
166	          before_finished = true;
167	      } else {
168	          before_range.moveEnd("character", -1)
169	          if (before_range.text == before_text) {
170	              untrimmed_before_text += "\r\n";
171	          } else {
172	              before_finished = true;
173	          }
174	      }
175	  }
176	  if (!selection_finished) {
177	      if (selection_range.compareEndPoints("StartToEnd", selection_range) == 0) {
178	          selection_finished = true;
179	      } else {
180	          selection_range.moveEnd("character", -1)
181	          if (selection_range.text == selection_text) {
182	              untrimmed_selection_text += "\r\n";
183	          } else {
184	              selection_finished = true;
185	          }
186	      }
187	  }
188	  if (!after_finished) {
189	      if (after_range.compareEndPoints("StartToEnd", after_range) == 0) {
190	          after_finished = true;
191	      } else {
192	          after_range.moveEnd("character", -1)
193	          if (after_range.text == after_text) {
194	              untrimmed_after_text += "\r\n";
195	          } else {
196	              after_finished = true;
197	          }
198	      }
199	  }
200
201	} while ((!before_finished || !selection_finished || !after_finished));
202
203	// Untrimmed success test to make sure our results match what is actually in the textarea
204	// This can be removed once you're confident it's working correctly
205	var untrimmed_text = untrimmed_before_text + untrimmed_selection_text + untrimmed_after_text;
206	var untrimmed_successful = false;
207	if (textarea.value == untrimmed_text) {
208	  untrimmed_successful = true;
209	}
210	// ** END Untrimmed success test
211
212	var startPoint = untrimmed_before_text.length;
213	var endPoint = startPoint + untrimmed_selection_text.length;
214	var selected_text = untrimmed_selection_text;
215
216	//alert("Start Index: " + startPoint + "\nEnd Index: " + endPoint + "\nSelected Text\n'" + selected_text + "'");
217	return startPoint;
218}
219
220// Count columns of table under cursor given a textarea
221function countTableColumns(fieldId) {
222	field = document.getElementById(fieldId);
223	if (field == null) { return 0; }
224	var parts = extractTable(field);
225	return countColumns(parts[1]);
226}
227
228// Prepare default value for order input field
229function defaultOrderValue(tableFieldId) {
230	if (tableFieldId == null) {
231		return "";
232	}
233	var defaultOrder = new Array();
234	var columns = countTableColumns(tableFieldId) - 1;
235	columns = (columns >= 0 ? columns : 0);
236	for (var i = 0; i < columns; i++) {
237		defaultOrder[i] = i + 1;
238	}
239	return defaultOrder.join(",");
240}
241
242// Find a continuous table which lies on "currentPosition" in "table".
243// Return array [before table, table, after table] (all could be empty).
244function extractTable(field) {
245	var table = field.value;
246	var currentPosition = caretPosition(field);
247	var emptyLine = "\n\n";
248	if (/MSIE/.test(navigator.userAgent)) {
249		emptyLine = "\r\n\r\n";
250	}
251	var startPos = table.lastIndexOf(emptyLine, currentPosition);
252	startPos = (startPos != -1 ? (startPos + emptyLine.length) : 0); // +2 for \n\n
253	var endPos = table.indexOf(emptyLine, currentPosition);
254	endPos = (endPos != -1 ? endPos : table.length);
255	var array = new Array();
256	array[0] = table.substring(0, startPos);
257	array[1] = table.substring(startPos, endPos);
258	array[2] = table.substring(endPos, table.length);
259	return array;
260}
261
262// Main editing function
263function editTable(fieldId, order) {
264	inputField = document.getElementById(fieldId);
265		var parts = extractTable(inputField);
266	inputField.value = parts[0] + replaceColumns(normalizeTable(parts[1]),
267		parseColumnOrder(order)) + parts[2];
268}
269
270// Callback for Dokuwiki toolbar button
271function addBtnActionTableedit(btn, props, edid)
272{
273    eval("btn.onclick = function(){var order = defaultOrderValue('wiki__text');"
274				+ "var result = prompt('"+jsEscape(props['prompt'])+"', order);"
275				+ "if (result != null) {editTable('wiki__text', result)};return false;}");
276    return true;
277}
278