Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add non-bip39 word underlining in mnemonic field
- Loading branch information
1 parent
59caab2
commit 3eda000
Showing
9 changed files
with
304 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import {h, Component} from 'preact' | ||
import {connect} from '../../../helpers/connect' | ||
import actions from '../../../actions' | ||
import {underlineNonBip39words} from '../../../helpers/dynamicTextFormatter' | ||
import {getCaretPosition, setCaretPosition} from '../../../../frontend/helpers/caretPosition' | ||
|
||
interface Props { | ||
formData: any | ||
updateMnemonic: (mnemonic) => void | ||
updateMnemonicValidationError: () => void | ||
onEnterKeyDown: (e) => void | ||
onTabKeyDown: (e) => void | ||
expose | ||
} | ||
|
||
class MnemonicField extends Component<Props> { | ||
mnemonicField: HTMLDivElement | ||
lastFormattedMnemonic = '' | ||
lastRawMnemonic = '' | ||
|
||
state = { | ||
focus: 'focus', | ||
} | ||
|
||
constructor(props) { | ||
super() | ||
props.expose.focus = () => this.mnemonicField && this.mnemonicField.focus() | ||
} | ||
|
||
updateMnemonic() { | ||
if (this.lastFormattedMnemonic !== this.mnemonicField.innerHTML) { | ||
const {formattedText, rawText} = underlineNonBip39words(this.mnemonicField.innerHTML) | ||
|
||
const caretPosition = getCaretPosition(this.mnemonicField) | ||
this.mnemonicField.innerHTML = `${formattedText}` | ||
setCaretPosition(this.mnemonicField, caretPosition) | ||
|
||
this.lastFormattedMnemonic = formattedText | ||
this.lastRawMnemonic = rawText | ||
this.props.updateMnemonic(rawText) | ||
} | ||
} | ||
|
||
overwriteMnemonic() { | ||
this.mnemonicField.innerHTML = this.props.formData.mnemonicInputValue | ||
this.updateMnemonic() | ||
setCaretPosition(this.mnemonicField, this.props.formData.mnemonicInputValue.length) | ||
} | ||
|
||
componentDidMount() { | ||
this.overwriteMnemonic() | ||
} | ||
|
||
componentDidUpdate() { | ||
if (this.props.formData.mnemonicInputValue !== this.lastRawMnemonic) { | ||
this.overwriteMnemonic() | ||
} | ||
} | ||
|
||
render({formData, updateMnemonicValidationError, onEnterKeyDown, onTabKeyDown}) { | ||
return ( | ||
<div className={`input fullwidth auth ${this.state.focus}`}> | ||
<div | ||
contentEditable | ||
// eslint-disable-next-line react/no-unknown-property | ||
spellcheck={false} | ||
tabIndex={0} | ||
type="text" | ||
className={`mnemonic-text-field ${ | ||
formData.mnemonicInputValue.length ? '' : 'mnemonic-placeholder' | ||
}`} | ||
onInput={() => this.updateMnemonic()} | ||
onBlur={() => { | ||
this.setState({focus: ''}) | ||
updateMnemonicValidationError() | ||
}} | ||
autoComplete="off" | ||
ref={(element) => { | ||
this.mnemonicField = element | ||
}} | ||
onKeyDown={(e) => { | ||
e.key === 'Enter' && onEnterKeyDown(e) | ||
e.key === 'Tab' && onTabKeyDown(e) | ||
}} | ||
onFocus={() => this.setState({focus: 'focus'})} | ||
/> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default connect( | ||
(state) => ({ | ||
formData: state.mnemonicAuthForm, | ||
}), | ||
actions | ||
)(MnemonicField) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
const nodeWalk = (node, func) => { | ||
if (node) { | ||
let result = func(node) | ||
for (node = node.firstChild; result !== false && node; node = node.nextSibling) { | ||
result = nodeWalk(node, func) | ||
} | ||
return result | ||
} else { | ||
return false | ||
} | ||
} | ||
|
||
const getCaretPosition = (elem) => { | ||
const sel = window.getSelection() | ||
const caretNode = sel.focusNode === elem ? elem.childNodes.item(sel.focusOffset) : sel.focusNode | ||
const offset = sel.focusNode === elem ? 0 : sel.focusOffset | ||
|
||
if (elem.contains(caretNode)) { | ||
let length = 0 | ||
|
||
nodeWalk(elem, (node) => { | ||
if (node === caretNode) { | ||
return false | ||
} else if (node.nodeType === Node.TEXT_NODE && node.textContent) { | ||
length += node.textContent.length | ||
} | ||
return true | ||
}) | ||
|
||
return length + offset | ||
} else { | ||
return elem.textContent.length | ||
} | ||
} | ||
|
||
const setCaretPosition = (elem, position) => { | ||
let targetNode = elem | ||
let length = 0 | ||
|
||
nodeWalk(elem, (node) => { | ||
targetNode = node | ||
|
||
if (node.nodeType === Node.TEXT_NODE && node.textContent) { | ||
if (length + node.textContent.length >= position) { | ||
return false | ||
} else { | ||
length += node.textContent.length | ||
} | ||
} | ||
|
||
return true | ||
}) | ||
|
||
const newRange = document.createRange() | ||
newRange.setStart(targetNode, position - length) | ||
newRange.collapse(true) | ||
|
||
const sel = window.getSelection() | ||
sel.removeAllRanges() | ||
sel.addRange(newRange) | ||
} | ||
|
||
export {getCaretPosition, setCaretPosition} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import {isBip39Word} from '../wallet/mnemonic' | ||
import matchAll from './matchAll' | ||
|
||
const underlineNonBip39words = (toFormat) => { | ||
// 1. forbid multiple divs (lines) | ||
toFormat = toFormat.replace(/<\/{0,1}(div|br|p)[^<>]*>/g, '') | ||
|
||
// 2. only non-breaking space allowed as whitespace | ||
toFormat = toFormat.replace(/\s| /g, ' ') | ||
|
||
// 3. wrap span around words at the start of line | ||
// ^[^<>a-zA-Z]* - find start of line and absence of '<' and '>' | ||
// [a-zA-Z]+ - find word itself | ||
// (\s|[,;])+<span - find next opening tag '<span' | ||
toFormat = toFormat.replace(/(^[^<>a-zA-Z]*)([a-zA-Z]+)((\s|[,;])+<span)/g, '$1<span>$2</span>$3') | ||
|
||
// 4. wrap span around words in the middle of line | ||
// <\/span>(\s|[,;])+ - find '</span>' before word | ||
// [a-zA-Z]+ - find word itself | ||
// (\s|[,;])+<span - find '<span' after word | ||
toFormat = toFormat.replace( | ||
/(<\/span>(\s|[,;])+)([a-zA-Z]+)((\s|[,;])+<span)/g, | ||
'$1<span>$3</span>$4' | ||
) | ||
|
||
// 5. wrap span around words in the end of the line | ||
// (?<=^|\s|[,;]) - check for delimiter before word | ||
// [a-zA-Z]+ - find word itself | ||
// (?=$|\s|[,;]) - check for delimiter after word | ||
// (?=[^<>]*$) - check for absence of '<' or '>' after word to prevent | ||
// wrapping tags inside span, like <span class="a b c"> | ||
toFormat = toFormat.replace(/(?<=^|\s|[,;])[a-zA-Z]+(?=$|\s|[,;])(?=[^<>]*$)/g, '<span>$&</span>') | ||
|
||
// 6. split words by whitespace or commas | ||
// (?<!<span) - check if space before word is inside span tag, | ||
// we don't want to split that; eg. <span class="bip-39"> | ||
// (?<![>,;]|\s) - check for '>' symbol from previous span tag | ||
// (\s|[,;])+ - find whitespaces or commas | ||
// (?!\s|[<,;]) - check for '<' symbol from next span tag | ||
toFormat = toFormat.replace(/(?<!<span)(?<![>,;]|\s)(\s|[,;])+(?!\s|[<,;])/g, '</span>$&<span>') | ||
|
||
// 7. merge words if there is no delimiter between them | ||
toFormat = toFormat.replace(/<\/span><span[^>]*>/g, '') | ||
|
||
// 8. append to wrapped word | ||
// <span[^<>]*> - find opening tag <span class="..."> before word | ||
// [a-zA-Z]+ - find word itself | ||
// <\/span> - find closing tag </span> after word | ||
// [a-zA-Z]+ - find text to append | ||
toFormat = toFormat.replace(/(<span[^<>]*>[a-zA-Z]+)(<\/span>)([a-zA-Z]+)/g, '$1$3$2') | ||
|
||
// 9. preppend to wrapped word | ||
// [a-zA-Z]+ - find text to prepend | ||
// <span[^<>]*> - find opening tag <span class="..."> before word | ||
// [a-zA-Z]+ - find word itself | ||
// <\/span> - find closing tag </span> after word | ||
toFormat = toFormat.replace(/([a-zA-Z]+)(<span[^<>]*>)([a-zA-Z]+)(<\/span>)/g, '$2$1$3$4') | ||
|
||
// 10. extract delimiters outside of spans | ||
// <span[^<>]*> - find opening tag <span class="..."> before word | ||
// (\s|[,;])* - find delimiters to the left of word | ||
// [a-zA-Z]+ - find word itself | ||
// (\s|[,;])* - find delimiters to the right of word | ||
// <\/span> - find closing tag </span> after word | ||
toFormat = toFormat.replace( | ||
/(<span[^<>]*>)(\s|[,;])*([a-zA-Z]+)(\s|[,;])*(<\/span>)/g, | ||
'$2$1$3$5$4' | ||
) | ||
|
||
// 11 remove empty spans | ||
toFormat = toFormat.replace(/<span[^>]*><\/span>/g, '') | ||
|
||
// 12. translate non-breaking space back to html | ||
// warning: this translates spaces inside span attributes as well, | ||
// but it doesn't matter for the next steps | ||
toFormat = toFormat.replace(/\s/g, ' ') | ||
|
||
// 13. extract raw words into array | ||
// <span[^>]*> - find opening tag | ||
// [^<>]* - find element content | ||
// <\/span>) - find closing tag | ||
const words = matchAll(/<span[^>]*>[^<>]*<\/span>/g, toFormat).map((wrappedWord) => | ||
wrappedWord.replace(/(<span[^>]*>)([^<>]*)(<\/span>)/g, '$2').replace(/(\s|[,;])/g, '') | ||
) | ||
|
||
// 14. reapply style rules to each word | ||
const areWordsBipP39 = words.map((word) => isBip39Word(word)).reverse() | ||
const formattedText = toFormat.replace( | ||
/(<span[^>]*>)([^<>]*)(<\/span>)/g, | ||
(match, p1, p2, p3, offset, string) => { | ||
const style = areWordsBipP39.pop() ? '' : ' class="not-bip-39"' | ||
return `<span${style}>${p2}${p3}` | ||
} | ||
) | ||
const rawText = words.join(' ') | ||
|
||
return {formattedText, rawText} | ||
} | ||
|
||
export {underlineNonBip39words} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
const matchAll = (regex: RegExp, str: string) => { | ||
const words: string[] = [] | ||
let it | ||
while ((it = regex.exec(str)) !== null) { | ||
words.push(it[0]) | ||
} | ||
return words | ||
} | ||
|
||
export default matchAll |
Oops, something went wrong.