Lashtw commited on
Commit
77b4f14
·
verified ·
1 Parent(s): 615844a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +170 -110
index.html CHANGED
@@ -21,7 +21,24 @@
21
  .btn:hover { filter: brightness(.94); }
22
  .btn.primary { background: #F97316; color: #fff; }
23
  .btn.danger { background: #EF4444; color: #fff; }
24
- .qr-box { margin-left: auto; text-align: right; font-size: 12px; color: #475569; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  #question-bar {
27
  display: flex; align-items: center; gap: 12px;
@@ -35,16 +52,14 @@
35
 
36
  #cloud-container {
37
  position: relative; width: calc(100% - 32px);
38
- height: calc(100vh - 300px); min-height: 420px;
39
  margin: 16px; background: #fff; border-radius: 16px; overflow: hidden;
40
  box-shadow: 0 4px 20px rgba(0,0,0,.06);
41
  }
42
  #word-canvas { width: 100%; height: 100%; display: block; cursor: pointer; }
43
-
44
  #stats { padding: 0 22px 10px; font-size: 14px; color: #475569; }
45
  #stats b { color: #F97316; font-size: 18px; }
46
 
47
- /* 詞彙選單 overlay */
48
  .overlay {
49
  display: none; position: fixed; inset: 0; z-index: 1000;
50
  background: rgba(15,23,42,.55); align-items: center; justify-content: center;
@@ -54,20 +69,16 @@
54
  max-width: 560px; width: 90%; max-height: 72vh; overflow-y: auto;
55
  box-shadow: 0 20px 60px rgba(0,0,0,.3);
56
  }
57
- .panel h3 { margin-bottom: 16px; color: #1E293B; }
58
  .word-chip {
59
  display: inline-block; margin: 5px; padding: 8px 14px; font-size: 15px;
60
  background: #FEF3C7; border: 1px solid #FCD34D; border-radius: 20px; cursor: pointer;
61
  }
62
  .word-chip:hover { background: #FDE68A; }
63
  .panel-close { margin-top: 16px; text-align: right; }
64
-
65
- /* 放大卡 */
66
- #zoom-card { text-align: center; }
67
- #zoom-text { font-size: 60px; font-weight: 900; color: #1E293B; word-break: break-word; }
68
  #zoom-actions { margin-top: 28px; display: flex; gap: 12px; justify-content: center; }
69
 
70
- /* 浮水印超連結列(DOM) */
71
  #credit-bar {
72
  position: fixed; left: 14px; bottom: 12px; z-index: 999;
73
  background: rgba(255,255,255,.9); padding: 6px 14px; border-radius: 8px;
@@ -75,53 +86,66 @@
75
  }
76
  #credit-bar a { color: #4361a8; text-decoration: none; font-weight: 600; }
77
  #credit-bar a:hover { text-decoration: underline; }
 
 
78
  </style>
79
  </head>
80
  <body>
81
 
 
 
82
  <header>
83
  <h1>☁️ 即時互動文字雲</h1>
84
- <span class="badge">房號:<span id="room-span">----</span></span>
85
- <button class="btn" id="btn-lock">🔒 鎖定房間</button>
86
- <button class="btn" id="btn-multiple">多重送出:關</button>
87
- <button class="btn" id="btn-fullscreen">⛶ 全螢幕</button>
88
- <button class="btn" id="btn-screenshot">📸 截圖</button>
89
- <button class="btn" id="btn-csv">📄 匯出 CSV</button>
90
- <button class="btn danger" id="btn-reset">🗑 清除重來</button>
91
- <div class="qr-box"><div id="qr"></div><div id="join-link" style="max-width:180px;word-break:break-all;margin-top:4px;"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
92
  </header>
93
 
94
  <div id="question-bar">
95
- <input id="q-input" type="text" placeholder="輸入討論題目,學生端將即時同步顯示…">
96
- <button class="btn primary" id="q-send">送出題目</button>
97
- <span id="question-preview"></span>
98
  </div>
99
 
100
- <div id="stats">總詞彙數量:<b id="word-count">0</b></div>
101
 
102
  <div id="cloud-container"><canvas id="word-canvas"></canvas></div>
103
 
104
- <!-- 詞彙選單 -->
105
- <div class="overlay" id="pick-overlay">
106
  <div class="panel">
107
  <h3>選擇要放大的詞彙:</h3>
108
- <div id="pick-list"></div>
109
- <div class="panel-close"><button class="btn" onclick="hidePick()">取消</button></div>
110
  </div>
111
  </div>
112
 
113
- <!-- 放大卡 -->
114
- <div class="overlay" id="zoom-overlay">
115
- <div class="panel" id="zoom-card">
116
- <div id="zoom-text"></div>
117
- <div id="zoom-actions">
118
- <button class="btn danger" id="zoom-delete">🗑 刪除此詞</button>
119
- <button class="btn" id="zoom-close">關閉</button>
120
  </div>
121
  </div>
122
  </div>
123
 
124
- <!-- 浮水印:只有「臉書社團:萬物皆數」是連結 -->
125
  <div id="credit-bar">
126
  程式設計者:精華國中 藍星宇 ·
127
  <a href="https://www.facebook.com/groups/1554372228718393" target="_blank" rel="noopener">臉書社團:萬物皆數</a>
@@ -129,7 +153,7 @@
129
 
130
  <script type="module">
131
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js';
132
- import { getFirestore, doc, setDoc, updateDoc, deleteField, onSnapshot } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js';
133
 
134
  const firebaseConfig = {
135
  apiKey: "AIzaSyAcIQYst9gFRyE2nCqCwNgCL3LG2VkBXbM",
@@ -141,45 +165,59 @@ const firebaseConfig = {
141
  };
142
 
143
  const app = initializeApp(firebaseConfig);
144
- const db = getFirestore(app);
145
 
146
- const roomCode = Math.floor(1000 + Math.random() * 9000).toString();
 
147
  let allowMultiple = false;
148
- let isLocked = false;
149
- let words = {};
150
- let isDrawing = false;
151
- let drawQueue = false;
152
  let selectedWord = null;
153
 
154
- const canvas = document.getElementById('word-canvas');
 
155
  const container = document.getElementById('cloud-container');
156
- const roomRef = doc(db, 'rooms', roomCode);
 
 
 
157
 
158
  await setDoc(roomRef, { roomCode, locked: false, allowMultiple: false, words: {}, question: '' }, { merge: true });
159
- document.getElementById('room-span').textContent = roomCode;
160
-
161
- onSnapshot(roomRef, (snap) => {
162
- const data = snap.data();
163
- if (!data) return;
164
- words = data.words || {};
165
- isLocked = data.locked || false;
166
- allowMultiple = data.allowMultiple || false;
167
- document.getElementById('question-preview').textContent = data.question ? '📌 目前題目:' + data.question : '';
168
- document.getElementById('word-count').textContent = Object.keys(words).length;
169
- document.getElementById('btn-lock').textContent = isLocked ? '🔓 解除鎖定' : '🔒 鎖定房間';
170
- document.getElementById('btn-multiple').textContent = '多重送出:' + (allowMultiple ? '開' : '關');
171
- queueDraw();
 
 
 
 
 
 
 
 
 
 
 
172
  });
173
 
174
- function queueDraw() {
175
- if (isDrawing) { drawQueue = true; return; }
176
- drawCloud();
177
- }
178
 
179
  function drawCloud() {
180
- canvas.width = container.clientWidth;
181
  canvas.height = container.clientHeight;
182
- const list = Object.entries(words).map(([word, count]) => [word, count]);
183
 
184
  if (list.length === 0) {
185
  const ctx = canvas.getContext('2d');
@@ -193,12 +231,13 @@ function drawCloud() {
193
  isDrawing = true;
194
  import('https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.0.2/wordcloud2.min.js')
195
  .then(() => WordCloud(canvas, {
196
- list, gridSize: 8,
 
197
  weightFactor: (w) => Math.max(1, Math.min(40, Math.pow(w, 1.3) * 12)),
198
  fontFamily: 'Noto Sans TC, Microsoft JhengHei, sans-serif',
199
  color: () => ['#F97316','#3B82F6','#10B981','#8B5CF6','#EF4444','#F59E0B','#06B6D4'][Math.floor(Math.random()*7)],
200
- rotateRatio: 0.2, minSize: 0, shape: 'circle', ellipticity: 0.7,
201
- backgroundColor: '#ffffff'
202
  }));
203
  }
204
 
@@ -209,97 +248,118 @@ canvas.addEventListener('wordcloudstop', function onStop() {
209
  if (drawQueue) { drawQueue = false; setTimeout(drawCloud, 500); }
210
  });
211
 
212
- // 浮水印畫在 canvas(僅示意;真正可點連結在下方 DOM)
213
  function drawWatermark() {
214
  const ctx = canvas.getContext('2d');
215
  const w = canvas.width, h = canvas.height;
216
- const fontSize = Math.max(10, Math.min(15, w/70));
217
  ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
218
- ctx.font = `bold ${fontSize}px 'Noto Sans TC'`;
219
  ctx.fillStyle = 'rgba(100,100,100,0.45)';
220
  ctx.fillText('程式設計者:精華國中 藍星宇', w - 16, h - 14);
221
- ctx.font = `500 ${Math.max(9, fontSize-2)}px 'Noto Sans TC'`;
222
- ctx.fillText('萬物皆數 fb.com/groups/...', w - 16, h - 14 - (fontSize + 6));
223
  }
224
 
225
- // ---- 點擊 canvas → 彈出詞彙選單(避免命中問題) ----
226
  canvas.addEventListener('click', () => {
227
- if (Object.keys(words).length === 0) return;
228
- const listEl = document.getElementById('pick-list');
229
  listEl.innerHTML = '';
230
- Object.keys(words).sort((a,b) => words[b] - words[a]).forEach(word => {
231
  const chip = document.createElement('span');
232
  chip.className = 'word-chip';
233
  chip.textContent = word;
234
  chip.addEventListener('click', () => { hidePick(); showZoom(word); });
235
  listEl.appendChild(chip);
236
  });
237
- document.getElementById('pick-overlay').style.display = 'flex';
238
  });
239
- function hidePick(){ document.getElementById('pick-overlay').style.display = 'none'; }
 
240
 
241
- // ---- 放大顯示 ----
242
  function showZoom(word) {
243
  selectedWord = word;
244
- document.getElementById('zoom-text').textContent = word;
245
- document.getElementById('zoom-overlay').style.display = 'flex';
246
  }
247
- function hideZoom(){ document.getElementById('zoom-overlay').style.display = 'none'; selectedWord = null; }
248
- document.getElementById('zoom-close').addEventListener('click', hideZoom);
249
- document.getElementById('zoom-overlay').addEventListener('click', e => {
250
- if (e.target === document.getElementById('zoom-overlay')) hideZoom();
251
  });
252
- document.getElementById('zoom-delete').addEventListener('click', async () => {
253
  if (!selectedWord) return;
254
  await updateDoc(roomRef, { words: { [selectedWord]: deleteField() } });
255
  hideZoom();
256
  });
257
 
258
- // ---- 控制 ----
259
- document.getElementById('btn-lock').addEventListener('click', async () => {
260
- isLocked = !isLocked;
261
  await updateDoc(roomRef, { locked: isLocked });
262
  });
263
- document.getElementById('btn-multiple').addEventListener('click', async () => {
264
- allowMultiple = !allowMultiple;
265
  await updateDoc(roomRef, { allowMultiple });
266
  });
267
- document.getElementById('q-send').addEventListener('click', async () => {
268
- const text = document.getElementById('q-input').value.trim();
269
  await updateDoc(roomRef, { question: text });
270
  });
271
- document.getElementById('btn-fullscreen').addEventListener('click', () => {
272
  if (!document.fullscreenElement) document.documentElement.requestFullscreen();
273
  else document.exitFullscreen();
274
  });
275
- document.getElementById('btn-screenshot').addEventListener('click', () => {
276
- const temp = document.createElement('canvas');
277
- temp.width = canvas.width; temp.height = canvas.height;
278
- const t = temp.getContext('2d');
279
- t.fillStyle = '#F8FAFC'; t.fillRect(0,0,temp.width,temp.height);
280
- t.drawImage(canvas, 0, 0);
 
 
281
  const a = document.createElement('a');
282
- a.href = temp.toDataURL('image/png');
283
- a.download = `即時互動文字雲_${roomCode}.png`;
284
- a.click();
285
  });
286
- document.getElementById('btn-csv').addEventListener('click', () => {
287
- const rows = [['詞彙','次數'], ...Object.entries(words)];
288
- const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n');
289
  const blob = new Blob(['\uFEFF'+csv], { type:'text/csv;charset=utf-8;' });
290
  const u = URL.createObjectURL(blob);
291
  const a = document.createElement('a'); a.href = u; a.download = `文字雲_${roomCode}.csv`; a.click();
292
  URL.revokeObjectURL(u);
293
  });
294
- document.getElementById('btn-reset').addEventListener('click', async () => {
295
- if (confirm('確定清除所有詞彙並重新開始?')) await updateDoc(roomRef, { words: {} });
 
 
 
296
  });
297
 
298
- // ---- QR Code ----
 
 
 
299
  import('https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js').then(() => {
300
- const joinUrl = `${location.origin}${location.pathname.replace(/[^/]*$/, '')}guest.html?room=${roomCode}`;
301
- document.getElementById('join-link').textContent = joinUrl;
302
- new QRCode(document.getElementById('qr'), { text: joinUrl, width: 90, height: 90 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  });
304
  </script>
305
 
 
21
  .btn:hover { filter: brightness(.94); }
22
  .btn.primary { background: #F97316; color: #fff; }
23
  .btn.danger { background: #EF4444; color: #fff; }
24
+ .qr-box { text-align: right; font-size: 12px; color: #475569; }
25
+
26
+ .toggle-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
27
+ .switch {
28
+ position: relative; display: inline-block;
29
+ width: 44px; height: 24px; margin: 0 6px;
30
+ }
31
+ .switch input { opacity: 0; width: 0; height: 0; }
32
+ .slider {
33
+ position: absolute; cursor: pointer; inset: 0;
34
+ background: #CBD5E1; border-radius: 24px; transition: .3s;
35
+ }
36
+ .slider:before {
37
+ content: ""; position: absolute; height: 18px; width: 18px;
38
+ left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: .3s;
39
+ }
40
+ .switch input:checked + .slider { background: #F97316; }
41
+ .switch input:checked + .slider:before { transform: translateX(20px); }
42
 
43
  #question-bar {
44
  display: flex; align-items: center; gap: 12px;
 
52
 
53
  #cloud-container {
54
  position: relative; width: calc(100% - 32px);
55
+ height: calc(100vh - 320px); min-height: 420px;
56
  margin: 16px; background: #fff; border-radius: 16px; overflow: hidden;
57
  box-shadow: 0 4px 20px rgba(0,0,0,.06);
58
  }
59
  #word-canvas { width: 100%; height: 100%; display: block; cursor: pointer; }
 
60
  #stats { padding: 0 22px 10px; font-size: 14px; color: #475569; }
61
  #stats b { color: #F97316; font-size: 18px; }
62
 
 
63
  .overlay {
64
  display: none; position: fixed; inset: 0; z-index: 1000;
65
  background: rgba(15,23,42,.55); align-items: center; justify-content: center;
 
69
  max-width: 560px; width: 90%; max-height: 72vh; overflow-y: auto;
70
  box-shadow: 0 20px 60px rgba(0,0,0,.3);
71
  }
72
+ .panel h3 { margin-bottom: 16px; }
73
  .word-chip {
74
  display: inline-block; margin: 5px; padding: 8px 14px; font-size: 15px;
75
  background: #FEF3C7; border: 1px solid #FCD34D; border-radius: 20px; cursor: pointer;
76
  }
77
  .word-chip:hover { background: #FDE68A; }
78
  .panel-close { margin-top: 16px; text-align: right; }
79
+ #zoom-text { font-size: 60px; font-weight: 900; color: #1E293B; word-break: break-word; text-align: center; }
 
 
 
80
  #zoom-actions { margin-top: 28px; display: flex; gap: 12px; justify-content: center; }
81
 
 
82
  #credit-bar {
83
  position: fixed; left: 14px; bottom: 12px; z-index: 999;
84
  background: rgba(255,255,255,.9); padding: 6px 14px; border-radius: 8px;
 
86
  }
87
  #credit-bar a { color: #4361a8; text-decoration: none; font-weight: 600; }
88
  #credit-bar a:hover { text-decoration: underline; }
89
+
90
+ #loading { position: fixed; inset: 0; background: #F1F5F9; display: flex; align-items: center; justify-content: center; font-size: 18px; z-index: 2000; }
91
  </style>
92
  </head>
93
  <body>
94
 
95
+ <div id="loading">載入中…</div>
96
+
97
  <header>
98
  <h1>☁️ 即時互動文字雲</h1>
99
+ <span class="badge">房號:<span id="roomCodeDisplay">----</span></span>
100
+
101
+ <div class="toggle-row">
102
+ <label>🔒 鎖定房間
103
+ <span class="switch"><input type="checkbox" id="lockToggle"><span class="slider"></span></span>
104
+ </label>
105
+ <label>多次送出
106
+ <span class="switch"><input type="checkbox" id="toggleSwitch"><span class="slider"></span></span>
107
+ </label>
108
+ </div>
109
+
110
+ <button class="btn" id="btnFullScreen">⛶ 全螢幕</button>
111
+ <button class="btn" id="btnScreenshot">📸 截圖</button>
112
+ <button class="btn" id="btnCSV">📄 匯出 CSV</button>
113
+ <button class="btn danger" id="btnReset">🗑 清除重來</button>
114
+
115
+ <div class="qr-box">
116
+ <div id="qrcode"></div>
117
+ <a id="joinLink" target="_blank" rel="noopener" style="display:block;word-break:break-all;margin-top:4px;"></a>
118
+ </div>
119
  </header>
120
 
121
  <div id="question-bar">
122
+ <input id="questionInput" type="text" placeholder="輸入討論題目,學生端將即時同步顯示…">
123
+ <button class="btn primary" id="btnQuestion">送出題目</button>
124
+ <span id="questionPreview"></span>
125
  </div>
126
 
127
+ <div id="stats">總詞彙數量:<b id="wordCount">0</b></div>
128
 
129
  <div id="cloud-container"><canvas id="word-canvas"></canvas></div>
130
 
131
+ <div class="overlay" id="pickOverlay">
 
132
  <div class="panel">
133
  <h3>選擇要放大的詞彙:</h3>
134
+ <div id="pickList"></div>
135
+ <div class="panel-close"><button class="btn" id="pickClose">取消</button></div>
136
  </div>
137
  </div>
138
 
139
+ <div class="overlay" id="zoomOverlay">
140
+ <div class="panel" id="zoomCard">
141
+ <div id="zoomText"></div>
142
+ <div id="zoomActions">
143
+ <button class="btn danger" id="zoomDelete">🗑 刪除此詞</button>
144
+ <button class="btn" id="zoomClose">關閉</button>
 
145
  </div>
146
  </div>
147
  </div>
148
 
 
149
  <div id="credit-bar">
150
  程式設計者:精華國中 藍星宇 ·
151
  <a href="https://www.facebook.com/groups/1554372228718393" target="_blank" rel="noopener">臉書社團:萬物皆數</a>
 
153
 
154
  <script type="module">
155
  import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js';
156
+ import { getFirestore, doc, setDoc, onSnapshot, updateDoc, deleteField } from 'https://www.gstatic.com/firebasejs/10.12.2/firebase-firestore.js';
157
 
158
  const firebaseConfig = {
159
  apiKey: "AIzaSyAcIQYst9gFRyE2nCqCwNgCL3LG2VkBXbM",
 
165
  };
166
 
167
  const app = initializeApp(firebaseConfig);
168
+ const db = getFirestore(app);
169
 
170
+ // State
171
+ const roomCode = Math.floor(1000 + Math.random() * 9000).toString();
172
  let allowMultiple = false;
173
+ let isLocked = false;
174
+ let wordData = {};
175
+ let isDrawing = false;
176
+ let drawQueue = false;
177
  let selectedWord = null;
178
 
179
+ // Elements
180
+ const canvas = document.getElementById('word-canvas');
181
  const container = document.getElementById('cloud-container');
182
+ const roomRef = doc(db, 'rooms', roomCode);
183
+
184
+ // 依參考檔保留 clientUrl 變數供 QRCode 使用
185
+ const clientUrl = `${location.origin}${location.pathname.replace(/[^/]*$/, '')}guest.html?room=${roomCode}`;
186
 
187
  await setDoc(roomRef, { roomCode, locked: false, allowMultiple: false, words: {}, question: '' }, { merge: true });
188
+ document.getElementById('roomCodeDisplay').textContent = roomCode;
189
+
190
+ // 監聽即時資料
191
+ onSnapshot(roomRef, (docSnap) => {
192
+ if (!docSnap.exists()) return;
193
+ const data = docSnap.data();
194
+
195
+ if (data.allowMultiple !== allowMultiple) {
196
+ allowMultiple = data.allowMultiple;
197
+ document.getElementById('toggleSwitch').checked = !!allowMultiple;
198
+ }
199
+ if (data.locked !== isLocked) {
200
+ isLocked = !!data.locked;
201
+ document.getElementById('lockToggle').checked = isLocked;
202
+ }
203
+
204
+ document.getElementById('questionPreview').textContent = data.question ? '📌 目前題目:' + data.question : '';
205
+ document.getElementById('wordCount').textContent = Object.keys(data.words || {}).length;
206
+
207
+ const newWords = data.words || {};
208
+ if (JSON.stringify(newWords) !== JSON.stringify(wordData)) {
209
+ wordData = newWords;
210
+ queueDraw();
211
+ }
212
  });
213
 
214
+ // ------------- 文字雲繪製 -------------
215
+ function queueDraw() { if (isDrawing) { drawQueue = true; return; } drawCloud(); }
 
 
216
 
217
  function drawCloud() {
218
+ canvas.width = container.clientWidth;
219
  canvas.height = container.clientHeight;
220
+ const list = Object.entries(wordData).map(([w, c]) => [w, c]);
221
 
222
  if (list.length === 0) {
223
  const ctx = canvas.getContext('2d');
 
231
  isDrawing = true;
232
  import('https://cdnjs.cloudflare.com/ajax/libs/wordcloud2.js/1.0.2/wordcloud2.min.js')
233
  .then(() => WordCloud(canvas, {
234
+ list,
235
+ gridSize: 8,
236
  weightFactor: (w) => Math.max(1, Math.min(40, Math.pow(w, 1.3) * 12)),
237
  fontFamily: 'Noto Sans TC, Microsoft JhengHei, sans-serif',
238
  color: () => ['#F97316','#3B82F6','#10B981','#8B5CF6','#EF4444','#F59E0B','#06B6D4'][Math.floor(Math.random()*7)],
239
+ rotateRatio: 0.2, minSize: 0, weightMode: 'size',
240
+ shape: 'circle', ellipticity: 0.7, backgroundColor: '#ffffff'
241
  }));
242
  }
243
 
 
248
  if (drawQueue) { drawQueue = false; setTimeout(drawCloud, 500); }
249
  });
250
 
 
251
  function drawWatermark() {
252
  const ctx = canvas.getContext('2d');
253
  const w = canvas.width, h = canvas.height;
254
+ const fs = Math.max(10, Math.min(15, w/70));
255
  ctx.textAlign = 'right'; ctx.textBaseline = 'bottom';
256
+ ctx.font = `bold ${fs}px 'Noto Sans TC'`;
257
  ctx.fillStyle = 'rgba(100,100,100,0.45)';
258
  ctx.fillText('程式設計者:精華國中 藍星宇', w - 16, h - 14);
259
+ ctx.font = `500 ${Math.max(9, fs-2)}px 'Noto Sans TC'`;
260
+ ctx.fillText('萬物皆數 (fb.com/groups/1554372228718393)', w - 16, h - 14 - (fs + 6));
261
  }
262
 
263
+ // ------------- 點擊 canvas → 彈出詞彙選單 -------------
264
  canvas.addEventListener('click', () => {
265
+ if (Object.keys(wordData).length === 0) return;
266
+ const listEl = document.getElementById('pickList');
267
  listEl.innerHTML = '';
268
+ Object.keys(wordData).sort((a,b) => wordData[b] - wordData[a]).forEach(word => {
269
  const chip = document.createElement('span');
270
  chip.className = 'word-chip';
271
  chip.textContent = word;
272
  chip.addEventListener('click', () => { hidePick(); showZoom(word); });
273
  listEl.appendChild(chip);
274
  });
275
+ document.getElementById('pickOverlay').style.display = 'flex';
276
  });
277
+ function hidePick(){ document.getElementById('pickOverlay').style.display = 'none'; }
278
+ document.getElementById('pickClose').addEventListener('click', hidePick);
279
 
280
+ // ------------- 放大顯示 -------------
281
  function showZoom(word) {
282
  selectedWord = word;
283
+ document.getElementById('zoomText').textContent = word;
284
+ document.getElementById('zoomOverlay').style.display = 'flex';
285
  }
286
+ function hideZoom(){ document.getElementById('zoomOverlay').style.display = 'none'; selectedWord = null; }
287
+ document.getElementById('zoomClose').addEventListener('click', hideZoom);
288
+ document.getElementById('zoomOverlay').addEventListener('click', e => {
289
+ if (e.target === document.getElementById('zoomOverlay')) hideZoom();
290
  });
291
+ document.getElementById('zoomDelete').addEventListener('click', async () => {
292
  if (!selectedWord) return;
293
  await updateDoc(roomRef, { words: { [selectedWord]: deleteField() } });
294
  hideZoom();
295
  });
296
 
297
+ // ------------- 控制按鈕 -------------
298
+ document.getElementById('lockToggle').addEventListener('change', async () => {
299
+ isLocked = document.getElementById('lockToggle').checked;
300
  await updateDoc(roomRef, { locked: isLocked });
301
  });
302
+ document.getElementById('toggleSwitch').addEventListener('change', async () => {
303
+ allowMultiple = document.getElementById('toggleSwitch').checked;
304
  await updateDoc(roomRef, { allowMultiple });
305
  });
306
+ document.getElementById('btnQuestion').addEventListener('click', async () => {
307
+ const text = document.getElementById('questionInput').value.trim();
308
  await updateDoc(roomRef, { question: text });
309
  });
310
+ document.getElementById('btnFullScreen').addEventListener('click', () => {
311
  if (!document.fullscreenElement) document.documentElement.requestFullscreen();
312
  else document.exitFullscreen();
313
  });
314
+ document.getElementById('btnScreenshot').addEventListener('click', () => {
315
+ const ctx = canvas.getContext('2d');
316
+ const tempCanvas = document.createElement('canvas');
317
+ tempCanvas.width = canvas.width; tempCanvas.height = canvas.height;
318
+ const tCtx = tempCanvas.getContext('2d');
319
+ tCtx.fillStyle = '#F8FAFC'; tCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
320
+ tCtx.drawImage(canvas, 0, 0);
321
+ const dataUrl = tempCanvas.toDataURL("image/png");
322
  const a = document.createElement('a');
323
+ a.href = dataUrl; a.download = `即時互動文字雲_${roomCode}.png`; a.click();
 
 
324
  });
325
+ document.getElementById('btnCSV').addEventListener('click', () => {
326
+ const rows = [['詞彙','次數'], ...Object.entries(wordData)];
327
+ const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g,'""')}"`).join(',')).join('\n');
328
  const blob = new Blob(['\uFEFF'+csv], { type:'text/csv;charset=utf-8;' });
329
  const u = URL.createObjectURL(blob);
330
  const a = document.createElement('a'); a.href = u; a.download = `文字雲_${roomCode}.csv`; a.click();
331
  URL.revokeObjectURL(u);
332
  });
333
+ document.getElementById('btnReset').addEventListener('click', async () => {
334
+ if (confirm('確定要清除畫面上所有的文字,重新開始嗎?')) {
335
+ try { await updateDoc(roomRef, { words: {} }); }
336
+ catch (e) { console.error("Failed to reset:", e); }
337
+ }
338
  });
339
 
340
+ // ------------- QR Code(圖片本身就是 guest 頁面超連結) -------------
341
+ document.getElementById('joinLink').href = clientUrl;
342
+ document.getElementById('joinLink').textContent = '點我開啟學員端 › ' + clientUrl;
343
+
344
  import('https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js').then(() => {
345
+ new QRCode(document.getElementById('qrcode'), {
346
+ text: clientUrl,
347
+ width: 180, height: 180,
348
+ colorDark: "#1E293B", colorLight: "#F8FAFC",
349
+ correctLevel: QRCode.CorrectLevel.H
350
+ });
351
+ // 把 QRCode 圖片包進超連結,點圖片即可開啟 guest 頁面
352
+ const qrCanvas = document.getElementById('qrcode').querySelector('canvas');
353
+ if (qrCanvas) {
354
+ const a = document.createElement('a');
355
+ a.href = clientUrl; a.target = '_blank'; a.rel = 'noopener';
356
+ qrCanvas.parentNode.insertBefore(a, qrCanvas);
357
+ a.appendChild(qrCanvas);
358
+ }
359
+ // 隱藏載入遮罩
360
+ const loading = document.getElementById('loading');
361
+ loading.style.opacity = '0';
362
+ setTimeout(() => loading.style.display = 'none', 500);
363
  });
364
  </script>
365