LoginSignup
4
3

More than 3 years have passed since last update.

element.tagName は readonly なので,HTML 要素のタグ名を変更する関数を作った

Last updated at Posted at 2020-09-17

element.tagName は,readonly なので1,*someElement.tagName = 'h2' といったかんじにタグ名を変更することはできません2.しかし,たとえば <h1> タグを <h2> に一括して変えたいといったことは結構あります.そういったわけで,element.tagName を変更する関数を作ってみました3

コード

※ 公開後,戻り値を void から Element に変更しました.

※ 2020 年 9 月 22 日,element.childNodes.forEach() に関するバグを修正しました.

TypeScript

replace-tag-name.ts
/**
 * @author JuthaDDA
 * @see [element.tagName は readonly なので,
 *     HTML 要素のタグ名を変更する関数を作った - Qiita](
 *     https://qiita.com/juthaDDA/items/974fda70945750e68120)
 */
const replaceTagName = ( target:Element, tagName:string ):Element => {
    if ( ! target.parentNode ) { return target; }

    const replacement = document.createElement( tagName );
    Array.from( target.attributes ).forEach( ( attribute ) => {
        const { nodeName, nodeValue } = attribute;
        if ( nodeValue ) {
            replacement.setAttribute( nodeName, nodeValue );
        }
    } );
    Array.from( target.childNodes ).forEach( ( node ) => {
        replacement.appendChild( node );
    } ); // For some reason, only textNodes are appended
        // without converting childNodes to Array.
    target.parentNode.replaceChild( replacement, target );
    return replacement;
};

JavaScript

replace-tag-name.js
/**
 * @author JuthaDDA
 * @see [element.tagName は readonly なので,
 *     HTML 要素のタグ名を変更する関数を作った - Qiita](
 *     https://qiita.com/juthaDDA/items/974fda70945750e68120)
 * @param {Element} target
 * @param {string} tagName
 * @return {Element}
 */
replaceTagName = ( target, tagName ) => {
    if ( ! target.parentNode ) { return target; }

    const replacement = document.createElement( tagName );
    Array.from( target.attributes ).forEach( ( attribute ) => {
        const { nodeName, nodeValue } = attribute;
        if ( nodeValue ) {
            replacement.setAttribute( nodeName, nodeValue );
        }
    } );
    Array.from( target.childNodes ).forEach( ( node ) => {
        replacement.appendChild( node );
    } ); // For some reason, only textNodes are appended
        // without converting childNodes to Array.
    target.parentNode.replaceChild( replacement, target );
    return replacement;
};

説明

第 1 引数にタグ名を変更したい Element,第 2 引数に変更後のタグ名を指定します.

Document.createElement() で生成した要素なんかは,target.parentNodenull ですが,想定しうる用途をからすると if ( ! target.parentNode ) { return; } はほとんどオマジナイみたいなものだと思います.

上述のとおり Element.tagNamereadonly なので,document.createElement(); で新しい要素を生成して,最後に元の target を新しく生成した replacement に置き換えています.replacementElement.tagName 以外とくに設定されていない要素なので,属性と内容を元の target からコピーしてくる必要があります.

まず属性ですが,Element.attributes もこれまた readonly なので,そのまま *replacement.attributes = target.attributes とか *Object.assign( replacement.attributes, target.attributes ) とか *replacement.attributes = { ...target.attributes } とかすることはできません.なので,target.attributes の各要素(のうち値をもつもの)ごとに,replacement.setAttribute() してやる必要があります.

内容については,replacement.innerHTML = target.innerHTML とか,replacement.insertAdjacentHTML( target.innerHTML ) 4とかでもおおむね問題ないですが,子要素がイベント・リスナーを持っていたり,mutationObserver 等のオブザーバー系 API の監視対象となっていた場合にも簡単に対応できるので,target.childeNodes Array.from( target.childNodes ).forEach() で回して,target.appendChild() してやっています.

※ 2020 年 9 月 22 日追記:target.childeNodes をそのまま .forEach() で回すと,textNode のみが対象となることが判明しました.Array.from() で配列に変換してやってから .forEach() で回すことにより,この問題は解消されるようです 5

【追記】戻り値は,タグ名の変更が成功した場合は replacement,失敗した場合は target です.以下のように書けば,成否を判定できます.

const h1Replacement = replaceTagName( h1, 'h2' )
const hasH1BeenReplaced = h1 !== h1Replacement;

補足

上では “HTML 要素” と書いていますが,厳密には,SVGElementMathMLElement といった,ほかの Element を継承する要素にも対応しているはずです(未検証).

また,target がイベント・リスナーを持っていたり,mutationObserver 等の監視対象となっていた場合は,それらを replacement に移してやることはできません6target.onClick 等のプロパティは,おそらくそのまま移すことが可能ですが,そこだけ対応するのも中途半端かなと思い,やっていません.

参考記事


  1. Cf. Document Object Model Core # Interface Element

  2. たとえば <div> から <p> への変換といった場合には,HTMLDivElement から HTMLParagraphElement へとインタフェースも変わるので,まあそう簡単には変更できないわなと思います. 

  3. <hx> タグのレベルを一括して変更する関数も作ったので,後日別途記事を立てる予定です. 

  4. document.innerHTML への代入と document.insertAdjacentHTML() の違いについては,innerHTML より insertAdjacentHTML を使う - Qiita が参考になります. 

  5. なぜこのような動きになるのか,詳細がわからないので,ご存じの方がいらっしゃれば教えていただきたいです.なお,Windows 版 Chrome 85 以外で同様の問題が生じるかは未検証です.  

  6. イベント・リスナーについては,GaurangTandon/checkEventAdded GitHub を使えば,うまく移せるようにできるかもしれません.おそらく EventTarget.prototype.addEventListener を呼び出される前に書き換えてやる必要があるので,複数のスクリプトを呼び出しているサイトだとそれなりにめんどくさそうですが.mutationObserver 等については,いまのところよさそうな方法が見当たりません. 

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3