FireFoxでdisplay:none要素内のiframeではgetSelectionできない。

掲載日
更新日

はじめに

仕事でCKEditor5というWYSIWYGエディターを使用しているのですが、以下のようなことがありました。

  1. CKEditor5は、CKEditor5を呼び出すページのスタイルをエディタ内にも反映してくれます。
    非常に便利なのですが、CKEditor4まではiframe要素内にエディタがあり、スタイルもiframe内のheadにcssを追加するという方法で反映していました。
    そういうわけで、4まではエディタを呼び出すページのスタイルとCKEditor内で反映させたいスタイルを別々に分けていたのですが、5からはそれができなくなりました。
  2. なので、CKEditor5を呼び出すHTMLページを作成しておき(スタイルもそっちで読み込む)、iframeで読み込むという形式で対応しました。
    (本当はスタイル側を対応すべきなのですが、仕事ではスタイル組む人とソフト作成者(自分)が分かれてしまっており、調整が難しかった。)
  3. モーダルの中にCKEditorが入っているため、ページ読み込み時はdisplay:noneになっている場所にiframeがありました。
    (そしてCKEditor5のcreateもページ読み込み時に行っていました。)

 

この時、Chromeだと問題ないのですが、FireFoxでは以下のようなエラーが出ました。

CKEditorError: can't access property "rangeCount", t is null
Read more: https://ckeditor.com/docs/ckeditor5/latest/support/error-codes.html#error-can't access property "rangeCount", t is null

_removeDomSelection editor.bundle.js:formatted:8262

色々試したところ、以下の状況であることが分かりました。

  • display:noneを削除すればエラーは消える。
  • iframeではなく直接呼出しならdisplay:noneでも問題ない。
  • ckeditor5の中にある_removeDomSelectionという関数のgetSelection()でエラー
_removeDomSelection() {
    for (const e of this.domDocuments) {
        console.log(e.getSelection())
        // -> FFではここがnull、chromeではnullにならない(Selectionオブジェクトになる)
        const t = e.getSelection();
        if (t.rangeCount) { // ここでエラー
            const n = e.activeElement,
                i = this.domConverter.mapDomToView(n);
            n && i && t.removeAllRanges()
        }
    }
}

調査

表示されていない <iframe> に対して呼び出された場合(例えば display: none が設定されている場合)、 Firefox は null を、他のブラウザーは None を設定した Selection.type オブジェクトを返します。

 

Window.getSelection()より引用

  • 13年前くらいからある問題みたいです。
    比較的新しいコメントもあるので今後修正される可能性はありそうですが、そうはいっても「直るまで待ってください」は通らない。
    また、直ったところで最新版に更新できない環境のユーザーもいるので、なんとかして対応できないか探ります。
  • 以下のファイル二つでお試しできます。(同じ階層に配置する必要があります。)
<!DOCTYPE html>
<html>
    <body>
        <p>
            ここはiframe外
        </p>
        <iframe style="display: none;" src="./iframe.html"></iframe>
    </body>
</html>

以下はiframe.htmlとして保存し、ブラウザで開く際は上記のファイルの方を開くと再現できます。

<!DOCTYPE html>
<html>
    <body>
        <p>
            ここはiframe
        </p>
    </body>
    <script>
        console.log(document.getSelection())
    </script>
</html>

結果は以下の添付ファイルの通り。ChromeではSelectionオブジェクト、FireFoxではnullが表示されます。

ChromeではSelectionオブジェクトが表示され、FireFoxではnullが表示されているスクリーンショット。

対応方法

1. iframe側のビルトイン関数を上書きする

あまりやりたくない方法ですが、今回は様々な理由で他の対処ができなかったため、この方法で対応しました。
他のブラウザで悪さをしないよう、念のためUAでFireFoxだけに絞り込んでおき、getSelection関数を上書きします。

var ua = window.navigator.userAgent.toLowerCase();
if (ua.indexOf("firefox") !== -1 || ua.indexOf("fxios") !== -1) {
    const originGetSelection = window.getSelection;
    document.getSelection = function() {
        let selection = originGetSelection();
        if (selection == null || typeof selection == 'undefined') {
            // nullを参照しているせいでエラーなので、空のオブジェクトを持たせておく
            selection = {}
            return selection
        }
        return selection
    };
}

今回のケースではnullオブジェクトの中のrangeCountを参照しようとしてエラーになっていたため、
nullの場合のみ、空のオブジェクトを返すようにしておきます。(修正内容はエラーの状況によって要変更。)
本当は空のSelectionオブジェクトを持たせたかったのですが、Selectionオブジェクトを作るにはwindow.getSelection()を使わないといけないので(それがnullで帰ってくるから困るという話なので)無理でした。
new Selection()とかで作れたらいいんですが。

2. iframeを使わない

それが出来るなら苦労しないという話ではありますが、そもそもCKEditor5ではCKEditor4の頃にiframeで困ることが多かったからiframeをやめたという経緯があります。
それをわざわざiframeに突っ込み直しているのでかなり本末転倒なことをしております。

CKEditor作成部分に固有のidを割り振って置き、その下の要素にのみスタイルを反映するようにしておけばいいだけです。
例えば以下のようなHTMLを作成し、

<div id="ckeditor-box">
  // ここにCKEditorで入力した内容を展開する
  <h2>見出しだよ</h2>
</div>

以下のようなscssを作成してビルドすればいいだけの話。

#ckeditor-box {
  // ここに記事に反映させたいスタイル
  h2 {
  	font-size:16px;
  }
}

が、残念ながら弊社は歴史的経緯でcssをエンジニアが触れないので直せませんでした。
通常この手順で対応して、iframe使わないのが一番だと思います。

3. getSelection後にnullチェックを入れる

ライブラリからエラーが出てない(自前の処理の方でエラーが出てる)ケースではこれもありだと思います。
(ライブラリの場合、元ソースの処理を上書きするのは大変なのであまりやりたくない。)

let selection = document.getSelection()
if (selection == null) {
  // selectionが取得できない場合の処理
} else {
  // 既存の処理
}

4. iframeが表示できる状態になってから処理を行う。

display:noneでなければいいので、例えば自分のケースであればモーダルが表示状態になってからiframeのsrcをセットする等でも行けると思います。

が、実際の画面では子要素はdisplay:noneではないが親要素がdisplay:noneのケースがあったりして、本当にその要素が見えてるのか、またその要素が見える状態になる操作のイベントを検知するのも容易ではないケースが多いと思います。
(自分の場合も、複数のモーダルでCKEditorが使われているため、全部のモーダルのshowイベントにiframeロード処理を追加する+ページ読み込み時にはロードしない処理を入れるというのは現実的ではなく諦めました。)

おわりに

FireFoxかつ、iframeが非表示の状態で存在し、iframe内でgetSelectionを行っている場合」、というかなり限られた状態でのみ起こる事象ですが、限られてる分情報も少なかったので原因と対策を残しておきます。

記事の作成者のA.W.のアイコン

この記事を書いた人

A.W.
茨城県在住Webエンジニアです。 PHPなどを業務で使用しています。 趣味ではGoやNuxt、Flutterをやってます。

Comment