|
71 | 71 | site: { value: '', is: true }, |
72 | 72 | content: { value: '', is: false } |
73 | 73 | } |
74 | | - $: contentHTML = '' |
75 | 74 |
|
76 | | - onMount(async () => { |
| 75 | + onMount(() => { |
77 | 76 | initInfo() |
78 | | - await getEmot() |
| 77 | + getEmot() |
79 | 78 | }) |
80 | 79 |
|
81 | 80 | afterUpdate(() => { |
|
103 | 102 | } else { |
104 | 103 | emotMaps = emot |
105 | 104 | } |
| 105 | + getEmotAll() |
106 | 106 | } |
107 | 107 |
|
108 | 108 | function getEmotAll() { |
|
118 | 118 | console.log(error) |
119 | 119 | } |
120 | 120 | } |
121 | | - function ParseEmot() { |
122 | | - getEmotAll() |
123 | | - let content = metas.content.value |
124 | | - const emots = [] |
125 | | - content.replace(/\[(.*?)\]/g, ($0, $1) => { |
126 | | - emots.push($1) |
127 | | - }) |
128 | | -
|
129 | | - for (const emot of emots) { |
130 | | - const link = emotAll[emot] |
131 | | - if (!link) continue |
132 | | - const img = `<img class='D-comment-emot' src='${link}' alt='${emot}'/>` |
133 | | - content = content.replace(`[${emot}]`, img) |
134 | | - } |
135 | | - contentHTML = content |
136 | | - } |
137 | 121 |
|
138 | 122 | function SaveInfo() { |
139 | 123 | for (const [k, v] of Object.entries(metas)) { |
|
142 | 126 | localStorage.discuss = JSON.stringify(storage) |
143 | 127 | } |
144 | 128 |
|
145 | | - $: isOnPreview = metas.content.value.length |
146 | | - let isPreview = false |
147 | | - function Preview() { |
148 | | - if (!isPreview) return |
149 | | - ParseEmot() |
150 | | - } |
151 | | - function onPreview() { |
152 | | - isPreview = !isPreview |
153 | | - Preview() |
154 | | - } |
155 | | -
|
156 | 129 | function onInput() { |
157 | 130 | SaveInfo() |
158 | | - Preview() |
159 | 131 | MetasChange() |
160 | 132 | } |
| 133 | +
|
| 134 | + let lastEditRange |
| 135 | + function getCursor() { |
| 136 | + const sel = window.getSelection() |
| 137 | + if (sel.rangeCount < 0) { |
| 138 | + lastEditRange = document.createRange() |
| 139 | + return |
| 140 | + } |
| 141 | +
|
| 142 | + lastEditRange = sel.getRangeAt(0) |
| 143 | + } |
| 144 | +
|
161 | 145 | /** |
162 | 146 | * @param {String} key 表情名(描述) |
163 | 147 | * @param {String} value 表情值(内容或地址) |
164 | 148 | * @param {String} type 表情类型(text or image) |
165 | 149 | */ |
166 | 150 | let textareaDOM |
| 151 | + // eslint-disable-next-line max-statements |
167 | 152 | function onClickEmot(key, value, type) { |
168 | | - const cObj = metas.content |
169 | | - let content = cObj.value |
| 153 | + textareaDOM.focus() |
| 154 | + const sel = window.getSelection() |
| 155 | + if (lastEditRange) { |
| 156 | + // 清除所有光标并添加最后光标编辑的状态 |
| 157 | + sel.removeAllRanges() |
| 158 | + sel.addRange(lastEditRange) |
| 159 | + } |
170 | 160 |
|
171 | | - // 获取输入框光标位置 |
172 | | - let cursorStart = textareaDOM.selectionStart |
173 | | - let cursorEnd = textareaDOM.selectionEnd |
174 | | - const Start = content.substring(0, cursorStart) |
175 | | - const Ent = content.substring(cursorEnd) |
| 161 | + let emojiEl |
176 | 162 |
|
177 | | - if (type === textStr) cObj.value = `${Start}${value}${Ent}` |
178 | | - else cObj.value = `${Start}[${key}]${Ent}` |
| 163 | + if (type === textStr) { |
| 164 | + emojiEl = document.createTextNode(value) |
| 165 | + } else { |
| 166 | + emojiEl = document.createElement('img') |
| 167 | + emojiEl.src = emotAll[key] |
| 168 | + emojiEl.className = 'D-comment-emot' |
| 169 | + emojiEl.alt = key |
| 170 | + } |
179 | 171 |
|
180 | | - textareaDOM.focus() |
181 | | - const contentLen = content.length |
182 | | - cursorStart = contentLen |
183 | | - cursorEnd = contentLen |
184 | | - // 重新解析表情 |
185 | | - ParseEmot() |
186 | | - // 重新保存 |
| 172 | + // 不存在光标,则获取光标,并将光标移动至最后 |
| 173 | + if (!lastEditRange) { |
| 174 | + getCursor() |
| 175 | + lastEditRange.selectNodeContents(textareaDOM) |
| 176 | + lastEditRange.collapse(false) |
| 177 | + sel.removeAllRanges() |
| 178 | + sel.addRange(lastEditRange) |
| 179 | + } |
| 180 | +
|
| 181 | + // 如果光标没有重叠,则代表光标选择了一部分内容,需要删除光标再插入表情 |
| 182 | + if (!lastEditRange.collapsed) lastEditRange.deleteContents() |
| 183 | +
|
| 184 | + lastEditRange.insertNode(emojiEl) |
| 185 | +
|
| 186 | + // 让光标保持在插入表情的后面, true 表示保持在前面 |
| 187 | + lastEditRange.collapse(false) |
| 188 | +
|
| 189 | + metas.content.value = textareaDOM.innerHTML |
| 190 | + // 保存 |
187 | 191 | SaveInfo() |
188 | | - // 由于这是Svelte的特性,引用类型需要重新给自身赋值才会触发双向绑定 |
189 | | - metas = metas |
190 | 192 | } |
191 | 193 |
|
192 | 194 | function MetasChange() { |
|
217 | 219 | async function onSend() { |
218 | 220 | try { |
219 | 221 | if (!isSend && !isLegal) return |
220 | | - ParseEmot() |
221 | 222 | const comment = { |
222 | 223 | type: 'COMMIT_COMMENT', |
223 | 224 | nick: metas.nick.value, |
224 | 225 | mail: metas.mail.value, |
225 | 226 | site: metas.site.value, |
226 | | - content: contentHTML, |
| 227 | + content: metas.content.value, |
227 | 228 | path: D.path, |
228 | 229 | pid, |
229 | 230 | rid |
|
247 | 248 | dispatch('submitComment', { comment: result.data, pid }) |
248 | 249 | metas.content.value = '' |
249 | 250 | SaveInfo() |
250 | | - isPreview = false |
251 | 251 | } |
252 | 252 | } catch (error) { |
253 | 253 | // eslint-disable-next-line no-console |
|
271 | 271 | on:input={onInput} |
272 | 272 | /> |
273 | 273 | {/each} |
274 | | - <textarea |
| 274 | + <div |
275 | 275 | name={contentStr} |
276 | 276 | class="D-input-content {metas.content.is ? '' : 'D-error'}" |
277 | | - bind:value={metas.content.value} |
278 | 277 | placeholder={D.ph} |
279 | | - on:input={onInput} |
| 278 | + on:input={function () { |
| 279 | + metas.content.value = this.innerHTML |
| 280 | + onInput() |
| 281 | + }} |
| 282 | + on:click={getCursor} |
| 283 | + on:keyup={getCursor} |
280 | 284 | bind:this={textareaDOM} |
| 285 | + bind:innerHTML={metas.content.value} |
| 286 | + contenteditable |
281 | 287 | /> |
| 288 | + |
282 | 289 | {#if wordLimitContent} |
283 | 290 | <span class="D-text-number"> |
284 | 291 | {metas.content.value.length} |
|
309 | 316 | > |
310 | 317 | {/if} |
311 | 318 |
|
312 | | - <button on:click={onPreview} class="D-cancel D-btn D-btn-main {!isOnPreview && 'D-disabled'}" |
313 | | - >{translate('preview')}</button |
314 | | - ><button class="D-send D-btn D-btn-main" on:click={onSend} disabled={isSend || !isLegal}> |
| 319 | + <button class="D-send D-btn D-btn-main" on:click={onSend} disabled={isSend || !isLegal}> |
315 | 320 | {#if isSend && isLegal} |
316 | 321 | <Loading /> |
317 | 322 | {:else} |
|
320 | 325 | </button> |
321 | 326 | </div> |
322 | 327 | </div> |
323 | | - {#if isPreview} |
324 | | - <div class="D-preview">{@html contentHTML}</div> |
325 | | - {/if} |
326 | 328 | {#if isEmot} |
327 | 329 | <div class="D-emot"> |
328 | 330 | {#each Object.entries(emotMaps) as [emotKey, emotValue], index} |
|
332 | 334 | {#if emotValue.type === 'text'} |
333 | 335 | <span title={iKey}>{iValue}</span> |
334 | 336 | {:else} |
335 | | - <img src={D.imgLoading} d-src={iValue} alt={iKey} title={iKey} /> |
| 337 | + <img class="D-comment-emot" src={D.imgLoading} d-src={iValue} alt={iKey} title={iKey} /> |
336 | 338 | {/if} |
337 | 339 | </li> |
338 | 340 | {/each} |
|
395 | 397 | transition: all 0.5s; |
396 | 398 | } |
397 | 399 |
|
| 400 | + .D-input-content:empty::before { |
| 401 | + content: attr(placeholder); |
| 402 | + color: #666; |
| 403 | + } |
398 | 404 | .D-input-content { |
399 | 405 | margin: 10px 0 0; |
| 406 | + padding: 6px; |
400 | 407 | resize: vertical; |
401 | 408 | width: 100%; |
402 | 409 | min-height: 140px; |
403 | 410 | max-height: 400px; |
404 | 411 | outline: none; |
405 | 412 | font-family: inherit; |
406 | 413 | transition: none; |
| 414 | + overflow-y: auto; |
| 415 | + letter-spacing: 1px; |
407 | 416 | } |
408 | 417 |
|
409 | 418 | .D-text-number { |
|
500 | 509 | cursor: pointer; |
501 | 510 | transition: 0.3s; |
502 | 511 |
|
503 | | - img { |
504 | | - width: 32px; |
505 | | - height: auto; |
506 | | - } |
507 | | -
|
508 | 512 | &:hover { |
509 | 513 | background: var(--D-Low-Color); |
510 | 514 | box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 20%), 0 1px 5px 0 rgb(0 0 0 / 12%); |
|
539 | 543 | background: var(--D-Low-Color); |
540 | 544 | } |
541 | 545 |
|
542 | | - /* preview */ |
543 | | -
|
544 | | - .D-preview { |
545 | | - padding: 10px; |
546 | | - overflow-x: auto; |
547 | | - min-height: 1.375rem /* 22/16 */; |
548 | | - margin: 10px 0; |
549 | | - border: 1px solid #dcdfe6; |
550 | | - border-radius: 4px; |
551 | | - } |
552 | | -
|
553 | 546 | @media screen and (max-width: 500px) { |
554 | 547 | .D-input { |
555 | 548 | display: flex; |
|
0 commit comments