Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ruby-3.4.5
ruby-4.0.5
34 changes: 34 additions & 0 deletions __tests__/components/highlighting_text.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Platform } from 'react-native'
import HighlightingText from '../../src/components/text/highlighting_text'
import editorHtml from '../../src/components/text/editor_html'
import App from '../../src/stores/App'

jest.mock('../../src/stores/App', () => ({
Expand All @@ -24,6 +26,38 @@ jest.mock('../../src/components/keyboard/editor_keyboard_avoiding_view', () => {
})

describe('HighlightingText font scaling', () => {
test('requests native WebView focus when focusing the editor', () => {
const requestFocus = jest.fn()
const editor = new HighlightingText({})
editor.webview = { current: { requestFocus } }
editor.editorConfig = () => ({ editable: true })
editor.injectJavaScript = jest.fn()

editor.syncEditor({ focus: true })

expect(requestFocus).toHaveBeenCalled()
})

test('keeps Android newlines in place instead of rewriting the document', () => {
expect(editorHtml).toContain('insertLineBreakInPlace')
expect(editorHtml).toContain('execCommand("insertLineBreak")')
expect(editorHtml).toMatch(/if \(!insertedNewline\) \{\s*insertLineBreakInPlace\(\)/)
})

test('enables the Android keyboard proxy only when autoFocus is set', () => {
const original = Platform.OS
Platform.OS = 'android'
try {
const with_focus = new HighlightingText({ autoFocus: true })
const without_focus = new HighlightingText({ autoFocus: false })
expect(with_focus.state.android_focus_proxy).toBe(true)
expect(without_focus.state.android_focus_proxy).toBe(false)
}
finally {
Platform.OS = original
}
})

test('uses the full system font scale by default', () => {
App.font_scale = 3.5
const editor = new HighlightingText({})
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ android {
applicationId "blog.micro.android"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 98
versionCode 99
versionName "3.0.2"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[tools]
ruby = "3.4.5"
ruby = "4.0.5"
70 changes: 66 additions & 4 deletions src/components/text/editor_html.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const editorHtml = String.raw`<!doctype html>

</style>
</head>
<body><div class="editor_shell"><div contenteditable="true" class="editor" id="editor" spellcheck="true" autocapitalize="sentences"></div><div class="editor_bottom_scrim" aria-hidden="true"></div></div>
<body><div class="editor_shell"><div contenteditable="true" class="editor" id="editor" spellcheck="true" autocapitalize="sentences" enterkeyhint="enter" inputmode="text"></div><div class="editor_bottom_scrim" aria-hidden="true"></div></div>
<script>
(function () {
var isIgnoringInput = false;
Expand Down Expand Up @@ -601,6 +601,35 @@ const editorHtml = String.raw`<!doctype html>
return text.length > 5000;
}

function editorHasFocus(root) {
var active = document.activeElement;
return active === root || !!(root && root.contains(active));
}

function insertLineBreakInPlace() {
var root = editor();
var selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return false;
}

try {
var range = selection.getRangeAt(0);
range.deleteContents();
var br = document.createElement("br");
range.insertNode(br);
range.setStartAfter(br);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
root.focus();
return true;
}
catch (error) {
return false;
}
}

function applyStyles(selection, options) {
var force = options && options.force;
if ((!force && isIgnoringInput) || isComposing || isApplyingStyles) {
Expand All @@ -614,8 +643,12 @@ const editorHtml = String.raw`<!doctype html>
}

var saved = selection || currentSelection();
var hadFocus = options && options.hadFocus != null ? !!options.hadFocus : editorHasFocus(root);
isApplyingStyles = true;
root.innerHTML = highlightHtml(text);
if (hadFocus) {
root.focus();
}
setSelectionRange(saved.start, saved.end);
scheduleClampScrollOffsets();
isApplyingStyles = false;
Expand Down Expand Up @@ -646,22 +679,26 @@ const editorHtml = String.raw`<!doctype html>

function replaceSelectionWithText(insertedText) {
var root = editor();
var hadFocus = editorHasFocus(root);
var text = editorPlainText(root);
var selection = currentSelection();
var start = Math.min(selection.start, selection.end);
var end = Math.max(selection.start, selection.end);
var nextText = text.slice(0, start) + insertedText + text.slice(end);
var nextPosition = start + insertedText.length;
var insertedNewline = insertedText.indexOf("\n") > -1;

root.textContent = nextText;
applyStyles({
start: nextPosition,
end: nextPosition
}, {
force: true
force: true,
hadFocus: hadFocus || insertedNewline
});
if (insertedText.indexOf("\n") > -1) {
if (insertedNewline) {
setTimeout(function () {
editor().focus();
setSelectionRange(nextPosition, nextPosition);
scrollSelectionIntoView();
sendSelectionNow();
Expand Down Expand Up @@ -770,6 +807,13 @@ const editorHtml = String.raw`<!doctype html>
return;
}

if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") {
scheduleChange();
scheduleSelection();
scheduleClampScrollOffsets();
return;
Comment thread
vincentritter marked this conversation as resolved.
}

var root = editor();
var data = event.data || "";
var shouldForce = hasTrailingMarker(root);
Expand Down Expand Up @@ -798,7 +842,25 @@ const editorHtml = String.raw`<!doctype html>

if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") {
event.preventDefault();
replaceSelectionWithText("\n");
var isAndroid = /Android/i.test(navigator.userAgent);
if (isAndroid) {
var insertedNewline = false;
try {
insertedNewline = document.execCommand("insertLineBreak");
}
catch (error) {
insertedNewline = false;
}
if (!insertedNewline) {
insertLineBreakInPlace();
}
}
else {
replaceSelectionWithText("\n");
}
scheduleChange();
scheduleSelection();
scheduleClampScrollOffsets();
return;
}

Expand Down
43 changes: 41 additions & 2 deletions src/components/text/highlighting_text.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { observer } from 'mobx-react'
import { Keyboard, Platform, StyleSheet, View } from 'react-native'
import { Keyboard, Platform, StyleSheet, TextInput, View } from 'react-native'
import { WebView } from 'react-native-webview'
import App from '../../stores/App'
import { EditorKeyboardFrameContext } from '../keyboard/editor_keyboard_avoiding_view'
Expand All @@ -15,7 +15,8 @@ export default class HighlightingText extends React.Component {
this.state = {
container_height: 0,
measured_editor_height: 0,
keyboard_scroll_request: 0
keyboard_scroll_request: 0,
android_focus_proxy: Platform.OS === 'android' && !!props.autoFocus
}
this.container = React.createRef()
this.webview = React.createRef()
Expand All @@ -27,6 +28,7 @@ export default class HighlightingText extends React.Component {
this.keyboard_show_listener = null
this.keyboard_hide_listener = null
this.pending_focus_options = null
this.android_proxy_frame = null
}

componentDidMount() {
Expand Down Expand Up @@ -69,6 +71,9 @@ export default class HighlightingText extends React.Component {
componentWillUnmount() {
this.keyboard_show_listener?.remove()
this.keyboard_hide_listener?.remove()
if (this.android_proxy_frame) {
cancelAnimationFrame(this.android_proxy_frame)
}
}

normalizedValue(props = this.props) {
Expand Down Expand Up @@ -290,6 +295,9 @@ export default class HighlightingText extends React.Component {
}

this.last_config = JSON.stringify(config)
if (payload.focus) {
this.webview.current?.requestFocus?.()
}
this.injectJavaScript(`window.MicroBlogReactEditor.updateFromReact(${JSON.stringify(payload)})`)
}

Expand All @@ -313,6 +321,14 @@ export default class HighlightingText extends React.Component {
if (this.pending_focus_options) {
this.focus(this.pending_focus_options)
}
if (this.state.android_focus_proxy) {
this.focus({ cursorToEnd: true })
this.android_proxy_frame = requestAnimationFrame(() => {
this.setState({
android_focus_proxy: false
})
})
}
return
}

Expand Down Expand Up @@ -354,6 +370,21 @@ export default class HighlightingText extends React.Component {

return (
<View ref={this.container} onLayout={this.handleLayout} style={this.webviewStyle()}>
{
this.state.android_focus_proxy ?
<TextInput
style={styles.android_focus_proxy}
autoFocus={true}
caretHidden={true}
autoCorrect={false}
spellCheck={false}
showSoftInputOnFocus={true}
importantForAutofill="no"
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
/>
: null
}
<WebView
ref={this.webview}
source={{ html: editorHtml, baseUrl: 'https://micro.blog' }}
Expand Down Expand Up @@ -383,5 +414,13 @@ export default class HighlightingText extends React.Component {
const styles = StyleSheet.create({
webview: {
flex: 1
},
android_focus_proxy: {
position: 'absolute',
width: 1,
height: 1,
opacity: 0,
left: 0,
top: 0
}
})