Skip to content

Commit 3f5f4c7

Browse files
committed
feat: use rangeContainer instead of target to replace quotes
1 parent 9b4585c commit 3f5f4c7

File tree

2 files changed

+48
-23
lines changed

2 files changed

+48
-23
lines changed

src/dispatcher.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,11 @@ export default class Dispatcher {
146146
const block = this.getEditableBlockByEvent(evt)
147147
if (!block) return
148148

149-
const target = evt.target
150-
151-
if (shouldApplySmartQuotes(config, target)) {
152-
const currentChar = evt.data
149+
if (shouldApplySmartQuotes(config, evt.target)) {
153150
const selection = this.selectionWatcher.getFreshSelection()
154-
const offset = selection.range.startOffset
155-
const wholeText = [...target.innerText]
156-
const resetCursor = () => this.editable.createCursorAtCharacterOffset({element: block, offset})
157-
applySmartQuotes(config, currentChar, wholeText, offset, target, resetCursor)
151+
applySmartQuotes(selection.range, config, evt.data, evt.target)
158152
}
153+
159154
this.notify('change', block)
160155
})
161156

src/smartQuotes.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,69 @@ export const shouldApplySmartQuotes = (config, target) => {
55
return !!smartQuotes && isValidQuotePairConfig(quotes) && isValidQuotePairConfig(singleQuotes) && target.isContentEditable
66
}
77

8-
// export const isQuote = (char) => /^[‘’‹›‚'«»"“”„]$/.test(char)
98
const isDoubleQuote = (char) => /^[«»"]$/.test(char)
109
const isSingleQuote = (char) => /^[']$/.test(char)
10+
const isApostrophe = (char) => /^[']$/.test(char)
1111

12-
// Test: '>', ' ', no space & all kinds of dashes dash
13-
const isOpeningQuote = (text, indexCharBefore) => indexCharBefore < 0 || /\s|[>\-]/.test(text[indexCharBefore])
14-
const isClosingQuote = (text, indexCharBefore) => text[indexCharBefore] !== ' '
12+
// TODO: Test with: '>', ' ', no space & all kinds of dashes dash
13+
// edge case: applied tooltip quotes and then inserted single quote after space
14+
const shouldBeOpeningQuote = (text, indexCharBefore) => indexCharBefore < 0 || /\s|[>\-]/.test(text[indexCharBefore])
15+
const shouldBeClosingQuote = (text, indexCharBefore) => text[indexCharBefore] && !/\s/.test(text[indexCharBefore])
1516

16-
const applySmartQuote = (textArr, index, target, quoteType) => {
17-
if (index >= 0 && index < textArr.length) {
18-
textArr[index] = quoteType
19-
target.innerText = textArr.join('')
17+
const replaceQuote = (range, index, quoteType) => {
18+
const startContainer = range.startContainer
19+
const textNode = document.createTextNode(`${startContainer.nodeValue.substring(0, index)}${quoteType}${startContainer.nodeValue.substring(index + 1)}`)
20+
range.startContainer.replaceWith(textNode)
21+
return textNode
22+
}
23+
24+
const hasSingleOpeningQuote = (textArr, offset, singleOpeningQuote) => {
25+
if (offset <= 0) {
26+
return false
2027
}
28+
for (let i = offset - 1; i >= 0; i--) {
29+
if (isSingleQuote(textArr[i]) && (!isApostrophe(singleOpeningQuote) && !isApostrophe(textArr[i]))) {
30+
return textArr[i] === singleOpeningQuote
31+
}
32+
}
33+
return false
2134
}
2235

23-
export const applySmartQuotes = (config, char, wholeText, offset, target, resetCursor) => {
36+
export const applySmartQuotes = (range, config, char, target) => {
2437
const isCharSingleQuote = isSingleQuote(char)
2538
const isCharDoubleQuote = isDoubleQuote(char)
2639

2740
if (!isCharDoubleQuote && !isCharSingleQuote) {
2841
return
2942
}
3043

44+
const offset = range.startOffset
45+
const textArr = [...range.startContainer.textContent]
3146
const {quotes, singleQuotes} = config
32-
if (isClosingQuote(wholeText, offset - 2)) {
47+
let newTextNode
48+
49+
if (shouldBeClosingQuote(textArr, offset - 2)) {
50+
// Don't transform single-quote if there is no respective single-opening-quote
51+
if (isCharSingleQuote && !hasSingleOpeningQuote(textArr, offset, singleQuotes[0])) {
52+
return
53+
}
54+
// TODO: Fix ‹Didn’t› case -> only works with timeout
55+
// if (isCharSingleQuote && hasTextAfter(target, offset)) {
56+
// return
57+
// }
3358
const closingQuote = isCharSingleQuote ? singleQuotes[1] : quotes[1]
34-
applySmartQuote(wholeText, offset - 1, target, closingQuote)
59+
newTextNode = replaceQuote(range, offset - 1, closingQuote)
60+
} else if (shouldBeOpeningQuote(textArr, offset - 2)) {
61+
const openingQuote = isCharSingleQuote ? singleQuotes[0] : quotes[0]
62+
newTextNode = replaceQuote(range, offset - 1, openingQuote)
3563
}
3664

37-
if (isOpeningQuote(wholeText, offset - 2)) {
38-
const openingQuote = isCharSingleQuote ? singleQuotes[0] : quotes[0]
39-
applySmartQuote(wholeText, offset - 1, target, openingQuote)
65+
if (!newTextNode) {
66+
return
4067
}
4168

42-
resetCursor()
69+
// Resets the cursor
70+
const window = target.ownerDocument.defaultView
71+
const selection = window.getSelection()
72+
selection.collapse(newTextNode, offset)
4373
}

0 commit comments

Comments
 (0)