Developer's Blog

【iOS】JavaScript を使って、UIWebView でページ内検索


find_in_page

こんにちは!

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]

Copyright © 2019 Fenrir Inc. All rights reserved.