- エーオーシステム コーポレートサイト
https://www.aosystem.co.jp/ - エーオーシステム プロダクトサイト
https://ao-system.net/ - レンタルサーバー
- バーチャル展示会
- ウェブカタログサービス
- 3Dグラフィック
- Android アプリ
- iOS (iPhone,iPad) アプリ
- Flutter開発
- プログラミング記録QuickAnswer
- 無料画像素材
- スカイボックス 3D SKY BOX
このページのQRコード
・textareaのサイズ変更にヘッダ部分が追従。
→ MutationObserver()でstyleの変更を監視。
・Closed な Shadow DOM なのにstyle設定可能。
→ Light DOM の style を Shadow DOM にコピー。
・複数の Light DOM に個別に適用。
→ Shadow DOM でクラスのインスタンス生成。
使用結果
オンライン音声を選択した場合は最後まで読み上げされません。及び完全なステータスが取得できません。
ここで書いたプログラムコードは実験的なものであり、実用性を求める場合は以下のページをご利用ください。
[HTML]
<style>
textarea-speak.one {
width: 100%;
height: 100px;
border: solid 1px #4ad;
}
textarea-speak.two {
width: 400px;
height: 300px;
border-style: dashed;
border-width: 0px 2px 2px 2px;
border-color: #d4a;
background-color: #fee;
padding: 5px 10px;
color: #333;
}
</style>
<textarea-speak class="one"></textarea-speak>
<textarea-speak class="two">ウェブコンポ・・・ます。</textarea-speak>
<script defer src="./js/ce_textarea_speak.js"></script>
/**
* 読み上げ付きtextarea
*
* @author ao-system, Inc.
* @date 2024-03-02
*
* e.g.
* <textarea-speak></textarea-speak>
* <script defer src="ce_textarea_speak.js"></script>
*
*/
(() => {
'use strict';
class TextareaSpeak {
#constProperties = [
{ prop: 'style',
value: `
:host > div { /*wrapper*/
--color-bar-text1: #fff;
--color-bar-text2: #000;
width: 100%;
position: relative;
> div.first { /*bar first*/
width: 100%;
padding: 2px 5px;
box-sizing: border-box;
> select {
font-size: 13px;
color: #000;
background-color: transparent;
border: solid 1px var(--color-bar-text2);
}
}
> div.second { /*bar second*/
width: 100%;
padding: 2px 5px;
box-sizing: border-box;
display: flex;
align-items: center;
column-gap: 10px;
> div {
user-select: none;
font-size: 14px;
color: var(--color-bar-text1);
cursor: pointer;
opacity: 1;
&.off {
opacity: 0.5;
}
}
> span {
user-select: none;
font-size: 14px;
color: var(--color-bar-text2);
}
}
> textarea {
box-sizing: border-box;
}
}
`,
},
{ prop: 'styleElm',
value: document.createElement('style'),
},
{ prop: 'wrapElm',
value: document.createElement('div'),
},
{ prop: 'barFirstElm',
value: document.createElement('div'),
},
{ prop: 'barSecondElm',
value: document.createElement('div'),
},
{ prop: 'selectElm',
value: document.createElement('select'),
},
{ prop: 'playElm',
value: document.createElement('div'),
},
{ prop: 'pauseElm',
value: document.createElement('div'),
},
{ prop: 'resumeElm',
value: document.createElement('div'),
},
{ prop: 'cancelElm',
value: document.createElement('div'),
},
{ prop: 'statusElm',
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}));
this.#setVoiceSelect();
speechSynthesis.addEventListener('voiceschanged',() => {
this.#setVoiceSelect();
this.#cancel();
});
}
#setVoiceSelect() {
if (this.selectElm.innerHTML != '') {
return;
}
const voices = speechSynthesis.getVoices();
const optionVoices = [];
for (let i = 0; i < voices.length; i++) {
const lang = voices[i]['lang'];
const nm = voices[i]['name'];
const df = voices[i]['default'];
const lo = voices[i]['localService'];
const elm = document.createElement('option');
elm.value = i;
if (df) {
elm.setAttribute('selected','selected');
}
elm.innerHTML = lang + ':' + nm;
elm.setAttribute('data-local',lo);
optionVoices.push(elm);
}
for (let i = 0; i < optionVoices.length; i++) {
this.selectElm.appendChild(optionVoices[i]);
}
}
#speak() {
if (speechSynthesis.speaking) {
return;
}
const ssu = new SpeechSynthesisUtterance();
const voices = speechSynthesis.getVoices();
const num = this.selectElm.value;
ssu.voice = voices[num];
ssu.volume = 1;
ssu.rate = 1;
ssu.pitch = 1;
ssu.text = this.textareaElm.value;
speechSynthesis.speak(ssu);
ssu.addEventListener('start', () => { this.#setSpeakStatus(); });
ssu.addEventListener('end', () => { this.#setSpeakStatus(); });
ssu.addEventListener('pause', () => { this.#setSpeakStatus(); });
ssu.addEventListener('resume', () => { this.#setSpeakStatus(); });
ssu.addEventListener('error', () => { this.#setSpeakStatus(); });
}
#setSpeakStatus() {
const selectedOption = this.selectElm.options[this.selectElm.selectedIndex];
const dataLocal = selectedOption.getAttribute('data-local');
if (dataLocal == 'true') {
if (speechSynthesis.speaking == true && speechSynthesis.paused == false && speechSynthesis.pending == false) {
this.statusElm.textContent = 'start';
this.playElm.classList.add('off');
this.pauseElm.classList.remove('off');
this.resumeElm.classList.add('off');
this.cancelElm.classList.remove('off');
} else if (speechSynthesis.speaking == true && speechSynthesis.paused == true && speechSynthesis.pending == false) {
this.statusElm.textContent = 'pause';
this.playElm.classList.add('off');
this.pauseElm.classList.add('off');
this.resumeElm.classList.remove('off');
this.cancelElm.classList.remove('off');
} else {
this.statusElm.textContent = 'ready';
this.playElm.classList.remove('off');
this.pauseElm.classList.add('off');
this.resumeElm.classList.add('off');
this.cancelElm.classList.add('off');
}
} else {
if (speechSynthesis.speaking == true && speechSynthesis.paused == false && speechSynthesis.pending == false) {
this.statusElm.textContent = 'start';
this.playElm.classList.add('off');
this.pauseElm.classList.remove('off');
this.resumeElm.classList.remove('off');
this.cancelElm.classList.remove('off');
} else {
this.statusElm.textContent = 'ready';
this.playElm.classList.remove('off');
this.pauseElm.classList.add('off');
this.resumeElm.classList.add('off');
this.cancelElm.classList.add('off');
}
}
}
#pause() {
speechSynthesis.pause();
}
#resume() {
speechSynthesis.resume();
}
#cancel() {
speechSynthesis.cancel();
}
//styleを作成
#styleElement() {
this.styleElm.textContent = this.style;
}
//barを作成
#barElement(elementRef) {
this.barFirstElm.classList.add('first');
this.barSecondElm.classList.add('second');
//
this.barFirstElm.appendChild(this.selectElm);
//
this.playElm.textContent = 'play';
this.playElm.addEventListener('click',() => {this.#speak();});
this.barSecondElm.appendChild(this.playElm);
//
this.pauseElm.textContent = 'pause';
this.pauseElm.addEventListener('click',() => {this.#pause();});
this.pauseElm.classList.add('off');
this.barSecondElm.appendChild(this.pauseElm);
//
this.resumeElm.textContent = 'resume';
this.resumeElm.addEventListener('click',() => {this.#resume();});
this.resumeElm.classList.add('off');
this.barSecondElm.appendChild(this.resumeElm);
//
this.cancelElm.textContent = 'cancel';
this.cancelElm.addEventListener('click',() => {this.#cancel();});
this.cancelElm.classList.add('off');
this.barSecondElm.appendChild(this.cancelElm);
//
this.barSecondElm.appendChild(this.statusElm);
//
this.wrapElm.appendChild(this.barFirstElm);
this.wrapElm.appendChild(this.barSecondElm);
}
//textareaを作成
#textareaElement(elementRef) {
this.textareaElm.value = elementRef.innerHTML;
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-speakのスタイルを取得して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-speakのborderTopColorをshadow内barのbackgroundColorに適用
this.barFirstElm.style.backgroundColor = computedStyle.borderTopColor;
this.barSecondElm.style.backgroundColor = computedStyle.borderTopColor;
//textarea-speakのborderTopLeftRadius,borderTopRightRadiusをbarFirstElmのborderTopLeftRadius,borderTopRightRadiusに適用
this.barFirstElm.style.setProperty('border-top-left-radius',computedStyle.borderTopLeftRadius);
this.barFirstElm.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-speakのスタイルを削除。<textarea-sort/>を非表示にする為
elementRef.style.border = 'none';
elementRef.style.padding = 0;
}
render(elementRef) {
this.#styleElement();
this.#barElement(elementRef);
this.#textareaElement(elementRef);
this.#attachShadow(elementRef);
this.#observeTextarea();
this.#applyStyle(elementRef);
}
}
customElements.define('textarea-speak',
class extends HTMLElement {
constructor() {
super();
(new TextareaSpeak()).render(this);
}
}
);
})();
このページのQRコード
便利ウェブサイト
便利 Android アプリ
便利 iOS(iPhone,iPad) アプリ