2

I wish to find a substring (from an array) within a string, then replace it with a dropdown box which will have title equal to the substring.

The string is from user input, the substrings having been drawn from a database in my working code.

I have worked from the answer given by DavidTonarini in this question: Javascript: replace() all but only outside html tags

However, this only excludes the text which is contained between '<' and '<'.

If you input: 'a levels a level' into the working fiddle included, then you will see that 'a levels' is returned as a dropdown box, but 'a level' is returned as plain text, nbut it is supposed to be matched with its entry in the array and replaced with a dropdown box. Problems also occur when repeating the same string within the user input. I would like the ability to match the same substring multiple times within a user input.

var data = {
  "a_levels": {
    "a_level": {
      id: 1,
      units: 2,
      created: "2016-10-04 19:00:05",
      updated: "2016-10-05 09:37:46"
    },
    "a_levels": {
      id: 2,
      units: 2,
      created: "2016-10-05 08:19:27",
      updated: "2016-10-05 09:37:39"
    }
  },
  "a_level": {
    "a_level": {
      id: 1,
      units: 2,
      created: "2016-10-04 19:00:05",
      updated: "2016-10-05 09:37:46"
    },
    "a_levels": {
      id: 2,
      units: 2,
      created: "2016-10-05 08:19:27",
      updated: "2016-10-05 09:37:39"
    }
  }
};
var input, // Create empty variables.
  response;

$('#submit').click(function() {
  input = $('#userInput').val();
  response = input;
  // CREATE DROPDOWN BOXES.
  var strings_used = [];
  $.each(data, function(i, v) { // Iterate over first level of output.

    for (var itr = 0; itr < strings_used.length; ++itr) {
      if (strings_used[itr].indexOf(i) !== -1) {
        return true;
      }
    }
    var searchWord = i.replace(/_/g, " "); // Replace underscores in matches with blank spaces.
    var regEx = new RegExp("(" + searchWord + ")(?!([^<]+)?>)", "gi"); // Create regular expression which searches only for matches found outside html tags.
    var tmp = response.replace(regEx, "<span class='btn-group'><button class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown'>" + searchWord + "<span class='caret'></span></button><ul class='" + i + " dropdown-menu'></ul></span>"); // Replace matching substrings with dropdown boxes.
    if (tmp !== response) { // Check if replacement is complete.
      response = tmp; // Update response.
      strings_used.push(i);
    }
  });
  $('#template').empty().append(response); // Populate template container with completed question response including dropdown boxes.
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<body>

  <div id="searchbox">
    <div class="input-group">
      <input id="userInput" type="text" class="form-control" placeholder="type here...">
      <span id="submit" class="input-group-btn">
     <button class="btn btn-default" type="submit">GO!</button>
   </span>
    </div>
  </div>

  <div class="row">
    <div id="template" class="col-sm-10 col-md-offset-1 text-left"></div>
  </div>

</body>
Community
  • 1
  • 1
Muckee
  • 474
  • 1
  • 8
  • 26

1 Answers1

1

Your bug originated in the regex, and the way in which you are using it:

var regEx = new RegExp("(" + searchWord + ")(?!([^<]+)?>)", "gi")

The issue, as you seem to have already found for yourself, was that after you replaced "a levels" with "...toggle='dropdown'>a levels<span class='caret'>...", this pattern still matched future iterations of the loop (namely, for "a level") - which screwed up the resulting HTML.

You tried to fix this by adding a patch:

for (var itr = 0; itr < strings_used.length; ++itr) {
  if (strings_used[itr].indexOf(i) !== -1) {
    return true;
  }
}

However, this also does not work - and only served to obscure the original error. Now, you are exiting as soon as any pattern matches - which is why "a level" does not even get searched for, if "a levels" matches.

Without totally changing how your method works, here is a quick patch - I've simply removed your strings_used logic and replaced the regular expression with:

var regEx = new RegExp("(\\b" + searchWord + "\\b)(?!<)", "gi");

var data = {
  "a_levels": {
    "a_level": {
      id: 1,
      units: 2,
      created: "2016-10-04 19:00:05",
      updated: "2016-10-05 09:37:46"
    },
    "a_levels": {
      id: 2,
      units: 2,
      created: "2016-10-05 08:19:27",
      updated: "2016-10-05 09:37:39"
    }
  },
  "a_level": {
    "a_level": {
      id: 1,
      units: 2,
      created: "2016-10-04 19:00:05",
      updated: "2016-10-05 09:37:46"
    },
    "a_levels": {
      id: 2,
      units: 2,
      created: "2016-10-05 08:19:27",
      updated: "2016-10-05 09:37:39"
    }
  }
};
var input, // Create empty variables.
  response;

$('#submit').click(function() {
  input = $('#userInput').val();
  response = input;
  // CREATE DROPDOWN BOXES.
  $.each(data, function(i, v) { // Iterate over first level of output.

  // ** REMOVED: **
  //  for (var itr = 0; itr < strings_used.length; ++itr) {
  //    if (strings_used[itr].indexOf(i) !== -1) {
  //      return true;
  //    }
  //  }

    var searchWord = i.replace(/_/g, " "); // Replace underscores in matches with blank spaces.

    // ** CHANGED: **
    var regEx = new RegExp("(\\b" + searchWord + "\\b)(?!<)", "gi");

    var tmp = response.replace(regEx, "<span class='btn-group'><button class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown'>" + searchWord + "<span class='caret'></span></button><ul class='" + i + " dropdown-menu'></ul></span>"); // Replace matching substrings with dropdown boxes.
    if (tmp !== response) { // Check if replacement is complete.
      response = tmp; // Update response.
    }
  });
  $('#template').empty().append(response); // Populate template container with completed question response including dropdown boxes.
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<body>

  <div id="searchbox">
    <div class="input-group">
      <input id="userInput" type="text" class="form-control" placeholder="type here...">
      <span id="submit" class="input-group-btn">
        <button class="btn btn-default" type="submit">GO!</button>
      </span>
    </div>
  </div>

  <div class="row">
    <div id="template" class="col-sm-10 col-md-offset-1 text-left"></div>
  </div>

</body>

However, a much cleaner solution would be to perform the find-and-replace in a one go - thereby avoiding this need to "search for strings that are not inside an HTML element" in the first place. As the comment on the post whose solution you copied quite rightly points out, it is a bad idea to parse HTML with regex -- due to precisely this sort of situation!

I'll leave this as an exercise for you to have a go at, but basically I'd recommend that your code simply searches for:

var regEx = new RegExp("\\b(a level|a levels)\\b", "gi");

And replaces with:

... data-toggle='dropdown'>$1<span class='caret'> ...

Edit: As discussed below, here is a possible sketch of a much shorter, simpler and bug-free implementation:

$('#submit').click( function() {
  var input = $('#userInput').val();
  var regEx = new RegExp("\\b(" +  Object.keys(data).join('|').replace(/_/g, " ") + ")\\b", "gi");
  $('#template').html(
    input.replace(
      regEx,
      "<span class='btn-group'><button class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown'>$1<span class='caret'></span></button><ul class='" + "$1".replace(/ /g, "_") + "' dropdown-menu'></ul></span>"
    )
  );
});
Community
  • 1
  • 1
Tom Lord
  • 27,404
  • 4
  • 50
  • 77
  • Thanks for this. I have looked and believe this is correct, though I have had limited internet access to test. Will have a go now and update shortly. I guess I just need to practice more regular expressions. – Muckee Oct 20 '16 at 09:19
  • Woah woah woah, hang on... Rather than just taking my patched version of your code and trying to tweak it for more complicated requirements, I strongly suggest you follow through on my recommendation to refactor the code. Here is a quick sketch to get you on the right track: https://jsfiddle.net/huwwefa0/15/ – Tom Lord Oct 20 '16 at 10:59
  • Here's an even closer version to what you need.... It's much easier to make changes to cleanly written code :) https://jsfiddle.net/huwwefa0/16/ – Tom Lord Oct 20 '16 at 11:13
  • Thanks mate, though I'm not sure that this is the right direction. The strings must be replaced in order of length, descending. Here is an attempt that is currently broken. Not sure exactly what I'm doing wrong but I think I just need different logic for the solution : https://jsfiddle.net/huwwefa0/19/ . I have added an extra entry to the 'data' array for improved clarity. – Muckee Oct 21 '16 at 11:27
  • Taking my solution above, I implemented your additional requirement in half a line of code ;) https://jsfiddle.net/huwwefa0/20/ ... I hope I'm getting some recognition in your A Level sources used haha – Tom Lord Oct 21 '16 at 11:49
  • Ahaha if anything is ever published I'll be sure to give you a mention ;). Although `"$1".replace(/ /g, "_")` does not work and instead leaves the class as separate words with spaces, rather than one string connected by underscores. I have also tested by using /\s/ instead of / /, but it still doesn't work. Do you have any idea why? There was a minor error in your update... there was a comma before dropdown-menu which was not supposed to be there, but the issue is still present regardless of this. – Muckee Oct 21 '16 at 12:13
  • In case it seems like it, I'm not just asking you for every little fix... but I have been trying to work around that since using your suggestions and cannot work out how to fix it. – Muckee Oct 21 '16 at 12:18
  • Dude, come on... You need to make more of an effort to figure this stuff out, it's **your** exams that are being studied for; I've already passed mine! 5 seconds on google found me this: http://stackoverflow.com/questions/2447915/javascript-string-regex-backreferences, and here is an implementation: https://jsfiddle.net/huwwefa0/22/ – Tom Lord Oct 21 '16 at 13:06
  • I think it is just that I am missing some basic piece of programming logic in my understanding. I had this all running smoothly with much more complex data, then rewrote it with a simpler database (actually rewrote it, didn't copy and paste), but now it isn't running properly. – Muckee Oct 21 '16 at 13:08
  • I will start again today from the ground up, using your advice, and see what I come up with. – Muckee Oct 21 '16 at 13:09
  • I apologise that it seems I am simply asking you to solve the problems for me. I am self-taught and occasionally come across subjects which I have absolutely no experience in and get quite muddled to begin with. I thought it would be to do with assigning a new variable, I'm just not confident if I don't know best practices. Anyway, I think that last one is what I need. I have made a revised version here : https://jsfiddle.net/huwwefa0/23/ This version will proceed to populate the dropdown boxes using the keys within each respective object value. – Muckee Oct 22 '16 at 09:56