Problem
The idea of my application is to have the user provides text which is pasted (or written) inside the textbook and further provides a search word.
The application then shows the overall count of words in the text, how many times the search word appears in the text, and the rate overall words to count of search word.
Any suggestions and hints concerning JavaScript-programming and layout are welcomed.
// Adding an object to global namespace.
var wr = {};
// -- Overall variable-member --------------------
wr.textBox = document.querySelector('#text');
wr.needleBox = document.querySelector('#needle');
wr.haystack = document.querySelector('#haystack');
wr.exec = document.querySelector('#exec');
wr.reset = document.querySelector('#reset');
wr.caseSensitive = document.querySelector('#case');
wr.countWords = document.querySelector('#word-count');
wr.countNeedle = document.querySelector('#count-needle');
wr.percentage = document.querySelector('#percentage-rate');
wr.inputs = document.querySelectorAll('input');
// -- Function-member ----------------------------
wr.calculate = function() {
var flags = '';
var reg;
var ret = {};
var textBoxHtml = this.textBox.value.trim();
var needle = this.needleBox.value.trim();
ret.countNeedle = 0;
ret.percentageRate = 0.0;
ret.countWords = textBoxHtml.split(/s/).length;
ret.replacement = '';
this.caseSensitive.checked ? flags = 'g' :
flags = 'ig';
reg = new RegExp( '(\s|^)' + needle +
'(?=\W)', flags );
ret.replacement = textBoxHtml.replace(reg, function(match) {
ret.countNeedle++;
return '<span class="high-lighted">' + match + '</span>';
});
ret.percentageRate =
((ret.countNeedle * 100) / ret.countWords).toFixed(1);
ret.needle = needle;
return ret;
}
wr.displayResult = function() {
var display = document.createElement('div');
var result = this.calculate();
var resultsRow = document.getElementById('results');
this.haystack.removeChild(this.textBox);
this.haystack.appendChild(display);
display.classList.add('display');
display.innerHTML = result.replacement;
this.countWords.value = result.countWords;
this.countNeedle.value = result.countNeedle;
this.percentage.value = result.percentageRate + ' %';
this.needleBox.value = result.needle || 'No value provided.';
this.exec.classList.add('hidden-element');
this.reset.classList.remove('hidden-element');
results.classList.remove('hidden-element');
for (var i in this.inputs) {
if (typeof this.inputs[i] === 'object')
this.inputs[i].classList.add('diabled-input');
}
}
// -- Event-Handler --------------------------
wr.exec.addEventListener('click', function() {
try {
if (!wr.textBox.value.trim().length)
throw new Error('Please provide a text.');
if (!wr.needleBox.value.trim().length)
throw new Error('Please provide a search value.');
wr.displayResult();
} catch (e) {
alert(e.message);
}
});
wr.reset.addEventListener('click', function() {
location.reload(true);
})
html {
color: #222;
background-color: whitesmoke;
font-size: 1em;
line-height: 1.4;
overflow: scroll; }
.container {
padding: 25px 0; }
input[type=checkbox] {
margin-left: 20px; }
.high-lighted {
font-weight: bold;
color: crimson; }
textarea, .display {
padding: 5px; }
textarea {
overflow: auto;
width: 100%;
min-height: 120px; }
.input-group-textarea {
width: 100%; }
input:hover, textarea:hover {
border-color: #265a88; }
#case {
border-color: pink; }
.display {
border: 1px solid black;
width: 100%;
min-height: 120px; }
.input-group {
margin: 20px 0; }
.hidden-element {
display: none; }
.diabled-input {
pointer-events: none; }
.btn-default {
border-radius: 8px; }
.label-textbox {
padding-bottom: 5px; }
#needle {
width: 100%; }
button:hover {
opacity: 0.6; }
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WordRate</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-lg-12">
<p><strong>Copy and paste your text into <i>Word to search</i>. Write the word to search into <i>Text to search through<i>. Afterward click <i>Execute</i>.</p></strong>
<p> <span class="glyphicon glyphicon-arrow-right" aria-hidden="true"></span>
Count of words in text and occurences of search word are shown.
<span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></p>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="input-group">
<span class="input-group-addon">Word to search</span>
<input type="text" id="needle" value="Lorem" />
</div>
</div>
<div class="col-lg-3">
<div class="input-group">
<span class="input-group-addon">Case-sensitive</span>
<input type="checkbox" id="case" checked />
</div>
</div>
</div>
<div class="row hidden-element" id="results">
<div class="col-lg-4">
<div class="input-group">
<span class="input-group-addon">Complete word-count</span>
<input type="text" id="word-count" placeholder=" - " />
</div>
</div>
<div class="col-lg-4">
<div class="input-group">
<span class="input-group-addon">Needle occurrences</span>
<input type="text" id="count-needle" placeholder=" - " />
</div>
</div>
<div class="col-lg-4">
<div class="input-group">
<span class="input-group-addon">Needle to word-count</span>
<input type="text" id="percentage-rate" placeholder=" - " />
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="btn-group btn-group-sm">
<button id="exec" class="btn btn-primary">Execute</button>
<button id="reset" class="btn btn-warning hidden-element">Reset</button>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="input-group input-group-textarea">
<div class="row">
<div class="col-xs-12">
<label class="label-textbox">Text to search through</label>
</div>
</div>
<div class="row">
<div class="col-xs-12" id="haystack">
<textarea id="text">Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem lorem ipsum dolor sit amet.
</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/main.js"></script>
</body>
</html>
For some reason it doesn’t work properly here, therefore there’s live-demo here.
Solution
First, you swapped out your textbox for a div. Not really friendly, and your reset requires a page reload. Consider preserving the textarea and make the reset just clear off the result, but not the text.
UX aside, the application works great, except it can be better:
wr.textBox = document.querySelector('#text');
wr.needleBox = document.querySelector('#needle');
wr.haystack = document.querySelector('#haystack');
wr.exec = document.querySelector('#exec');
wr.reset = document.querySelector('#reset');
wr.caseSensitive = document.querySelector('#case');
wr.countWords = document.querySelector('#word-count');
wr.countNeedle = document.querySelector('#count-needle');
wr.percentage = document.querySelector('#percentage-rate');
wr.inputs = document.querySelectorAll('input');
IDs are unique. If some element already uses the ID, then you’ll be going back to this script to modify the selector. Consider using classes instead.
Also, your selectors are very generic. Consider “namespacing” them to something specific to your app. Read about the BEM naming scheme. The naming scheme is primarily geared towards CSS naming, but it also does wonders in terms of selector matching in JS.
wr.textBox = document.querySelector('.wr__text');
wr.needleBox = document.querySelector('.wr__needle');
wr.haystack = document.querySelector('.wr__haystack');
wr.exec = document.querySelector('.wr__exec');
wr.reset = document.querySelector('.wr__reset');
wr.caseSensitive = document.querySelector('.wr__case');
wr.countWords = document.querySelector('.wr__word-count');
wr.countNeedle = document.querySelector('.wr__count-needle');
wr.percentage = document.querySelector('.wr__percentage-rate');
wr.inputs = document.querySelectorAll('.wr__input');
Your code looks pretty long, and that’s because you used vanilla JavaScript. While there’s nothing wrong with it, a framework could cut down its length. Consider using one, any one for that matter. I suggest getting one that does data binding, as DOM manipulation is a very tedious task to do over and over again.
reg = new RegExp( '\b(' + needle + ')\b', flags );
Matching words can be done by using \b
. It means word boundary. It will only match needle
when it is considered a word (bounded by word boundary characters).
ret.replacement = textBoxHtml.replace(reg, function(match) {
ret.countNeedle++;
return '<span class="high-lighted">' + match + '</span>';
});
I’ve seen this a lot before, where a counting operation is done at the same time as the matching operation. While this improves performance by doing stuff while you can, it is also a maintenance nightmare since your code is overlapping in functionality. Consider separating word count from the replacer. string.match
should be able to give you an array of matches. You can use that array’s length to count the match.
Here’s a demo using a framework. Note how the code only defines the data, computes derived data, defines a template and a few other things. Everything else like DOM manipulation, event management etc. are done by the framework:
const WordCounter = Ractive.extend({
pattern: null,
template: `
<div>
<div>
<label>Text:</label>
<textarea value="{{ text }}"></textarea>
</div>
<div>
<label>Needle:</label>
<input type="text" value="{{ query }}" />
</div>
<div>
<label>Case Insensitive:</label>
<input type="checkbox" checked ="{{ caseInsensitive }}" />
</div>
<div>
<button type="button" on-click="reset()">Reset</button>
</div>
</div>
<dl>
<dt>Stats:</dt>
<dd>Word Count: {{ wordCount }}</dd>
<dd>Needle Ocurrences: {{ needleOccurrences }}</dd>
<dd>Needle Rate: {{ needleRate }}</dd>
</dl>
<div>{{{ highlightedResults }}}</div>
`,
css: `
.highlighted{
color: red;
}
`,
// Initial data
data: {
text: 'text Text test rest',
query: 'text',
caseInsensitive: true,
},
// Derrived data (data computed from source of record)
computed: {
wordCount(){
var text = this.get('text');
return text.length ? text.split(/s/).length : 0;
},
needleOccurrences(){
var pattern = this.get('pattern');
var text = this.get('text');
var query = this.get('query');
var matches = text.match(pattern);
return !query || !matches ? 0 : matches.length
},
needleRate(){
var wordCount = this.get('wordCount');
var needleOccurrences = this.get('needleOccurrences');
return wordCount ? needleOccurrences / wordCount : 0;
},
highlightedResults(){
var pattern = this.get('pattern');
var text = this.get('text');
return text.replace(pattern, function(match){
return '<span class="highlighted">' + match + '</span>';
});
},
pattern(){
var query = this.get('query');
var flags = this.get('caseInsensitive') ? 'gi' : 'g';
return new RegExp( `\b(${query})\b`, flags );
}
},
reset(){
this.set('text', '');
this.set('query', '');
this.set('caseInsensitive', true);
}
});
new WordCounter({ el: 'body' });