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>

1 Answer 1

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>"
    )
  );
});
Sign up to request clarification or add additional context in comments.

11 Comments

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.
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: jsfiddle.net/huwwefa0/15
Here's an even closer version to what you need.... It's much easier to make changes to cleanly written code :) jsfiddle.net/huwwefa0/16
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 : jsfiddle.net/huwwefa0/19 . I have added an extra entry to the 'data' array for improved clarity.
Taking my solution above, I implemented your additional requirement in half a line of code ;) jsfiddle.net/huwwefa0/20 ... I hope I'm getting some recognition in your A Level sources used haha
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.