From 053f6f208092e9b986f54ca84726fde11fc44e57 Mon Sep 17 00:00:00 2001
From: Jonathan Wilkes <jon.w.wilkes@gmail.com>
Date: Thu, 1 Oct 2015 20:33:40 -0400
Subject: [PATCH] first draft of a functioning "Find" bar for the Pd console

---
 pd/nw/console_search.js | 632 ++++++++++++++++++++++++++++++++++++++++
 pd/nw/css/default.css   |  19 +-
 pd/nw/index.html        |  18 +-
 pd/nw/index.js          | 157 +++++++++-
 4 files changed, 807 insertions(+), 19 deletions(-)
 create mode 100644 pd/nw/console_search.js

diff --git a/pd/nw/console_search.js b/pd/nw/console_search.js
new file mode 100644
index 000000000..d288c231c
--- /dev/null
+++ b/pd/nw/console_search.js
@@ -0,0 +1,632 @@
+/**
+ * findAndReplaceDOMText v 0.4.3
+ * @author James Padolsey http://james.padolsey.com
+ * @license http://unlicense.org/UNLICENSE
+ *
+ * Matches the text of a DOM node against a regular expression
+ * and replaces each match (or node-separated portions of the match)
+ * in the specified element.
+ */
+ (function (root, factory) {
+     if (typeof module === 'object' && module.exports) {
+         // Node/CommonJS
+         module.exports = factory();
+     } else if (typeof define === 'function' && define.amd) {
+         // AMD. Register as an anonymous module.
+         define(factory);
+     } else {
+         // Browser globals
+         root.findAndReplaceDOMText = factory();
+     }
+ }(this, function factory() {
+
+	var PORTION_MODE_RETAIN = 'retain';
+	var PORTION_MODE_FIRST = 'first';
+
+	var doc = document;
+	var toString = {}.toString;
+	var hasOwn = {}.hasOwnProperty;
+
+	function isArray(a) {
+		return toString.call(a) == '[object Array]';
+	}
+
+	function escapeRegExp(s) {
+		return String(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+	}
+
+	function exposed() {
+		// Try deprecated arg signature first:
+		return deprecated.apply(null, arguments) || findAndReplaceDOMText.apply(null, arguments);
+	}
+
+	function deprecated(regex, node, replacement, captureGroup, elFilter) {
+		if ((node && !node.nodeType) && arguments.length <= 2) {
+			return false;
+		}
+		var isReplacementFunction = typeof replacement == 'function';
+
+		if (isReplacementFunction) {
+			replacement = (function(original) {
+				return function(portion, match) {
+					return original(portion.text, match.startIndex);
+				};
+			}(replacement));
+		}
+
+		// Awkward support for deprecated argument signature (<0.4.0)
+		var instance = findAndReplaceDOMText(node, {
+
+			find: regex,
+
+			wrap: isReplacementFunction ? null : replacement,
+			replace: isReplacementFunction ? replacement : '$' + (captureGroup || '&'),
+
+			prepMatch: function(m, mi) {
+
+				// Support captureGroup (a deprecated feature)
+
+				if (!m[0]) throw 'findAndReplaceDOMText cannot handle zero-length matches';
+
+				if (captureGroup > 0) {
+					var cg = m[captureGroup];
+					m.index += m[0].indexOf(cg);
+					m[0] = cg;
+				}
+		 
+				m.endIndex = m.index + m[0].length;
+				m.startIndex = m.index;
+				m.index = mi;
+
+				return m;
+			},
+			filterElements: elFilter
+		});
+
+		exposed.revert = function() {
+			return instance.revert();
+		};
+
+		return true;
+	}
+
+	/** 
+	 * findAndReplaceDOMText
+	 * 
+	 * Locates matches and replaces with replacementNode
+	 *
+	 * @param {Node} node Element or Text node to search within
+	 * @param {RegExp} options.find The regular expression to match
+	 * @param {String|Element} [options.wrap] A NodeName, or a Node to clone
+	 * @param {String|Function} [options.replace='$&'] What to replace each match with
+	 * @param {Function} [options.filterElements] A Function to be called to check whether to
+	 *	process an element. (returning true = process element,
+	 *	returning false = avoid element)
+	 */
+	function findAndReplaceDOMText(node, options) {
+		return new Finder(node, options);
+	}
+
+	exposed.NON_PROSE_ELEMENTS = {
+		br:1, hr:1,
+		// Media / Source elements:
+		script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
+		// Input elements
+		input:1, textarea:1, select:1, option:1, optgroup: 1, button:1
+	};
+
+	exposed.NON_CONTIGUOUS_PROSE_ELEMENTS = {
+
+		// Elements that will not contain prose or block elements where we don't
+		// want prose to be matches across element borders:
+
+		// Block Elements
+		address:1, article:1, aside:1, blockquote:1, dd:1, div:1,
+		dl:1, fieldset:1, figcaption:1, figure:1, footer:1, form:1, h1:1, h2:1, h3:1,
+		h4:1, h5:1, h6:1, header:1, hgroup:1, hr:1, main:1, nav:1, noscript:1, ol:1,
+		output:1, p:1, pre:1, section:1, ul:1,
+		// Other misc. elements that are not part of continuous inline prose:
+		br:1, li: 1, summary: 1, dt:1, details:1, rp:1, rt:1, rtc:1,
+		// Media / Source elements:
+		script:1, style:1, img:1, video:1, audio:1, canvas:1, svg:1, map:1, object:1,
+		// Input elements
+		input:1, textarea:1, select:1, option:1, optgroup: 1, button:1,
+		// Table related elements:
+		table:1, tbody:1, thead:1, th:1, tr:1, td:1, caption:1, col:1, tfoot:1, colgroup:1
+
+	};
+
+	exposed.NON_INLINE_PROSE = function(el) {
+		return hasOwn.call(exposed.NON_CONTIGUOUS_PROSE_ELEMENTS, el.nodeName.toLowerCase());
+	};
+
+	// Presets accessed via `options.preset` when calling findAndReplaceDOMText():
+	exposed.PRESETS = {
+		prose: {
+			forceContext: exposed.NON_INLINE_PROSE,
+			filterElements: function(el) {
+				return !hasOwn.call(exposed.NON_PROSE_ELEMENTS, el.nodeName.toLowerCase());
+			}
+		}
+	};
+
+	exposed.Finder = Finder;
+
+	/**
+	 * Finder -- encapsulates logic to find and replace.
+	 */
+	function Finder(node, options) {
+
+		var preset = options.preset && exposed.PRESETS[options.preset];
+
+		options.portionMode = options.portionMode || PORTION_MODE_RETAIN;
+
+		if (preset) {
+			for (var i in preset) {
+				if (hasOwn.call(preset, i) && !hasOwn.call(options, i)) {
+					options[i] = preset[i];
+				}
+			}
+		}
+
+		this.node = node;
+		this.options = options;
+
+		// ENable match-preparation method to be passed as option:
+		this.prepMatch = options.prepMatch || this.prepMatch;
+
+		this.reverts = [];
+
+		this.matches = this.search();
+
+		if (this.matches.length) {
+			this.processMatches();
+		}
+
+	}
+
+	Finder.prototype = {
+
+		/**
+		 * Searches for all matches that comply with the instance's 'match' option
+		 */
+		search: function() {
+
+			var match;
+			var matchIndex = 0;
+			var offset = 0;
+			var regex = this.options.find;
+			var textAggregation = this.getAggregateText();
+			var matches = [];
+			var self = this;
+
+			regex = typeof regex === 'string' ? RegExp(escapeRegExp(regex), 'g') : regex;
+
+			matchAggregation(textAggregation);
+
+			function matchAggregation(textAggregation) {
+				for (var i = 0, l = textAggregation.length; i < l; ++i) {
+
+					var text = textAggregation[i];
+
+					if (typeof text !== 'string') {
+						// Deal with nested contexts: (recursive)
+						matchAggregation(text);
+						continue;
+					}
+
+					if (regex.global) {
+						while (match = regex.exec(text)) {
+							matches.push(self.prepMatch(match, matchIndex++, offset));
+						}
+					} else {
+						if (match = text.match(regex)) {
+							matches.push(self.prepMatch(match, 0, offset));
+						}
+					}
+
+					offset += text.length;
+				}
+			}
+
+			return matches;
+
+		},
+
+		/**
+		 * Prepares a single match with useful meta info:
+		 */
+		prepMatch: function(match, matchIndex, characterOffset) {
+
+			if (!match[0]) {
+				throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
+			}
+	 
+			match.endIndex = characterOffset + match.index + match[0].length;
+			match.startIndex = characterOffset + match.index;
+			match.index = matchIndex;
+
+			return match;
+		},
+
+		/**
+		 * Gets aggregate text within subject node
+		 */
+		getAggregateText: function() {
+
+			var elementFilter = this.options.filterElements;
+			var forceContext = this.options.forceContext;
+
+			return getText(this.node);
+
+			/**
+			 * Gets aggregate text of a node without resorting
+			 * to broken innerText/textContent
+			 */
+			function getText(node, txt) {
+
+				if (node.nodeType === 3) {
+					return [node.data];
+				}
+
+				if (elementFilter && !elementFilter(node)) {
+					return [];
+				}
+
+				var txt = [''];
+				var i = 0;
+
+				if (node = node.firstChild) do {
+
+					if (node.nodeType === 3) {
+						txt[i] += node.data;
+						continue;
+					}
+
+					var innerText = getText(node);
+
+					if (
+						forceContext &&
+						node.nodeType === 1 &&
+						(forceContext === true || forceContext(node))
+					) {
+						txt[++i] = innerText;
+						txt[++i] = '';
+					} else {
+						if (typeof innerText[0] === 'string') {
+							// Bridge nested text-node data so that they're
+							// not considered their own contexts:
+							// I.e. ['some', ['thing']] -> ['something']
+							txt[i] += innerText.shift();
+						}
+						if (innerText.length) {
+							txt[++i] = innerText;
+							txt[++i] = '';
+						}
+					}
+				} while (node = node.nextSibling);
+
+				return txt;
+
+			}
+			
+		},
+
+		/** 
+		 * Steps through the target node, looking for matches, and
+		 * calling replaceFn when a match is found.
+		 */
+		processMatches: function() {
+
+			var matches = this.matches;
+			var node = this.node;
+			var elementFilter = this.options.filterElements;
+
+			var startPortion,
+				endPortion,
+				innerPortions = [],
+				curNode = node,
+				match = matches.shift(),
+				atIndex = 0, // i.e. nodeAtIndex
+				matchIndex = 0,
+				portionIndex = 0,
+				doAvoidNode,
+				nodeStack = [node];
+
+			out: while (true) {
+
+				if (curNode.nodeType === 3) {
+
+					if (!endPortion && curNode.length + atIndex >= match.endIndex) {
+
+						// We've found the ending
+						endPortion = {
+							node: curNode,
+							index: portionIndex++,
+							text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex),
+							indexInMatch: atIndex - match.startIndex,
+							indexInNode: match.startIndex - atIndex, // always zero for end-portions
+							endIndexInNode: match.endIndex - atIndex,
+							isEnd: true
+						};
+
+					} else if (startPortion) {
+						// Intersecting node
+						innerPortions.push({
+							node: curNode,
+							index: portionIndex++,
+							text: curNode.data,
+							indexInMatch: atIndex - match.startIndex,
+							indexInNode: 0 // always zero for inner-portions
+						});
+					}
+
+					if (!startPortion && curNode.length + atIndex > match.startIndex) {
+						// We've found the match start
+						startPortion = {
+							node: curNode,
+							index: portionIndex++,
+							indexInMatch: 0,
+							indexInNode: match.startIndex - atIndex,
+							endIndexInNode: match.endIndex - atIndex,
+							text: curNode.data.substring(match.startIndex - atIndex, match.endIndex - atIndex)
+						};
+					}
+
+					atIndex += curNode.data.length;
+
+				}
+
+				doAvoidNode = curNode.nodeType === 1 && elementFilter && !elementFilter(curNode);
+
+				if (startPortion && endPortion) {
+
+					curNode = this.replaceMatch(match, startPortion, innerPortions, endPortion);
+
+					// processMatches has to return the node that replaced the endNode
+					// and then we step back so we can continue from the end of the 
+					// match:
+
+					atIndex -= (endPortion.node.data.length - endPortion.endIndexInNode);
+
+					startPortion = null;
+					endPortion = null;
+					innerPortions = [];
+					match = matches.shift();
+					portionIndex = 0;
+					matchIndex++;
+
+					if (!match) {
+						break; // no more matches
+					}
+
+				} else if (
+					!doAvoidNode &&
+					(curNode.firstChild || curNode.nextSibling)
+				) {
+					// Move down or forward:
+					if (curNode.firstChild) {
+						nodeStack.push(curNode);
+						curNode = curNode.firstChild;
+					} else {
+						curNode = curNode.nextSibling;
+					}
+					continue;
+				}
+
+				// Move forward or up:
+				while (true) {
+					if (curNode.nextSibling) {
+						curNode = curNode.nextSibling;
+						break;
+					}
+					curNode = nodeStack.pop();
+					if (curNode === node) {
+						break out;
+					}
+				}
+
+			}
+
+		},
+
+		/**
+		 * Reverts ... TODO
+		 */
+		revert: function() {
+			// Reversion occurs backwards so as to avoid nodes subsequently
+			// replaced during the matching phase (a forward process):
+			for (var l = this.reverts.length; l--;) {
+				this.reverts[l]();
+			}
+			this.reverts = [];
+		},
+
+		prepareReplacementString: function(string, portion, match, matchIndex) {
+			var portionMode = this.options.portionMode;
+			if (
+				portionMode === PORTION_MODE_FIRST &&
+				portion.indexInMatch > 0
+			) {
+				return '';
+			}
+			string = string.replace(/\$(\d+|&|`|')/g, function($0, t) {
+				var replacement;
+				switch(t) {
+					case '&':
+						replacement = match[0];
+						break;
+					case '`':
+						replacement = match.input.substring(0, match.startIndex);
+						break;
+					case '\'':
+						replacement = match.input.substring(match.endIndex);
+						break;
+					default:
+						replacement = match[+t];
+				}
+				return replacement;
+			});
+
+			if (portionMode === PORTION_MODE_FIRST) {
+				return string;
+			}
+
+			if (portion.isEnd) {
+				return string.substring(portion.indexInMatch);
+			}
+
+			return string.substring(portion.indexInMatch, portion.indexInMatch + portion.text.length);
+		},
+
+		getPortionReplacementNode: function(portion, match, matchIndex) {
+
+			var replacement = this.options.replace || '$&';
+			var wrapper = this.options.wrap;
+
+			if (wrapper && wrapper.nodeType) {
+				// Wrapper has been provided as a stencil-node for us to clone:
+				var clone = doc.createElement('div');
+				clone.innerHTML = wrapper.outerHTML || new XMLSerializer().serializeToString(wrapper);
+				wrapper = clone.firstChild;
+			}
+
+			if (typeof replacement == 'function') {
+				replacement = replacement(portion, match, matchIndex);
+				if (replacement && replacement.nodeType) {
+					return replacement;
+				}
+				return doc.createTextNode(String(replacement));
+			}
+
+			var el = typeof wrapper == 'string' ? doc.createElement(wrapper) : wrapper;
+
+			replacement = doc.createTextNode(
+				this.prepareReplacementString(
+					replacement, portion, match, matchIndex
+				)
+			);
+
+			if (!replacement.data) {
+				return replacement;
+			}
+
+			if (!el) {
+				return replacement;
+			}
+
+			el.appendChild(replacement);
+
+			return el;
+		},
+
+		replaceMatch: function(match, startPortion, innerPortions, endPortion) {
+
+			var matchStartNode = startPortion.node;
+			var matchEndNode = endPortion.node;
+
+			var preceedingTextNode;
+			var followingTextNode;
+
+			if (matchStartNode === matchEndNode) {
+
+				var node = matchStartNode;
+
+				if (startPortion.indexInNode > 0) {
+					// Add `before` text node (before the match)
+					preceedingTextNode = doc.createTextNode(node.data.substring(0, startPortion.indexInNode));
+					node.parentNode.insertBefore(preceedingTextNode, node);
+				}
+
+				// Create the replacement node:
+				var newNode = this.getPortionReplacementNode(
+					endPortion,
+					match
+				);
+
+				node.parentNode.insertBefore(newNode, node);
+
+				if (endPortion.endIndexInNode < node.length) { // ?????
+					// Add `after` text node (after the match)
+					followingTextNode = doc.createTextNode(node.data.substring(endPortion.endIndexInNode));
+					node.parentNode.insertBefore(followingTextNode, node);
+				}
+
+				node.parentNode.removeChild(node);
+
+				this.reverts.push(function() {
+					if (preceedingTextNode === newNode.previousSibling) {
+						preceedingTextNode.parentNode.removeChild(preceedingTextNode);
+					}
+					if (followingTextNode === newNode.nextSibling) {
+						followingTextNode.parentNode.removeChild(followingTextNode);
+					}
+					newNode.parentNode.replaceChild(node, newNode);
+				});
+
+				return newNode;
+
+			} else {
+				// Replace matchStartNode -> [innerMatchNodes...] -> matchEndNode (in that order)
+
+
+				preceedingTextNode = doc.createTextNode(
+					matchStartNode.data.substring(0, startPortion.indexInNode)
+				);
+
+				followingTextNode = doc.createTextNode(
+					matchEndNode.data.substring(endPortion.endIndexInNode)
+				);
+
+				var firstNode = this.getPortionReplacementNode(
+					startPortion,
+					match
+				);
+
+				var innerNodes = [];
+
+				for (var i = 0, l = innerPortions.length; i < l; ++i) {
+					var portion = innerPortions[i];
+					var innerNode = this.getPortionReplacementNode(
+						portion,
+						match
+					);
+					portion.node.parentNode.replaceChild(innerNode, portion.node);
+					this.reverts.push((function(portion, innerNode) {
+						return function() {
+							innerNode.parentNode.replaceChild(portion.node, innerNode);
+						};
+					}(portion, innerNode)));
+					innerNodes.push(innerNode);
+				}
+
+				var lastNode = this.getPortionReplacementNode(
+					endPortion,
+					match
+				);
+
+				matchStartNode.parentNode.insertBefore(preceedingTextNode, matchStartNode);
+				matchStartNode.parentNode.insertBefore(firstNode, matchStartNode);
+				matchStartNode.parentNode.removeChild(matchStartNode);
+
+				matchEndNode.parentNode.insertBefore(lastNode, matchEndNode);
+				matchEndNode.parentNode.insertBefore(followingTextNode, matchEndNode);
+				matchEndNode.parentNode.removeChild(matchEndNode);
+
+				this.reverts.push(function() {
+					preceedingTextNode.parentNode.removeChild(preceedingTextNode);
+					firstNode.parentNode.replaceChild(matchStartNode, firstNode);
+					followingTextNode.parentNode.removeChild(followingTextNode);
+					lastNode.parentNode.replaceChild(matchEndNode, lastNode);
+				});
+
+				return lastNode;
+			}
+		}
+
+	};
+
+	return exposed;
+
+}));
+
diff --git a/pd/nw/css/default.css b/pd/nw/css/default.css
index 3bb087c62..7c704b8b5 100644
--- a/pd/nw/css/default.css
+++ b/pd/nw/css/default.css
@@ -1,8 +1,9 @@
 /* Global CSS */
 
 @font-face {
-    font-family: "DejaVu Sans Mono";
-    src: url("../DejaVuSansMono.ttf");
+    font-family: "Droid Sans Mono Dotted";
+/*    src: url("../DejaVuSansMono.ttf"); */
+    src: url("../DroidSansMonoDotted.ttf");
 }
 
 .noselect {
@@ -48,6 +49,20 @@
     overflow-y: scroll;    
 }
 
+/* marks for matches to console_find */
+mark {
+    background: white;
+}
+
+mark.console_find_current.console_find_highlighted,
+mark.console_find_current {
+    background: yellow;
+}
+
+mark.console_find_highlighted {
+    background: red;
+}
+
 #console_find {
     width: 100%;
     height: 1em;
diff --git a/pd/nw/index.html b/pd/nw/index.html
index 298ab0233..324586bd2 100644
--- a/pd/nw/index.html
+++ b/pd/nw/index.html
@@ -14,7 +14,6 @@
         </label>
       </div>
     </div>
-    <script type="text/javascript" src="index.js"></script>
     <input style="display:none;" id="saveDialog" type="file" nwsaveas />
     <div id = "console_bottom">
       <div id = "printout">
@@ -29,16 +28,17 @@
                       id="console_find_text"
                       name="console_find_text"
                       defaultValue="Search in Console"
-                      style="width:10em;"
-                      onfocus="console_find_input_focus(this)"
-                      onblur="console_find_input_blur(this)"/>XXXXXX   
+                      style="width:10em;"/>
+        </label>
+        <label>Highlight All
+               <input type="checkbox"
+                      id="console_find_highlight"
+                      name="console_find_highlight"
+                      onchange="console_find_highlight_all(this);"/>
         </label>
       </div>
     </div>
-    <script>
-       var t = document.getElementById('console_find_text');
-       t.defaultValue = "Search in Console";
-       console_find_input_blur(t);
-    </script>
+    <script src="./console_search.js"></script>
+    <script type="text/javascript" src="index.js"></script>
   </body>
 </html>
diff --git a/pd/nw/index.js b/pd/nw/index.js
index 9cbd22de1..b16d7d923 100644
--- a/pd/nw/index.js
+++ b/pd/nw/index.js
@@ -36,6 +36,24 @@ document.getElementById("dsp_control").addEventListener("click",
     }
 );
 
+var find_bar = document.getElementById('console_find_text');
+find_bar.addEventListener("keydown",
+    function(e) {
+        return console_find_keydown(this, e);
+    }, false
+);
+
+find_bar.addEventListener("keypress",
+    function(e) {
+        console_find_keypress(this, e);
+    }, false
+);
+
+find_bar.defaultValue = "Search in Console";
+
+console_find_set_default(find_bar);
+
+
 // Invoke some functions to create main menus, etc.
 nw.Window.get().on("close", function() {
     pdgui.menu_quit();
@@ -55,18 +73,138 @@ pdgui.init_socket_events();
 pdgui.set_new_window_fn(nw_create_window);
 pdgui.set_close_window_fn(nw_close_window);
 
-// Greyed out text for the "Find" bar
-function console_find_input_focus(e) {
+function console_find_check_default(e) {
     if (e.value === e.defaultValue) {
-        e.value = '';
-        e.style.color = "#000";
+        return true;
+    } else {
+        return false;
+    }
+}
+
+function console_find_set_default(e) {
+    e.value = e.defaultValue;
+    e.setSelectionRange(0,0);
+    e.style.color = "#888";
+}
+
+function console_unwrap_tag(console_elem, tag_name) {
+    var b = console_elem.getElementsByTagName(tag_name),
+        parent_elem;
+    while (b.length) {
+        parent_elem = b[0].parentNode;
+        while(b[0].firstChild) {
+            parent_elem.insertBefore(b[0].firstChild, b[0]);
+        }
+        parent_elem.removeChild(b[0]); 
+        parent_elem.normalize();
+    }
+}
+
+function console_find_text(elem, evt, callback) {
+    var console_text = document.getElementById('p1'),
+        wrap_tag = 'mark',
+        wrapper_count;
+    // Check the input for default text before the event happens
+    if (console_find_check_default(elem)) {
+       // if so, erase it
+        elem.value = '';
+        // put this in css and use class here
+        elem.style.color = "#000";
     }
+    window.setTimeout(function () {
+        console_unwrap_tag(console_text, wrap_tag);
+
+        // Check after the event if the value is empty, and if
+        // so set it to default value
+        if (elem.value === undefined || elem.value === '') {
+            console_find_set_default(elem);
+        } else if (!console_find_check_default(elem)) {
+            window.findAndReplaceDOMText(console_text, {
+                //preset: 'prose',
+                find: elem.value.toLowerCase(),
+                wrap: wrap_tag
+            });
+            // The searchAndReplace API is so bad you can't even know how
+            // many matches there were without traversing the DOM and
+            // counting the wrappers!
+            wrapper_count = console_text.getElementsByTagName(wrap_tag).length;
+            if (wrapper_count < 1) {
+                elem.style.setProperty('background', 'red');
+            } else {
+                elem.style.setProperty('background', 'white');
+            }
+        }
+        if (callback) {
+            callback();
+        }
+    }, 0);
+}
+
+// start at top and highlight the first result after a search
+function console_find_callback() {
+    console_find_traverse.set_index(0);
+    console_find_traverse.next();
+}
+
+function console_find_keypress(elem, e) {
+    console_find_text(elem, e, console_find_callback);
 }
 
-function console_find_input_blur(e) {
-    if (e.value === '' || e.value === e.defaultValue) {
-        e.value = e.defaultValue;
-        e.style.color = "#888";
+function console_find_highlight_all(elem) {
+    var matches = document.getElementById('p1').getElementsByTagName('mark'),
+        highlight_tag = 'console_find_highlighted',
+        state = elem.checked,
+        i;
+    for (i = 0; i < matches.length; i++) {
+        if (state) {
+            matches[i].classList.add(highlight_tag);
+        } else {
+            matches[i].classList.remove(highlight_tag);
+        }
+    }
+}
+
+var console_find_traverse = (function() {
+    var count = 0,
+        console_text = document.getElementById('p1'),
+        wrap_tag = 'mark';
+    return {
+        next: function() {
+            var i, last, next,
+                elements = console_text.getElementsByTagName(wrap_tag);
+            if (elements.length > 0) {
+                i = count % elements.length;
+                elements[i].classList.add('console_find_current');
+                if (elements.length > 1) {
+                    last = i === 0 ? elements.length - 1 : i - 1;
+                    next = (i + 1) % elements.length;
+                    elements[last].classList.remove('console_find_current');
+                    elements[next].classList.remove('console_find_current');
+                }
+                // adjust the scrollbar to make sure the element is visible,
+                // but only if necessary.
+                // I don't think this is available on all browsers...
+                elements[i].scrollIntoViewIfNeeded();
+                count++;
+            }
+        },
+        set_index: function(c) {
+            count = c;
+        }
+    }
+}());
+
+function console_find_keydown(elem, evt) {
+    if (evt.keyCode === 13) {
+        console_find_traverse.next();
+        evt.stopPropagation();
+        evt.preventDefault();
+        return false;
+    } else if (evt.keyCode === 27) { // escape
+
+    } else if (evt.keyCode === 8 || // backspace or delete
+               evt.keyCode === 46) {
+        console_find_text(elem, evt, console_find_callback);
     }
 }
 
@@ -312,6 +450,7 @@ function nw_create_pd_window_menus () {
         label: l('menu.find'),
         click: function () {
             var find_bar = document.getElementById('console_find'),
+                find_bar_text = document.getElementById('console_find_text'),
                 text_container = document.getElementById('console_bottom'),
                 state = find_bar.style.getPropertyValue('display');
             if (state === 'none') {
@@ -319,6 +458,8 @@ function nw_create_pd_window_menus () {
                 find_bar.style.setProperty('display', 'inline');
                 find_bar.style.setProperty('height', '1em');
                 text_container.scrollTop = text_container.scrollHeight;
+                find_bar_text.focus();
+                find_bar_text.select();
             } else {
                 text_container.style.setProperty('bottom', '0px');
                 find_bar.style.setProperty('display', 'none');
-- 
GitLab