こんにちは!
Sleipnir Mobile for iPhone / iPad 開発担当の宮本です。
ちょっと前ですが、UIWebView についての記事を書きました。
【iOS】UIWebView Hacks 〜ブラウザ開発テクニック
ページ内検索についての内容が抜けていたので、今回はその話です。
UIWebView にはページ内検索関連の API はありません。Objective-C で DOM をたどることもできないので、ほとんどを JavaScript を使って実装します。
実装するポイント
ソースコードは一番最後にあります。まあまあ長いコードなので、ポイントとなる部分だけ紹介します。
全てのエレメントをたどる
body から childNodes をたどっていっても、iframe 内の document にはアクセスできません。window.frames で frame 一覧が取得できるので、全ての frame に対して、再帰的に処理します。
span で囲んで、class をいじってハイライト
マッチしたテキストノードを span で囲んで、背景色をつけるために class を指定します。ロード完了時に予めスタイルは埋め込んでおきます。
見えているかどうかをチェックする
HTML 上では該当のテキストがあるのに、見えないパターンがあります。hidden になっていたり、マイナス座標にいたり、サイズが 0 などの場合ですね。
使い方
// マッチしたテキストがグレーにハイライトして、最初のテキストだけ黄色くハイライトします。 // マッチした件数が返ります。 __Browser.findInPage.highlightWords("keyword"); // 次に進めます __Browser.findInPage.goNext(); // 前に戻れます __Browser.findInPage.goNext(); // ハイライトを解除します __Browser.findInPage.clearHighlights();
さいごに
UIWebView でページ内検索を実現する方法を紹介しました。今回紹介しているコードは、ほとんどの場合で動きますが、まだ対応するべき点があります。
例えば、ウェブページが非常に長くて、マッチする件数が多すぎる場合です。もし数千件レベルでマッチすると、あまりにも時間がかかってフリーズしてしまいます。時間を制限して、途中で検索をやめるようなコードがあっても良さそうです。必要があれば、付け足して下さい。
また、マッチしたポイントへのスクロールを JavaScript で実現しています。ここは goNext や goPrevious で座標を Objective-C 側に返して、UIWebView の scrollView を使うとアニメーションさせられます。
ソースコード
__Browser = {};
__Browser.findInPage = (function() {
var IGNORE_NODE_NAMES = {
EMBED: 1,
OBJECT: 1,
SCRIPT: 1,
STYLE: 1,
};
var CSS_CLASS_NAME = “FIND_IN_PAGE”;
var CSS_SELECT_CLASS_NAME = “FIND_IN_PAGE_SELECT”;
var index = -1;
var spans = [];
var frameDocuments = function() {
var docs = [];
var wins = [window];
while (wins.length > 0) {
var win = wins.pop();
for (var i = 0; i < win.frames.length; i++) {
try { // cross-origin な問題で DOMException がでるので
win.frames[i].document && docs.push(win.frames[i].document);
wins.push(win.frames[i]);
} catch (e) {}
}
}
return docs;
};
var select = function(element) {
element.className = (element.className || "") + " " + CSS_SELECT_CLASS_NAME;
};
var unselect = function(element) {
var pat = " " + CSS_SELECT_CLASS_NAME;
element.className = (element.className || "").replace(pat, "");
};
var getPosition = function(element) {
var rect = element.getBoundingClientRect();
return [Math.round(rect.left + window.pageXOffset),
Math.round(rect.top + window.pageYOffset)];
};
var getAllElementsByClassName = function(className) {
var elements = document.getElementsByClassName(className);
for (var i = 0, docs = frameDocuments(); i < docs.length; i++) {
var doc = docs[i];
elements.concat(doc.getElementsByClassName(className).toArray());
}
return [].slice.call(elements);
};
var addStyle = function() {
var docs = [document].concat(frameDocuments());
var sel1 = "." + CSS_CLASS_NAME;
var sel2 = "." + CSS_SELECT_CLASS_NAME;
var style1 = "{background-color:#D2D2D2 !important; padding:0px;margin:0px;overflow:visible !important;}";
var style2 = "{background-color:#F2D80A !important; padding:0px;margin:0px;overflow:visible !important;}";
for (var i = 0; i < docs.length; i++) {
var doc = docs[i];
var style = doc.createElement("style");
style.type = "text/css";
doc.body.appendChild(style);
style.appendChild(doc.createTextNode(sel1 + style1 + sel2 + style2));
}
};
var isVisible = function(element) {
var style = element.ownerDocument.defaultView.getComputedStyle(element, null);
if (element.style.display == "none" ||
element.style.visibility == "hidden" ||
style.display == "none" ||
style.visibility == "hidden") return false;
var rect = element.getBoundingClientRect();
if (rect.width == 0 ||
rect.height == 0 ||
rect.left < 0) return false;
return true;
};
var global = {
highlightWords: function(keyword) {
if (spans.length) this.clearHighlights();
var docs = [document].concat(frameDocuments());
var regex = RegExp(keyword, "ig");
for (var i = 0; i < docs.length; i++) {
var doc = docs[i];
(function replaceTextWithSpan(node, regex) {
if (node.nodeType == 3) {
var result = regex.exec(node.data);
if (result && result.index >= 0) {
var middlebit = node.splitText(result.index);
middlebit.splitText(keyword.length);
var middleClone = middlebit.cloneNode(true);
var span = doc.createElement(“span”);
span.className = CSS_CLASS_NAME;
span.appendChild(middleClone);
middlebit.parentNode.replaceChild(span, middlebit);
}
} else if (node.nodeType == 1 && node.childNodes && !IGNORE_NODE_NAMES[node.tagName]) {
var c = node.firstChild;
while (c) {
replaceTextWithSpan(c, regex);
var next = c.nextSibling;
c = next;
}
} else {
}
})(docs[i].body, regex);
}
spans = getAllElementsByClassName(CSS_CLASS_NAME).filter(isVisible);
if (spans.length) {
this.goNext();
}
return spans.length;
},
clearHighlights: function() {
for (var i = 0; i < spans.length; i++) {
if (!spans[i].parentNode) continue;
else {
var parent = spans[i].parentNode;
parent.replaceChild(spans[i].firstChild, spans[i]);
parent.normalize();
}
}
index = -1;
spans = [];
},
goNext: function() {
var newIndex = (index + 1 >= spans.length) ? 0 : index + 1;
if (index >= 0) unselect(spans[index]);
select(spans[newIndex]);
index = newIndex;
var pos = getPosition(spans[newIndex]);
console.log(pos);
window.scrollTo(pos[0] – 100, pos[1] – 100);
},
goPrev: function() {
var newIndex = (index – 1 < 0) ? spans.length - 1 : index - 1;
unselect(spans[index]);
select(spans[newIndex]);
index = newIndex;
var pos = getPosition(spans[newIndex]);
window.scrollTo(pos[0] - 100, pos[1] - 100);
},
};
addStyle();
return global;
})();
[/javascript]