Shadow DOM 使用例

custom element と shadow DOM で作るソート機能付きtextarea

ソート機能付きtextareaを作成する。
custom element と shadow DOM で機能を持ったエレメントを作成する。

特長:

・textareaのサイズ変更にヘッダ部分が追従。
→ MutationObserver()でstyleの変更を監視。

・Closed な Shadow DOM なのにstyle設定可能。
→ Light DOM の style を Shadow DOM にコピー。

・複数の Light DOM に個別に適用。
→ Shadow DOM でクラスのインスタンス生成。

使用結果

Lion Elephant Giraffe Tiger 01 1 001 Panda Dolphin Eagle Eagle Koala Kangaroo Cheetah Dolphin Koala
使用方法
[HTML]
<style>
textarea-sort.one {
	width: 300px;
	height: 100px;
	border: solid 1px #4ad;
}
textarea-sort.two {
	width: 400px;
	height: 500px;
	border-style: dashed;
	border-width: 0px 2px 2px 2px;
	border-color: #d4a;
	border-radius: 10px;
	background-color: #fee;
	padding: 5px 10px;
	color: #333;
}
</style>

<textarea-sort class="one"></textarea-sort>

<textarea-sort class="two">Lion
Elephant
Giraffe
Tiger
01
1
001
Panda
Dolphin
Eagle
Eagle
Koala
Kangaroo
Cheetah
Dolphin
Koala</textarea-sort>

<script defer src="./js/ce_textarea_sort.js"></script>
		
現時点のコードは以下の通り:
/**
 * ソート、コピー機能付きtextarea
 *
 * @author ao-system, Inc.
 * @date 2024-03-03
 *
 * e.g.
 * <textarea-sort></textarea-sort>
 * <script defer src="ce_textarea_sort.js"></script>
 *
 */

(() => {
	'use strict';
	class TextareaSort {
		#constProperties = [
			{	prop: 'svgPath',
				value: '<path d="M2.77,10.45l-1.5,1.5V5.17c0-2.49,2.02-4.51,4.51-4.51h6.78l-1.5,1.5H5.78c-1.66,0-3.01,1.35-3.01,3.01V10.45z"/><path class="icon" d="M12.18,15.34H5.84c-1.3,0-2.35-1.05-2.35-2.35V5.15c0-1.3,1.05-2.35,2.35-2.35h6.34c1.3,0,2.35,1.05,2.35,2.35v7.84C14.53,14.29,13.48,15.34,12.18,15.34z M5.84,4.3c-0.47,0-0.85,0.38-0.85,0.85v7.84c0,0.47,0.38,0.85,0.85,0.85h6.34c0.47,0,0.85-0.38,0.85-0.85V5.15c0-0.47-0.38-0.85-0.85-0.85H5.84z"/>',
			},
			{	prop: 'style',
				value: `
					:host > div { /*wrapper*/
						--color-bar-fore: #fff;
						width: 100%;
						position: relative;
						> div {	/*bar*/
							width: 100%;
							padding: 2px 5px;
							box-sizing: border-box;
							display: flex;
							flex-wrap: wrap;
							justify-content: space-between;
							align-items: center;
							column-gap: 5px;
							> div {		/*text button*/
								font-size: 14px;
								color: var(--color-bar-fore);
								cursor: pointer;
								user-select: none;
								white-space: nowrap;
								opacity: 0.6;
								&:hover {
									opacity: 1;
								}
							}
							> span {	/*count,sign*/
								font-size: 14px;
								color: var(--color-bar-fore);
								white-space: nowrap;
							}
							> svg {		/*copy*/
								fill: var(--color-bar-fore);
								cursor: pointer;
								opacity: 0.6;
								&:hover {
									opacity: 1;
								}
							}
						}
						> textarea {
							box-sizing: border-box;
						}
					}
				`,
			},
			{	prop: 'styleElm',
				value: document.createElement('style'),
			},
			{	prop: 'wrapElm',
				value: document.createElement('div'),
			},
			{	prop: 'barElm',
				value: document.createElement('div'),
			},
			{	prop: 'countElm',
				value: document.createElement('span'),
			},
			{	prop: 'textareaElm',
				value: document.createElement('textarea'),
			},
		];
		constructor() {
			//class内でwritable:falseの定数を作成する方法
			this.#constProperties.forEach((obj) => Object.defineProperty(this, obj.prop, {value:obj.value, writable:false}));
		}
		#dataCount() {
			const count = this.textareaElm.value.split('\n').filter(line => line.trim() !== '').length;
			this.countElm.textContent = 'count:' + count;
		}
		#dataSort() {
			const ary = this.textareaElm.value.split('\n').filter(line => line.trim() !== '');
			const ary2 = ary.sort();
			this.textareaElm.value = ary2.join('\n');
		}
		#dataReverse() {
			const ary = this.textareaElm.value.split('\n').filter(line => line.trim() !== '');
			const ary2 = ary.reverse();
			this.textareaElm.value = ary2.join('\n');
		}
		#dataUnique() {
			const ary = this.textareaElm.value.split('\n').filter(line => line.trim() !== '');
			const ary2 = [...new Set(ary)];
			this.textareaElm.value = ary2.join('\n');
			this.#dataCount();
		}
		#clipboardCopy() {
			return new Promise((resolve, reject) => {
				try {
					navigator.clipboard.writeText(this.textareaElm.value).then(() => {
						resolve('Copied!');
					}, () => {
						resolve('Could not copy');
					});
				} catch(e) {
					resolve('HTTPS required');
				}
			});
		}
		//styleを作成
		#styleElement() {
			this.styleElm.textContent = this.style;
		}
		//barを作成
		#barElement() {
			this.barElm.appendChild(this.countElm);
			//
			const sortElm = document.createElement('div');
			sortElm.textContent = 'sort';
			sortElm.addEventListener('click', () => {this.#dataSort();});
			this.barElm.appendChild(sortElm);
			//
			const reverseElm = document.createElement('div');
			reverseElm.textContent = 'reverse';
			reverseElm.addEventListener('click', () => {this.#dataReverse();});
			this.barElm.appendChild(reverseElm);
			//
			const uniqueElm = document.createElement('div');
			uniqueElm.textContent = 'unique';
			uniqueElm.addEventListener('click', () => {this.#dataUnique();});
			this.barElm.appendChild(uniqueElm);
			//
			const signElm = document.createElement('span');
			this.barElm.appendChild(signElm);
			//
			const svgElm = document.createElementNS('http://www.w3.org/2000/svg','svg');
			svgElm.setAttribute('width',16);
			svgElm.setAttribute('height',16);
			svgElm.innerHTML = this.svgPath;
			svgElm.addEventListener('click', async () => {
				signElm.textContent = await this.#clipboardCopy();
				setTimeout(() => { signElm.textContent = ''; },3000);
			});
			this.barElm.appendChild(svgElm);
			this.wrapElm.appendChild(this.barElm);
		}
		//textareaを作成
		#textareaElement(elementRef) {
			this.textareaElm.value = elementRef.innerHTML;
			this.textareaElm.addEventListener('input',() => {this.#dataCount();});
			this.wrapElm.appendChild(this.textareaElm);
		}
		//shadowDOMにappend
		#attachShadow(elementRef,styleElm,wrapElm) {
			const shadowClosed = elementRef.attachShadow({mode:'closed'});
			shadowClosed.innerHTML = '';
			shadowClosed.appendChild(this.styleElm);
			shadowClosed.appendChild(this.wrapElm);
		}
		//textareaのスタイル変更を監視
		#observeTextarea() {
			const observer = new MutationObserver((mutationsList, observer) => {
				for(const mutation of mutationsList) {
					if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
						//textareaのwidthが変化したらwrapperのwidthに適用
						this.wrapElm.style.width = this.textareaElm.style.width;
					}
				}
			});
			observer.observe(this.textareaElm, { attributes:true, attributeFilter:['style'] });
		}
		//textarea-sortのスタイルを取得してshadowDOM内のエレメントに適用する
		#applyStyle(elementRef) {
			const computedStyle = window.getComputedStyle(elementRef);
			for (let i = 0; i < computedStyle.length; i++) {
			    const propertyName = computedStyle[i];
			    const propertyValue = computedStyle.getPropertyValue(propertyName);
				if (['box-sizing','resize','white-space-collapse'].includes(propertyName) == false) {	//これらを除く
				    this.textareaElm.style.setProperty(propertyName, propertyValue);
				}
			}
			//textarea-sortのborderTopColorをshadow内barのbackgroundColorに適用
			this.barElm.style.backgroundColor = computedStyle.borderTopColor;
			//textarea-sortのborderTopLeftRadius,borderTopRightRadiusをbarのborderTopLeftRadius,borderTopRightRadiusに適用
			this.barElm.style.setProperty('border-top-left-radius',computedStyle.borderTopLeftRadius);
			this.barElm.style.setProperty('border-top-right-radius',computedStyle.borderTopRightRadius);
			this.textareaElm.style.borderTopLeftRadius = 0;
			this.textareaElm.style.borderTopRightRadius = 0;
			//textareaのwidthをwrapperのwidthに適用
			this.wrapElm.style.width = this.textareaElm.clientWidth + 'px';
			//textarea-sortのスタイルを削除。<textarea-sort/>を非表示にする為
			elementRef.style.border = 'none';
			elementRef.style.padding = 0;
		}
		render(elementRef) {
			this.#styleElement();
			this.#barElement();
			this.#textareaElement(elementRef);
			this.#attachShadow(elementRef);
			this.#observeTextarea();
			this.#applyStyle(elementRef);
			this.#dataCount();
		}
	}
	customElements.define('textarea-sort',
		class extends HTMLElement {
			constructor() {
				super();
				(new TextareaSort()).render(this);
			}
		}
	);
})();
資料
custom element:
https://developer.mozilla.org/ja/docs/Web/API/Web_components/Using_custom_elements
shadow DOM:
https://developer.mozilla.org/ja/docs/Web/API/Web_components/Using_shadow_DOM
2024年3月初版
このサイトについてのお問い合わせはエーオーシステムまでお願いいたします。
ご使用上の過失の有無を問わず、本プログラムの運用において発生した損害に対するいかなる請求があったとしても、その責任を負うものではありません。