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.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 要素” と書いていますが,厳密には,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
等については,いまのところよさそうな方法が見当たりません. ↩