Files: 5e58ae1fa193ed1aaf2d8f17757a788cac5725e5 / index.js
6435 bytesRaw
1 | /* |
2 | * TextNodeSeacher |
3 | * Copyright (c) 2015 Charles Lehner |
4 | * |
5 | * Usage of the works is permitted provided that this instrument is |
6 | * retained with the works, so that any entity that uses the works is |
7 | * notified of this instrument. |
8 | * |
9 | * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. |
10 | */ |
11 | |
12 | (function (global) { |
13 | |
14 | function addAccents(str) { |
15 | // http://www.the-art-of-web.com/javascript/search-highlight/ |
16 | return str |
17 | // .replace(/([ao])e/ig, "$1") |
18 | .replace(/e/ig, "[eèéêë]") |
19 | .replace(/a/ig, "([aàâä]|ae)") |
20 | .replace(/i/ig, "[iîï]") |
21 | .replace(/o/ig, "([oôö]|oe)") |
22 | .replace(/u/ig, "[uùûü]") |
23 | .replace(/y/ig, "[yÿ]"); |
24 | } |
25 | |
26 | function quoteRegex(str) { |
27 | return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); |
28 | } |
29 | |
30 | function setSelection(startNode, startOffset, endNode, endOffset) { |
31 | var range = document.createRange(); |
32 | range.setStart(startNode, startOffset); |
33 | range.setEnd(endNode, endOffset); |
34 | |
35 | var sel = window.getSelection(); |
36 | sel.removeAllRanges(); |
37 | sel.addRange(range); |
38 | } |
39 | |
40 | function selectText(node, offset, len, align) { |
41 | // Put the text into its own element so we can scroll it into view |
42 | var parent = node.parentNode; |
43 | var el = document.createElement("span"); |
44 | var middle = offset > 0 ? node.splitText(offset) : node; |
45 | var end = middle.splitText(len); |
46 | el.appendChild(middle); |
47 | parent.insertBefore(el, end); |
48 | el.scrollIntoView(align); |
49 | |
50 | // Restore the text and set the selection |
51 | parent.removeChild(el); |
52 | parent.insertBefore(middle, end); |
53 | parent.normalize(); |
54 | setSelection(node, offset, node, offset + len); |
55 | } |
56 | |
57 | function TextNodeSearcher(opt) { |
58 | if (!opt) |
59 | opt = {}; |
60 | else if (opt instanceof window.Element) |
61 | opt = {container: opt}; |
62 | this.container = opt.container || document.body; |
63 | this.highlightTagName = opt.highlightTagName || this.highlightTagName; |
64 | } |
65 | |
66 | TextNodeSearcher.prototype.highlightTagName = "highlight"; |
67 | |
68 | TextNodeSearcher.prototype.setQuery = function (str) { |
69 | if (str == this.queryStr) |
70 | return; |
71 | |
72 | this.queryStr = str; |
73 | this.query = new RegExp(addAccents(quoteRegex(str)), "ig"); |
74 | }; |
75 | |
76 | function shouldDescendInto(node) { |
77 | return node.nodeName != "SCRIPT" && node.nodeName != "STYLE"; |
78 | } |
79 | |
80 | function getNextTextNode(node, container) { |
81 | do { |
82 | if (shouldDescendInto(node) && node.firstChild) { |
83 | node = node.firstChild; |
84 | } else { |
85 | while (!node.nextSibling) { |
86 | node = node.parentNode; |
87 | if (node == container || !node) |
88 | return null; |
89 | } |
90 | node = node.nextSibling; |
91 | } |
92 | } while (node.nodeType != node.TEXT_NODE); |
93 | return node; |
94 | } |
95 | |
96 | function getPreviousTextNode(node, container) { |
97 | if (node == container) { |
98 | while (node.lastChild && shouldDescendInto(node)) |
99 | node = node.lastChild; |
100 | if (node.nodeType == node.TEXT_NODE) |
101 | return node; |
102 | } |
103 | do { |
104 | if (!node || node == container) { |
105 | return null; |
106 | } else if (node.previousSibling) { |
107 | node = node.previousSibling; |
108 | while (shouldDescendInto(node) && node.lastChild) |
109 | node = node.lastChild; |
110 | } else { |
111 | node = node.parentNode; |
112 | } |
113 | } while (node.nodeType != node.TEXT_NODE); |
114 | return node; |
115 | } |
116 | |
117 | function matchLast(re, str) { |
118 | var last; |
119 | re.lastIndex = 0; |
120 | for (var m = re.exec(str); m; m = re.exec(str)) |
121 | last = m; |
122 | return last; |
123 | } |
124 | |
125 | TextNodeSearcher.prototype.highlight = function () { |
126 | if (this.highlightedQuery == this.query) |
127 | return; |
128 | else if (this.highlightedQuery) |
129 | this.unhighlight(); |
130 | var query = this.highlightedQuery = this.query; |
131 | |
132 | query.lastIndex = 0; |
133 | for (var node = getNextTextNode(this.container, this.container); node; |
134 | node = getNextTextNode(node, this.container)) { |
135 | var m = query.exec(node.data); |
136 | if (m) { |
137 | var offset = m.index; |
138 | var len = m[0].length; |
139 | if (len === 0) |
140 | return; |
141 | var hl = document.createElement(this.highlightTagName); |
142 | var middle = offset > 0 ? node.splitText(offset) : node; |
143 | var next; |
144 | if (middle.data.length > len) { |
145 | next = middle.splitText(len); |
146 | } else { |
147 | next = middle.nextSibling; |
148 | } |
149 | var parent = node.parentNode; |
150 | hl.appendChild(middle); |
151 | if (next) |
152 | parent.insertBefore(hl, next); |
153 | else |
154 | parent.appendChild(hl); |
155 | node = middle; |
156 | query.lastIndex = len; |
157 | } |
158 | } |
159 | }; |
160 | |
161 | TextNodeSearcher.prototype.unhighlight = function () { |
162 | this.highlightedQuery = null; |
163 | var els = this.container.getElementsByTagName(this.highlightTagName); |
164 | els = [].slice.call(els); |
165 | for (var i = 0; i < els.length; i++) { |
166 | var el = els[i]; |
167 | var parent = el.parentNode; |
168 | var text = el.firstChild; |
169 | parent.insertBefore(text, el); |
170 | parent.removeChild(el); |
171 | parent.normalize(); |
172 | } |
173 | }; |
174 | |
175 | TextNodeSearcher.prototype.selectNext = function () { |
176 | if (!this.queryStr || !this.container) |
177 | return; |
178 | |
179 | var sel = window.getSelection(); |
180 | var startNode = sel.focusNode; |
181 | var startOffset = 0; |
182 | if (!startNode || !this.container.contains(startNode)) |
183 | startNode = getNextTextNode(this.container, this.container); |
184 | else if (startNode.nodeType != startNode.TEXT_NODE) |
185 | startNode = getNextTextNode(startNode, this.container); |
186 | else |
187 | startOffset = sel.focusOffset; |
188 | |
189 | var wrapped = false; |
190 | for (var node = startNode; node;) { |
191 | var str = node.data; |
192 | this.query.lastIndex = startOffset; |
193 | if (startOffset) |
194 | startOffset = 0; |
195 | var m = this.query.exec(str); |
196 | if (m) { |
197 | selectText(node, m.index, m[0].length, false); |
198 | return; |
199 | } |
200 | node = getNextTextNode(node, this.container); |
201 | if (!node) { |
202 | if (wrapped) |
203 | return; |
204 | wrapped = true; |
205 | node = getNextTextNode(this.container, this.container); |
206 | } |
207 | } |
208 | }; |
209 | |
210 | TextNodeSearcher.prototype.selectPrevious = function () { |
211 | if (!this.queryStr || !this.container) |
212 | return; |
213 | |
214 | var sel = window.getSelection(); |
215 | var endNode = sel.anchorNode; |
216 | var endOffset = 0; |
217 | if (!endNode || !this.container.contains(endNode)) |
218 | endNode = getPreviousTextNode(this.container, this.container); |
219 | else if (endNode.nodeType != endNode.TEXT_NODE) |
220 | endNode = getPreviousTextNode(endNode, this.container); |
221 | else |
222 | endOffset = sel.anchorOffset; |
223 | |
224 | var wrapped = false; |
225 | for (var node = endNode; node;) { |
226 | var str = node.data; |
227 | if (endOffset < Infinity) { |
228 | str = node.data.substr(0, endOffset); |
229 | endOffset = Infinity; |
230 | } |
231 | var m = matchLast(this.query, str); |
232 | if (m) { |
233 | selectText(node, m.index, m[0].length, false); |
234 | return; |
235 | } |
236 | node = getPreviousTextNode(node, this.container); |
237 | if (!node) { |
238 | if (wrapped) |
239 | return; |
240 | wrapped = true; |
241 | node = getPreviousTextNode(this.container, this.container); |
242 | } |
243 | } |
244 | }; |
245 | |
246 | if (typeof module != "undefined") |
247 | module.exports = TextNodeSearcher; |
248 | else if (global) |
249 | global.TextNodeSearcher = TextNodeSearcher; |
250 | }(this)); |
251 |
Built with git-ssb-web