<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<div>Some text here without any enclosing p element.
<p>Some <b> more text </b></p>
</div>
Solution 2 :
First off as I commented above, your API does not take into account the html when producing the offsets. In your example you technically have 5 white space characters before “Some” (new line and indentation) so the actual offsets should be [[10, 14], [73, 77]]. We can visualise this by encoding the contents of the <div> element and placing each character on a new line…
01. %0A
02. 20%
03. 20%
04. 20%
05. 20%
06. S
07. o
08. m
09. e
10. 20%
11. t
12. e
13. x
14. t
15. 20%
16. h
17. e
18. r
19. e
20. 20%
21. w
22. i
23. t
24. h
25. o
26. u
27. t
28. 20%
29. a
30. n
31. y
32. 20%
33. e
34. n
35. c
36. l
37. o
38. s
39. i
40. n
41. g
42. 20%
43. p
44. 20%
45. e
46. l
47. e
48. m
49. e
50. n
51. t
52. .
53. %0A
54. 20%
55. 20%
56. 20%
57. 20%
58. %3C
59. p
60. %3E
61. S
62. o
63. m
64. e
65. 20%
66. %3C
67. b
68. %3E
69. m
70. o
71. r
72. e
73. 20%
74. t
75. e
76. x
77. t
78. %3C
79. %2F
80. b
81. %3E
82. %3C
83. %2F
84. p
85. %3E
86. %0A
Now assuming your API is returning offsets including HTML characters please see my my code implmentation below.
/**
* -----
* Core wrapping functions
* -----
*/
(function($) {
'use strict';
/**
* @param {string|jquery} wrap DOM element used for wraping.
* @param {integer} start The starting offset for the opening dom tag.
* @param {integer} end The end offset for the closing dom tag.
* @return {object}
*/
$.fn.wrapAt = function(wrap, start, end) {
const origHtml = this.html();
const origLength = origHtml.length;
const firstPart = origHtml.substr(0, start);
const middlePart = $(wrap).html(origHtml.substr(start, end - start)).prop('outerHTML');
const endPart = origHtml.substr(end);
const newHtml = `${firstPart}${middlePart}${endPart}`;
const newLength = newHtml.length;
this.html(newHtml);
return {
original: origLength,
new: newLength,
difference: newLength - origLength
};
};
/**
* @param {string|jquery} wrap DOM element used for wraping.
* @param {array[]} offsets An array of start and end offsets.
*/
$.fn.bulkWrapAt = function(wrap, offsets = []) {
let diff = 0;
for (let i = 0; i < offsets.length; i++) {
let start = offsets[i][0];
let end = offsets[i][1];
// Modify offsets based on mutated string length difference
start += diff;
end += diff;
const result = this.wrapAt(wrap, start, end);
diff += result.difference;
}
};
})(jQuery);
/**
* -----
* Wraping initiator
* -----
*/
(function($) {
const apiResponse = [
[10, 14], //[5, 9],
[73, 77] //[59, 63]
];
$('div').bulkWrapAt('<span>', apiResponse);
})(jQuery);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
Some text here without any enclosing p element.
<p>Some <b>more text</b></p>
</div>
The “Core wrapping functions” gives you two methods to play with. The first being a single replacement, for example…
$('div').wrapAt('<span>', 10, 14);
The second method allows you to bulk wrap based on a multidimensional array of start and end offsets (like your API response example).
This isn’t a complete answer but I’m hoping it will be enough to steer you in the right direction.
Problem :
A browser extension I’m building needs to insert span elements around certain text bits (usually single words).
As an example, for some input HTML like <p>This is some text</p>, it might generate <p>This is some <span class="insertedByMe">text</span></p>.
A REST API tells the browser extension where to put those span elements via character offsets. In the example above, the word “text” spans the character offsets [13, 17], so the server would tell the extension that it should put the span around those offsets. NB: those character offsets currently only relate to the actual text, excluding any markup, although that could be changed on the server side.
My question: How do I insert HTML tags at given character offsets? Is this possible at all, preferably without destroying any listeners on the elements I’m replacing? Note also that I can’t work on text nodes alone, because I always need the context of at least the containing sentence of a word, and I can’t assume that a single text node always encloses a whole sentence (because of links, markup etc.).
Bonus: The example above is quite a simple case. Things will probably get trickier when I’m working on more nested HTML elements, or when it becomes ambiguous whether to put a span before or after other HTML elements that already start or end at some offset (because the offsets do not count markup). See example below. Any suggestions that help me handle those more complex cases are very welcome!
<div>
Some text here without any enclosing p element.
<p>Some <b>more text</b></p>
</div>
Server response: put spans around offsets [[5, 9], [59, 63]] (the two occurrences of the word "text")
Challenges:
The first span is to be put into the text node that’s a direct child of the div, so the parent element for that text node is also an ancestor to the text that contains the second span. I image this might be problematic when I replace the entire inner HTML of the div, because it reloads all children, possibly destroying listeners.
The closing tag for the second span must be inserted before the closing b tag to safeguard valid HTML, while both closing tags are at character offset 63.
Comments
Comment posted by Burham B. Soliman
all you need to get offset of an element to inject the code with another element? is that what exactly u need ?
Comment posted by Joko
Yeah that goes a long way, and sort of is my current implementation. But if possible, I’d like to avoid having to re-load the parent element so as not to destroy any listeners on it. Not sure if that’s possible though.
Comment posted by here
as long as jquery let’s you inject your code with Offset() i think it can be done
Comment posted by CBroe
Setting the innerHTML property of an element, should not remove any event handlers that already exist.
Comment posted by CBroe
βAny suggestions that help me handle those more complex cases are very welcome!β
Comment posted by Burham B. Soliman
@Joko i hope that meets your requirements,
Comment posted by Joko
Thanks, that goes a long way. π It’s still not optimal, because the nested
Comment posted by Burham B. Soliman
at least you got a hint now ^^ goodluck bud
Comment posted by Joko
Oh and of course I’d only do this on the root element, not all