element.tagName は,readonly なので1,*someElement.tagName = 'h2' といったかんじにタグ名を変更することはできません2.しかし,たとえば <h1> タグを <h2> に一括して変えたいといったことは結構あります.そういったわけで,element.tagName を変更する関数を作ってみました3.
コード
※ 公開後,戻り値を void から Element に変更しました.
※ 2020 年 9 月 22 日,element.childNodes.forEach() に関するバグを修正しました.
TypeScript
/**
* @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
/**
* @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.parentNode が null ですが,想定しうる用途をからすると if ( ! target.parentNode ) { return; } はほとんどオマジナイみたいなものだと思います.
上述のとおり Element.tagName は readonly なので,document.createElement(); で新しい要素を生成して,最後に元の target を新しく生成した replacement に置き換えています.replacement は Element.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.childeNodesArray.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 要素” と書いていますが,厳密には,SVGElement や MathMLElement といった,ほかの Element を継承する要素にも対応しているはずです(未検証).
また,target がイベント・リスナーを持っていたり,mutationObserver 等の監視対象となっていた場合は,それらを replacement に移してやることはできません6.target.onClick 等のプロパティは,おそらくそのまま移すことが可能ですが,そこだけ対応するのも中途半端かなと思い,やっていません.
参考記事
-
たとえば
<div>から<p>への変換といった場合には,HTMLDivElementからHTMLParagraphElementへとインタフェースも変わるので,まあそう簡単には変更できないわなと思います. ↩ -
<hx>タグのレベルを一括して変更する関数も作ったので,後日別途記事を立てる予定です. ↩ -
document.innerHTMLへの代入とdocument.insertAdjacentHTML()の違いについては,innerHTML より insertAdjacentHTML を使う - Qiita が参考になります. ↩ -
なぜこのような動きになるのか,詳細がわからないので,ご存じの方がいらっしゃれば教えていただきたいです.なお,Windows 版 Chrome 85 以外で同様の問題が生じるかは未検証です. ↩
-
イベント・リスナーについては,GaurangTandon/checkEventAdded GitHub を使えば,うまく移せるようにできるかもしれません.おそらく
EventTarget.prototype.addEventListenerを呼び出される前に書き換えてやる必要があるので,複数のスクリプトを呼び出しているサイトだとそれなりにめんどくさそうですが.mutationObserver等については,いまのところよさそうな方法が見当たりません. ↩