|
1 | 1 | <script> |
2 | | - import { onMount, afterUpdate, createEventDispatcher } from 'svelte' |
| 2 | + import { onMount, afterUpdate, createEventDispatcher, tick } from 'svelte' |
3 | 3 | import { options, msg, lazy } from '../lib/stores' |
4 | 4 | import request from '../lib/request' |
5 | 5 | import emotFn from '../lib/emot' |
|
46 | 46 | let emotIndex = 0 |
47 | 47 | let emotMaps = {} |
48 | 48 | let emotAll = {} |
| 49 | + let textareaDOM |
| 50 | + let isPreview = false |
49 | 51 | let isSend = false |
50 | 52 | let isLegal = false |
51 | 53 | const inputs = [ |
|
71 | 73 | site: { value: '', is: true }, |
72 | 74 | content: { value: '', is: false } |
73 | 75 | } |
74 | | -
|
| 76 | + let contentHTML = '' |
75 | 77 | let wordLimitContent = wordLimit.content |
76 | 78 | let limitContentLen |
77 | 79 | $: { |
78 | 80 | wordLimitContent = wordLimit.content |
79 | | - const dom = new DOMParser().parseFromString(metas.content.value, 'text/html') |
| 81 | + const dom = new DOMParser().parseFromString(contentHTML, 'text/html') |
80 | 82 | limitContentLen = dom.body.textContent.length + dom.body.querySelectorAll('img').length |
81 | 83 | } |
82 | 84 |
|
83 | 85 | onMount(() => { |
84 | 86 | initInfo() |
85 | 87 | getEmot() |
| 88 | + onInput() |
86 | 89 | }) |
87 | 90 |
|
88 | 91 | afterUpdate(() => { |
89 | | - MetasChange() |
| 92 | + metasChange() |
90 | 93 | $lazy() |
91 | 94 | }) |
92 | 95 |
|
|
126 | 129 | console.log(error) |
127 | 130 | } |
128 | 131 | } |
| 132 | + function parseEmot() { |
| 133 | + let content = metas.content.value |
| 134 | + const emots = [] |
| 135 | + content.replace(/\[(.*?)\]/g, ($0, $1) => { |
| 136 | + emots.push($1) |
| 137 | + }) |
| 138 | +
|
| 139 | + for (const emot of emots) { |
| 140 | + const link = emotAll[emot] |
| 141 | + if (!link) continue |
| 142 | + const img = `<img class='D-comment-emot' src='${link}' alt='${emot}'/>` |
| 143 | + content = content.replace(`[${emot}]`, img) |
| 144 | + } |
| 145 | + contentHTML = content |
| 146 | + } |
129 | 147 |
|
130 | | - function SaveInfo() { |
| 148 | + function saveInfo() { |
131 | 149 | for (const [k, v] of Object.entries(metas)) { |
132 | 150 | storage[k] = v.value.trim() |
133 | 151 | } |
134 | 152 | localStorage.discuss = JSON.stringify(storage) |
| 153 | + // 重新解析表情 |
| 154 | + parseEmot() |
135 | 155 | } |
136 | 156 |
|
137 | | - function onInput() { |
138 | | - SaveInfo() |
139 | | - MetasChange() |
| 157 | + function onPreview() { |
| 158 | + isPreview = !isPreview |
140 | 159 | } |
141 | 160 |
|
142 | | - let lastEditRange |
143 | | - function getCursor() { |
144 | | - const sel = window.getSelection() |
145 | | - if (sel.rangeCount < 0) { |
146 | | - lastEditRange = document.createRange() |
147 | | - return |
148 | | - } |
149 | | -
|
150 | | - lastEditRange = sel.getRangeAt(0) |
| 161 | + function onInput() { |
| 162 | + saveInfo() |
| 163 | + metasChange() |
151 | 164 | } |
152 | | -
|
153 | 165 | /** |
154 | 166 | * @param {String} key 表情名(描述) |
155 | 167 | * @param {String} value 表情值(内容或地址) |
156 | 168 | * @param {String} type 表情类型(text or image) |
157 | 169 | */ |
158 | | - let textareaDOM |
159 | | - // eslint-disable-next-line max-statements |
160 | 170 | function onClickEmot(key, value, type) { |
161 | | - textareaDOM.focus() |
162 | | - const sel = window.getSelection() |
163 | | - if (lastEditRange) { |
164 | | - // 清除所有光标并添加最后光标编辑的状态 |
165 | | - sel.removeAllRanges() |
166 | | - sel.addRange(lastEditRange) |
167 | | - } |
| 171 | + const content = metas.content.value |
168 | 172 |
|
169 | | - let emojiEl |
| 173 | + // 获取输入框光标位置 |
| 174 | + let cursorStart = textareaDOM.selectionStart |
| 175 | + let cursorEnd = textareaDOM.selectionEnd |
| 176 | + const Start = content.substring(0, cursorStart) |
| 177 | + const Ent = content.substring(cursorEnd) |
170 | 178 |
|
| 179 | + let range |
| 180 | + textareaDOM.focus() |
171 | 181 | if (type === textStr) { |
172 | | - emojiEl = document.createTextNode(value) |
| 182 | + metas.content.value = `${Start}${value}${Ent}` |
| 183 | + range = (Start + value).length |
173 | 184 | } else { |
174 | | - emojiEl = document.createElement('img') |
175 | | - emojiEl.src = emotAll[key] |
176 | | - emojiEl.className = 'D-comment-emot' |
177 | | - emojiEl.alt = key |
| 185 | + metas.content.value = `${Start}[${key}]${Ent}` |
| 186 | + range = (Start + key).length + 2 |
178 | 187 | } |
| 188 | + // 重新保存 |
| 189 | + saveInfo() |
179 | 190 |
|
180 | | - // 不存在光标,则获取光标,并将光标移动至最后 |
181 | | - if (!lastEditRange) { |
182 | | - getCursor() |
183 | | - lastEditRange.selectNodeContents(textareaDOM) |
184 | | - lastEditRange.collapse(false) |
185 | | - sel.removeAllRanges() |
186 | | - sel.addRange(lastEditRange) |
187 | | - } |
188 | | -
|
189 | | - // 如果光标没有重叠,则代表光标选择了一部分内容,需要删除光标再插入表情 |
190 | | - if (!lastEditRange.collapsed) lastEditRange.deleteContents() |
191 | | -
|
192 | | - lastEditRange.insertNode(emojiEl) |
193 | | -
|
194 | | - // 让光标保持在插入表情的后面, true 表示保持在前面 |
195 | | - lastEditRange.collapse(false) |
196 | | -
|
197 | | - metas.content.value = textareaDOM.innerHTML |
198 | | - // 保存 |
199 | | - SaveInfo() |
| 191 | + tick().then(() => { |
| 192 | + textareaDOM.setSelectionRange(range, range) |
| 193 | + }) |
200 | 194 | } |
201 | 195 |
|
202 | 196 | // eslint-disable-next-line max-statements |
203 | | - function MetasChange() { |
| 197 | + function metasChange() { |
204 | 198 | try { |
205 | 199 | const { nick, mail, site, content } = metas |
206 | 200 | const { nick: nickWord, mail: mailWord, site: siteWord, content: contentWord } = wordLimit |
|
224 | 218 | } |
225 | 219 |
|
226 | 220 | // 网站 |
227 | | - if (siteLen === 0 || siteLen <= siteWord && isUrl(site.value)) { |
| 221 | + if (siteLen === 0 || (siteLen <= siteWord && isUrl(site.value))) { |
228 | 222 | metas.site.is = true |
229 | 223 | } else if (siteLen !== 0) { |
230 | 224 | metas.site.is = false |
231 | 225 | } |
232 | 226 |
|
233 | 227 | // 内容 |
234 | | - const dom = new DOMParser().parseFromString(content.value, 'text/html') |
| 228 | + const dom = new DOMParser().parseFromString(contentHTML, 'text/html') |
235 | 229 | const textContent = dom.body.textContent.length |
236 | 230 | if (contentLen > 1 && textContent <= contentWord) { |
237 | 231 | metas.content.is = true |
|
255 | 249 | nick: metas.nick.value, |
256 | 250 | mail: metas.mail.value, |
257 | 251 | site: metas.site.value, |
258 | | - content: metas.content.value, |
| 252 | + content: contentHTML, |
259 | 253 | path: D.path, |
260 | 254 | pid, |
261 | 255 | rid |
|
278 | 272 |
|
279 | 273 | dispatch('submitComment', { comment: result.data, pid }) |
280 | 274 | metas.content.value = '' |
281 | | - SaveInfo() |
| 275 | + saveInfo() |
| 276 | + isPreview = false |
282 | 277 | } |
283 | 278 | } catch (error) { |
284 | 279 | // eslint-disable-next-line no-console |
|
302 | 297 | on:input={onInput} |
303 | 298 | /> |
304 | 299 | {/each} |
305 | | - <div |
| 300 | + <textarea |
306 | 301 | name={contentStr} |
307 | 302 | class="D-input-content {metas.content.is ? '' : 'D-error'}" |
| 303 | + bind:value={metas.content.value} |
308 | 304 | placeholder={D.ph} |
309 | | - on:input={function () { |
310 | | - metas.content.value = this.innerHTML |
311 | | - onInput() |
312 | | - }} |
313 | | - on:click={getCursor} |
314 | | - on:keyup={getCursor} |
| 305 | + on:input={onInput} |
315 | 306 | bind:this={textareaDOM} |
316 | | - bind:innerHTML={metas.content.value} |
317 | | - contenteditable |
318 | 307 | /> |
319 | | - |
320 | 308 | {#if wordLimitContent} |
321 | 309 | <span class="D-text-number"> |
322 | 310 | {limitContentLen} |
|
345 | 333 | > |
346 | 334 | {/if} |
347 | 335 |
|
348 | | - <button class="D-send D-btn D-btn-main" on:click={onSend} disabled={isSend || !isLegal}> |
| 336 | + <button |
| 337 | + on:click={onPreview} |
| 338 | + class="D-cancel D-btn D-btn-main {/* 没有内容则禁用预览按钮 */ !metas.content.value.length && 'D-disabled'}" |
| 339 | + >{translate('preview')}</button |
| 340 | + ><button class="D-send D-btn D-btn-main" on:click={onSend} disabled={isSend || !isLegal}> |
349 | 341 | {#if isSend && isLegal} |
350 | 342 | <Loading /> |
351 | 343 | {:else} |
|
354 | 346 | </button> |
355 | 347 | </div> |
356 | 348 | </div> |
| 349 | + {#if isPreview} |
| 350 | + <div class="D-preview">{@html contentHTML}</div> |
| 351 | + {/if} |
357 | 352 | {#if isEmot} |
358 | 353 | <div class="D-emot"> |
359 | 354 | {#each Object.entries(emotMaps) as [emotKey, emotValue], index} |
|
426 | 421 | transition: all 0.5s; |
427 | 422 | } |
428 | 423 |
|
429 | | - .D-input-content:empty::before { |
430 | | - content: attr(placeholder); |
431 | | - color: #666; |
432 | | - } |
433 | 424 | .D-input-content { |
434 | 425 | margin: 10px 0 0; |
435 | | - padding: 6px; |
436 | 426 | resize: vertical; |
437 | 427 | width: 100%; |
438 | 428 | min-height: 140px; |
439 | 429 | max-height: 400px; |
440 | 430 | outline: none; |
441 | 431 | font-family: inherit; |
442 | 432 | transition: none; |
443 | | - overflow-y: auto; |
444 | | - letter-spacing: 1px; |
445 | 433 | } |
446 | 434 |
|
447 | 435 | .D-text-number { |
|
572 | 560 | background: var(--D-Low-Color); |
573 | 561 | } |
574 | 562 |
|
| 563 | + /* preview */ |
| 564 | +
|
| 565 | + .D-preview { |
| 566 | + padding: 10px; |
| 567 | + overflow-x: auto; |
| 568 | + min-height: 1.375rem /* 22/16 */; |
| 569 | + margin: 10px 0; |
| 570 | + border: 1px solid #dcdfe6; |
| 571 | + border-radius: 4px; |
| 572 | + } |
| 573 | +
|
575 | 574 | @media screen and (max-width: 500px) { |
576 | 575 | .D-input { |
577 | 576 | display: flex; |
|
0 commit comments