【JS】見出しタグ（<h1>～<h6>）を一括取得する関数（親要素やクラス等の指定にも対応）

見出しタグを `.querySelectorAll()` で取得する場合，いちいち `.querySelectorAll( 'h1, h2, h3, h4, h5, h6' )` などと書かなければならず面倒くさい．SCSS なら，

```SCSS
.post--content {
	h1,
	h2,
	h3,
	h4,
	h5,
	h6 {
		&.heading--background-black {
			// なんらかのスタイル指定．
		}
	}
}
```

みたいにネストさせて書けるセレクタも，いちいち，

```javascript
document.querySelectorAll(
	'.post__content h1.heading--background-black' +
	'.post__content h2.heading--background-black' +
	'.post__content h3.heading--background-black' +
	'.post__content h4.heading--background-black' +
	'.post__content h5.heading--background-black' +
	'.post__content h6.heading--background-black',
);
```

といった具合に書かなければならず面倒くさい．こういう面倒くさいのをいちいち手作業でやってると，エラーの原因にもなりがちなので，関数化しました．

なお，本記事における “hx” は、“h1”，“h2”, “h3”, “h4”, “h5”, “h6” の総称です．

## コード

### TypeScript

```typescript:headings.ts
/**
 * @author JuthaDDA
 * @param elderSelector - ex. '.post', 'section >', 'hr +', '.meta ~'.
 *     White space should be added after this selector.
 * @param classSelector - ex. '.heading--red', ':hover', ':not(:first-child)'.
 *     White space before this selector should be trimmed
 *     to avoid being interpreted as a nested selector.
 * @param topLevel - should be corrected to an integer in the range 1-6
 *     (ex. 0 to 1, 42 to 6, 1.9 to 1).
 * @param bottomLevel - should be corrected to an integer
 *     in the range from `topLevel` to 6.
 */
interface HxSelectorOptions {
	elderSelector?:string,
	classSelector?:string,
	topLevel?:number,
	bottomLevel?:number
}

/**
 * @author JuthaDDA
 */
interface HxOptions extends HxSelectorOptions {
	root?:Element|Document,
}

/**
 * @author JuthaDDA
 */
const getHxSelector = ( options:HxSelectorOptions ):string => {
	const { min, max, floor } = Math;
	const elderSelector = options.elderSelector ?
		`${ options.elderSelector } ` : '';
	const classSelector = options.classSelector ?
		options.classSelector.trim() : '';
	const topLevel = floor( min( max( options.topLevel || 1, 1 ), 6 ) );
	const bottomLevel =
		floor( min( max( options.bottomLevel || 6, topLevel ), 6 ) );

	return Array.from( Array( bottomLevel - topLevel + 1 ), ( v, k ) => {
		const hxTagName = `h${ k + topLevel }`;
		return elderSelector + hxTagName + classSelector;
	} ).join( ',' );
};

/**
 * @author JuthaDDA
 */
const getHxs = ( options:HxOptions ):NodeListOf<HTMLHeadingElement> => {
	const root = options.root || document;
	return root.querySelectorAll( getHxSelector( options ) );
};

```

### JavaScript

<details><div>

```javascript:headings.js
/**
 * @author JuthaDDA
 * @param {Object} [options]
 * @param {string} [options.elderSelector]
 *     - ex. '.post', 'section >', 'hr +', '.meta ~'.
 *         White space will be added after this selector.
 * @param  {string} [options.classSelector]
 *     - ex. '.heading--red', ':hover', ':not(:first-child)'.
 *         White space before this selector will be trimmed
 *         to avoid being interpreted as a nested selector.
 * @param {number} [options.topLevel]
 *     - will be corrected to an integer in the range 1-6
 *         (ex. 0 to 1, 42 to 6, 1.9 to 1).
 * @param {number} [options.bottomLevel]
 *     - will be corrected to an integer in the range from `topLevel` to 6.
 * @return {string}
 */
const getHxSelector = ( options = {} ) => {
	const { min, max, floor } = Math;
	const elderSelector = options.elderSelector ?
		`${ options.elderSelector } ` : '';
	const classSelector = options.classSelector ?
		options.classSelector.trim() : '';
	const topLevel = floor( min( max( options.topLevel || 1, 1 ), 6 ) );
	const bottomLevel =
		floor( min( max( options.bottomLevel || 6, topLevel ), 6 ) );

	return Array.from( Array( bottomLevel - topLevel + 1 ), ( v, k ) => {
		const hxTagName = `h${ k + topLevel }`;
		return elderSelector + hxTagName + classSelector;
	} ).join( ',' );
};

/**
 * @author JuthaDDA
 * @see getHxSelector for further about `options`.
 * @param {Object} [options]
 * @param {Element|Document} [options.root]
 * @param {string} [options.elderSelector]
 * @param {string} [options.classSelector]
 * @param {number} [options.topLevel]
 * @param {number} [options.bottomLevel]
 * @return {NodeList}
 */
const getHxs = ( options = {} ) => {
	const root = options.root || document;
	return root.querySelectorAll( getHxSelector( options ) );
};

```

</div></details>

## 説明

### getHxSelector()

第 1 引数 `options` のプロパティは，すべてオプショナルです．

`options.elderSelector` は，各 `hx` の前に附くセレクターです．子孫結合子のほか，子結合子，一般兄弟結合子，隣接兄弟接合子も使えます．後ろに半角スペースが挿入されるので，子孫結合子の半角スペースは省略可能です．

`options.classSelector` は，各 `hx` の後に附くセレクターです．クラス・セレクター，擬似クラス・セレクターが使えます．半角スペースが頭に含まれると子孫結合子として解釈されるので，`.trim()` されます．

`options.topLevel` と `options.bottomLevel` で取得する見出しレベルの範囲を指定できます．たとえば，`{ topLevel: 2, bottomLevel: 4 }` と指定した場合は，h2, h3, h4 が対象となり，h1, h5, h6 は対象外となります．

`Array.from( Array( n ), ( v, k ) => k )` [<sup>MDN</sup>](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from "Array.from() - JavaScript | MDN") は，JavaScript でライブラリを使用せず 0 から n-1 の配列を作る方法の 1 つです[^range]．第 2 引数のコールバック関数を書き換えることで，`lodash.range( n ).map( ( n ) => someFunc( n ) )` [<sup>Lodash</sup>](https://lodash.com/docs/4.17.15#range "Lodash Documentation # _.range([start=0], end, [step=1])") 的なことをライブラリなしのメソッド 1 つで書けるので，覚えておいて損はないかと思います．

戻り値は `string` です．

[^range]: Cf: [jsでrange関数をつくる - Qiita](https://qiita.com/akameco/items/a2b698dd4a067754997b)

### getHxs()

`options.root` は，`document` が既定で，`Element` を指定することで適用範囲を限定できます．その他の `options` のプロパティは，そのまま `getHxSelector()` に渡され，`getHxSelector()` から戻ってきたセレクターに一致する見出し要素の `NodeList` が `getHxs()` の戻り値になります[^return]．

[^return]: TypeScript での戻り値の型指定が `NodeListOf<Element>` じゃなくて `NodeListOf<HTMLHeadingElement>` でエラーが出ないのが，ちょっと不思議です．

## あとがき

[前回の記事](https://qiita.com/juthaDDA/items/974fda70945750e68120#fn3 "element.tagName は readonly なので，HTML 要素のタグ名を変更する関数を作った - Qiita")で予告した “<hx> タグのレベルを一括して変更する関数” で今回の関数を使っていることに気付いたので，べつの記事にしたほうがいいかなと思い，こちらを先に投稿しました．

以上です．