| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function analyzeText(text, readingWPM, speakingWPM) { |
| 5 |
var words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length; |
| 6 |
var chars = text.length; |
| 7 |
var charsNoSp = text.replace(/\s/g, '').length; |
| 8 |
var sentences = text.trim() === '' ? 0 : (text.match(/[^.!?]+[.!?]+/g) || []).length; |
| 9 |
var paras = text.trim() === '' ? 0 : (text.trim().split(/\n\s*\n+/).filter(function(p) { return p.trim() !== ''; }).length || (text.trim() !== '' ? 1 : 0)); |
| 10 |
var readSec = words > 0 ? Math.ceil((words / readingWPM) * 60) : 0; |
| 11 |
var speakSec = words > 0 ? Math.ceil((words / speakingWPM) * 60) : 0; |
| 12 |
return { words: words, chars: chars, charsNoSp: charsNoSp, sentences: sentences, paragraphs: paras, readSec: readSec, speakSec: speakSec }; |
| 13 |
} |
| 14 |
|
| 15 |
function fmtTime(secs) { |
| 16 |
if (secs < 60) return secs + 's'; |
| 17 |
var m = Math.floor(secs / 60); |
| 18 |
var s = secs % 60; |
| 19 |
return m + 'm' + (s ? ' ' + s + 's' : ''); |
| 20 |
} |
| 21 |
|
| 22 |
function buildApp(app) { |
| 23 |
var opts = {}; |
| 24 |
try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch (e) {} |
| 25 |
|
| 26 |
var o = opts; |
| 27 |
var rWPM = parseInt(o.readingWPM) || 238; |
| 28 |
var sWPM = parseInt(o.speakingWPM) || 130; |
| 29 |
var radius = (o.cardRadius || 16) + 'px'; |
| 30 |
var inpRad = (o.inputRadius || 8) + 'px'; |
| 31 |
var accentC = o.accentColor || '#6c3fb5'; |
| 32 |
var labelC = o.labelColor || '#374151'; |
| 33 |
var wrdC = o.wordCountColor || '#6c3fb5'; |
| 34 |
var charC = o.charCountColor || '#3b82f6'; |
| 35 |
var sentC = o.sentenceColor || '#10b981'; |
| 36 |
var paraC = o.paragraphColor || '#f59e0b'; |
| 37 |
var readC = o.readingColor || '#8b5cf6'; |
| 38 |
var speakC = o.speakingColor || '#ec4899'; |
| 39 |
var barFillC = o.limitBarFill || '#6c3fb5'; |
| 40 |
var barOverC = o.limitBarOver || '#ef4444'; |
| 41 |
var barBgC = o.limitBarBg || '#e5e7eb'; |
| 42 |
var statBg = o.statCardBg || '#f5f3ff'; |
| 43 |
var statBdr = o.statCardBorder || '#ede9fe'; |
| 44 |
var txtaBg = o.textareaBg || '#f9fafb'; |
| 45 |
var txtaBdr = o.textareaBorder || '#e5e7eb'; |
| 46 |
var valSz = o.statValueSize || 36; |
| 47 |
var charLimit = parseInt(o.charLimit) || 500; |
| 48 |
|
| 49 |
app.innerHTML = ''; |
| 50 |
app.style.fontFamily = 'inherit'; |
| 51 |
app.style.boxSizing = 'border-box'; |
| 52 |
|
| 53 |
// Section wrapper |
| 54 |
var section = document.createElement('div'); |
| 55 |
section.style.paddingTop = (o.paddingTop || 60) + 'px'; |
| 56 |
section.style.paddingBottom = (o.paddingBottom || 60) + 'px'; |
| 57 |
section.style.background = o.sectionBg || ''; |
| 58 |
app.appendChild(section); |
| 59 |
|
| 60 |
var card = document.createElement('div'); |
| 61 |
card.style.cssText = 'background:' + (o.cardBg || '#fff') + ';border-radius:' + radius + ';padding:36px 32px;max-width:' + (o.maxWidth || 680) + 'px;margin:0 auto;box-shadow:0 4px 24px rgba(0,0,0,.09);box-sizing:border-box;'; |
| 62 |
section.appendChild(card); |
| 63 |
|
| 64 |
// Title / subtitle |
| 65 |
if (o.showTitle || o.showSubtitle) { |
| 66 |
var hdr = document.createElement('div'); |
| 67 |
hdr.style.marginBottom = '20px'; |
| 68 |
if (o.showTitle) { |
| 69 |
var ttl = document.createElement('div'); |
| 70 |
ttl.className = 'bkbg-wrd-title'; |
| 71 |
ttl.textContent = o.title || 'Word Counter'; |
| 72 |
ttl.style.color = o.titleColor || '#1e1b4b'; |
| 73 |
hdr.appendChild(ttl); |
| 74 |
} |
| 75 |
if (o.showSubtitle && o.subtitle) { |
| 76 |
var sub = document.createElement('div'); |
| 77 |
sub.className = 'bkbg-wrd-subtitle'; |
| 78 |
sub.textContent = o.subtitle; |
| 79 |
sub.style.color = o.subtitleColor || '#6b7280'; |
| 80 |
hdr.appendChild(sub); |
| 81 |
} |
| 82 |
card.appendChild(hdr); |
| 83 |
} |
| 84 |
|
| 85 |
// Textarea |
| 86 |
var textarea = document.createElement('textarea'); |
| 87 |
textarea.className = 'bkbg-wrd-textarea'; |
| 88 |
textarea.placeholder = o.placeholder || 'Start typing or paste your text here\u2026'; |
| 89 |
textarea.value = o.defaultText || ''; |
| 90 |
textarea.style.cssText = 'width:100%;height:' + (o.textareaHeight || 200) + 'px;padding:14px 16px;border-radius:' + inpRad + ';border:2px solid ' + txtaBdr + ';background:' + txtaBg + ';font-size:15px;line-height:1.6;resize:vertical;outline:none;box-sizing:border-box;margin-bottom:16px;font-family:inherit;transition:border-color .2s;'; |
| 91 |
textarea.addEventListener('focus', function () { textarea.style.borderColor = accentC; textarea.style.boxShadow = '0 0 0 3px rgba(108,63,181,.15)'; }); |
| 92 |
textarea.addEventListener('blur', function () { |
| 93 |
var over = o.showCharLimit && textarea.value.length > charLimit; |
| 94 |
textarea.style.borderColor = over ? barOverC : txtaBdr; |
| 95 |
textarea.style.boxShadow = ''; |
| 96 |
}); |
| 97 |
textarea.addEventListener('input', update); |
| 98 |
card.appendChild(textarea); |
| 99 |
|
| 100 |
// Char limit bar |
| 101 |
var limitSection = null; |
| 102 |
var limitLabel = null; |
| 103 |
var limitFill = null; |
| 104 |
if (o.showCharLimit) { |
| 105 |
limitSection = document.createElement('div'); |
| 106 |
limitSection.style.marginBottom = '16px'; |
| 107 |
var limitRow = document.createElement('div'); |
| 108 |
limitRow.style.cssText = 'display:flex;justify-content:space-between;font-size:12px;color:' + labelC + ';margin-bottom:5px;'; |
| 109 |
var limitTitle = document.createElement('span'); limitTitle.textContent = 'Character Limit'; |
| 110 |
limitLabel = document.createElement('span'); |
| 111 |
limitRow.appendChild(limitTitle); limitRow.appendChild(limitLabel); |
| 112 |
limitSection.appendChild(limitRow); |
| 113 |
var limitTrack = document.createElement('div'); |
| 114 |
limitTrack.className = 'bkbg-wrd-limit-bar-track'; |
| 115 |
limitTrack.style.cssText = 'height:8px;border-radius:100px;background:' + barBgC + ';overflow:hidden;'; |
| 116 |
limitFill = document.createElement('div'); |
| 117 |
limitFill.className = 'bkbg-wrd-limit-bar-fill'; |
| 118 |
limitFill.style.cssText = 'height:100%;width:0%;background:' + barFillC + ';border-radius:100px;transition:width .3s,background .3s;'; |
| 119 |
limitTrack.appendChild(limitFill); |
| 120 |
limitSection.appendChild(limitTrack); |
| 121 |
card.appendChild(limitSection); |
| 122 |
} |
| 123 |
|
| 124 |
// Stats grid |
| 125 |
var statsGrid = document.createElement('div'); |
| 126 |
statsGrid.className = 'bkbg-wrd-stats-grid'; |
| 127 |
statsGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;'; |
| 128 |
card.appendChild(statsGrid); |
| 129 |
|
| 130 |
// Stat definitions |
| 131 |
var statDefs = [ |
| 132 |
{ key: 'words', label: 'Words', val: 0, color: wrdC, show: true }, |
| 133 |
{ key: 'chars', label: 'Characters', val: 0, color: charC, show: true }, |
| 134 |
{ key: 'charsNoSp', label: 'Chars (no spaces)', val: 0, color: charC, show: !!o.showCharNoSpaces }, |
| 135 |
{ key: 'sentences', label: 'Sentences', val: 0, color: sentC, show: !!o.showSentences }, |
| 136 |
{ key: 'paragraphs', label: 'Paragraphs', val: 0, color: paraC, show: !!o.showParagraphs }, |
| 137 |
{ key: 'readTime', label: 'Reading Time', val: 0, color: readC, show: !!o.showReadingTime }, |
| 138 |
{ key: 'speakTime', label: 'Speaking Time', val: 0, color: speakC, show: !!o.showSpeakingTime } |
| 139 |
].filter(function(s) { return s.show; }); |
| 140 |
|
| 141 |
var statEls = {}; |
| 142 |
statDefs.forEach(function (s) { |
| 143 |
var card2 = document.createElement('div'); |
| 144 |
card2.className = 'bkbg-wrd-stat-card'; |
| 145 |
card2.style.cssText = 'background:' + statBg + ';border:1.5px solid ' + statBdr + ';border-radius:' + radius + ';padding:16px 14px;text-align:center;transition:transform .18s,box-shadow .18s;'; |
| 146 |
var valEl = document.createElement('div'); |
| 147 |
valEl.className = 'bkbg-wrd-stat-value'; |
| 148 |
valEl.textContent = '0'; |
| 149 |
valEl.style.cssText = 'font-size:' + valSz + 'px;font-weight:800;color:' + s.color + ';line-height:1.1;'; |
| 150 |
var lblEl = document.createElement('div'); |
| 151 |
lblEl.textContent = s.label; |
| 152 |
lblEl.style.cssText = 'font-size:12px;color:' + labelC + ';margin-top:4px;font-weight:600;'; |
| 153 |
card2.appendChild(valEl); |
| 154 |
card2.appendChild(lblEl); |
| 155 |
statsGrid.appendChild(card2); |
| 156 |
statEls[s.key] = valEl; |
| 157 |
}); |
| 158 |
|
| 159 |
function update() { |
| 160 |
var text = textarea.value; |
| 161 |
var stats = analyzeText(text, rWPM, sWPM); |
| 162 |
var over = o.showCharLimit && stats.chars > charLimit; |
| 163 |
|
| 164 |
// Update textarea border |
| 165 |
if (document.activeElement !== textarea) { |
| 166 |
textarea.style.borderColor = over ? barOverC : txtaBdr; |
| 167 |
} |
| 168 |
|
| 169 |
// Update stat cards |
| 170 |
if (statEls.words) statEls.words.textContent = stats.words; |
| 171 |
if (statEls.chars) statEls.chars.textContent = stats.chars; |
| 172 |
if (statEls.charsNoSp) statEls.charsNoSp.textContent = stats.charsNoSp; |
| 173 |
if (statEls.sentences) statEls.sentences.textContent = stats.sentences; |
| 174 |
if (statEls.paragraphs) statEls.paragraphs.textContent= stats.paragraphs; |
| 175 |
if (statEls.readTime) statEls.readTime.textContent = fmtTime(stats.readSec); |
| 176 |
if (statEls.speakTime) statEls.speakTime.textContent = fmtTime(stats.speakSec); |
| 177 |
|
| 178 |
// Update limit bar |
| 179 |
if (o.showCharLimit && limitFill && limitLabel) { |
| 180 |
var pct = Math.min(100, (stats.chars / charLimit) * 100); |
| 181 |
limitFill.style.width = pct.toFixed(1) + '%'; |
| 182 |
limitFill.style.background = over ? barOverC : barFillC; |
| 183 |
limitLabel.textContent = stats.chars + ' / ' + charLimit; |
| 184 |
limitLabel.style.color = over ? barOverC : labelC; |
| 185 |
limitLabel.style.fontWeight= over ? '700' : '400'; |
| 186 |
} |
| 187 |
} |
| 188 |
|
| 189 |
update(); |
| 190 |
} |
| 191 |
|
| 192 |
document.querySelectorAll('.bkbg-wrd-app').forEach(buildApp); |
| 193 |
})(); |
| 194 |
|