Skip to content
Snippets Groups Projects
Commit 053f6f20 authored by Jonathan Wilkes's avatar Jonathan Wilkes
Browse files

first draft of a functioning "Find" bar for the Pd console

parent d4fbe0a8
No related branches found
No related tags found
No related merge requests found
/**
* 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;
}));
/* 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;
......
......@@ -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>
......@@ -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');
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment