156 lines
5.5 KiB
JavaScript
156 lines
5.5 KiB
JavaScript
class TypingPractice extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.dict = {}
|
|
this.dict_display = null
|
|
}
|
|
|
|
async connectedCallback() {
|
|
await this.render();
|
|
}
|
|
|
|
async render() {
|
|
const raw = await fetch('https://korean-advice-open-api.vercel.app/api/advice')
|
|
const res = await raw.json()
|
|
const author = `- ${res['author']} (${res['authorProfile']})`
|
|
const sentence = res["message"]
|
|
const sentence_jamo = decompose(sentence)
|
|
let progress = 0
|
|
let recording_mistake = false
|
|
|
|
const dict = this.dict
|
|
|
|
if (!this.dict_display) {
|
|
this.dict_display = document.createElement('div')
|
|
this.dict_display.className = 'dict-display'
|
|
document.body.append(this.dict_display)
|
|
}
|
|
|
|
const updateDict = () => {
|
|
const sorted = Object.entries(dict).sort((a, b) => b[1] - a[1])
|
|
this.dict_display.innerHTML = sorted.length
|
|
? sorted.map(([ch, cnt]) => `<span class="dict-item"><span class="dict-char">${ch}</span><span class="dict-count">${cnt}</span></span>`).join('')
|
|
: ''
|
|
}
|
|
|
|
const sentence_display = document.createElement('div')
|
|
sentence_display.className = 'sentence'
|
|
;[...sentence].forEach(ch => {
|
|
const span = document.createElement('span')
|
|
span.textContent = ch
|
|
span.className = 'pending'
|
|
sentence_display.append(span)
|
|
})
|
|
|
|
const author_info = document.createElement('p')
|
|
author_info.className = 'author'
|
|
author_info.textContent = author
|
|
|
|
const updateColors = (has_error) => {
|
|
;[...sentence].forEach((_, idx) => {
|
|
const span = sentence_display.children[idx]
|
|
if (idx < progress) {
|
|
span.className = 'correct'
|
|
} else if (idx === progress && has_error) {
|
|
span.className = 'wrong'
|
|
} else {
|
|
span.className = 'pending'
|
|
}
|
|
})
|
|
}
|
|
|
|
const input_box = document.createElement('input')
|
|
input_box.setAttribute('autofocus', '')
|
|
input_box.addEventListener('input', (e) => {
|
|
let cur = e.target.value
|
|
let cur_jamo = decompose(cur)
|
|
|
|
if (cur.length < progress) {
|
|
e.target.value = sentence.slice(0, progress)
|
|
updateColors(false)
|
|
} else {
|
|
let i = 0
|
|
let found_mistake = false
|
|
while (i < Math.min(sentence_jamo.length, cur_jamo.length)) {
|
|
if (sentence_jamo[i] !== cur_jamo[i]) {
|
|
found_mistake = true
|
|
if (!recording_mistake) {
|
|
recording_mistake = true
|
|
dict[sentence_jamo[i]] = (dict[sentence_jamo[i]] || 0) + 1
|
|
updateDict()
|
|
}
|
|
break
|
|
}
|
|
i += 1
|
|
}
|
|
if (!found_mistake) recording_mistake = false
|
|
|
|
const sentence_sliced = sentence.slice(progress, cur.length)
|
|
const cur_sliced = cur.slice(progress)
|
|
|
|
let j = 0
|
|
while (j < Math.min(sentence_sliced.length, cur_sliced.length)) {
|
|
if (sentence_sliced[j] !== cur_sliced[j]) {
|
|
break
|
|
}
|
|
j += 1
|
|
}
|
|
progress += j
|
|
|
|
updateColors(found_mistake)
|
|
|
|
if (progress === sentence.length) {
|
|
this.innerHTML = ''
|
|
this.render().then(() => {
|
|
this.querySelector('input')?.focus()
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
this.append(sentence_display)
|
|
this.append(author_info)
|
|
this.append(input_box)
|
|
}
|
|
}
|
|
|
|
customElements.define("typing-practice", TypingPractice)
|
|
|
|
|
|
|
|
const JONG_MAP = {
|
|
'ㄱ': 'ㄱ', 'ㄲ': 'ㄲ', 'ㄳ': 'ㄱㅅ', 'ㄴ': 'ㄴ',
|
|
'ㄵ': 'ㄴㅈ', 'ㄶ': 'ㄴㅎ', 'ㄷ': 'ㄷ', 'ㄹ': 'ㄹ',
|
|
'ㄺ': 'ㄹㄱ', 'ㄻ': 'ㄹㅁ', 'ㄼ': 'ㄹㅂ', 'ㄽ': 'ㄹㅅ',
|
|
'ㄾ': 'ㄹㅌ', 'ㄿ': 'ㄹㅍ', 'ㅀ': 'ㄹㅎ', 'ㅁ': 'ㅁ',
|
|
'ㅂ': 'ㅂ', 'ㅄ': 'ㅂㅅ', 'ㅅ': 'ㅅ', 'ㅆ': 'ㅆ',
|
|
'ㅇ': 'ㅇ', 'ㅈ': 'ㅈ', 'ㅊ': 'ㅊ', 'ㅋ': 'ㅋ',
|
|
'ㅌ': 'ㅌ', 'ㅍ': 'ㅍ', 'ㅎ': 'ㅎ'
|
|
};
|
|
|
|
const CHO = ['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
|
const JUNG = ['ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'];
|
|
const JONG = ['','ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
|
|
|
|
const COMPOUND_JUNG = {
|
|
'ㅘ': 'ㅗㅏ', 'ㅙ': 'ㅗㅐ', 'ㅚ': 'ㅗㅣ',
|
|
'ㅝ': 'ㅜㅓ', 'ㅞ': 'ㅜㅔ', 'ㅟ': 'ㅜㅣ',
|
|
'ㅢ': 'ㅡㅣ'
|
|
}
|
|
|
|
function decompose(str) {
|
|
return [...str].map(ch => {
|
|
const code = ch.charCodeAt(0);
|
|
|
|
if (code >= 0xAC00 && code <= 0xD7A3) {
|
|
const offset = code - 0xAC00;
|
|
const cho = CHO[Math.floor(offset / (21 * 28))];
|
|
const jung = JUNG[Math.floor((offset % (21 * 28)) / 28)];
|
|
const jong = JONG[offset % 28];
|
|
const jung_expanded = COMPOUND_JUNG[jung] || jung
|
|
return cho + jung_expanded + (jong ? JONG_MAP[jong] : '');
|
|
}
|
|
|
|
return ch;
|
|
}).join('');
|
|
} |