diff --git a/client/components/editor/editor-asciidoc.vue b/client/components/editor/editor-asciidoc.vue index 126ba370e..b30cf7723 100644 --- a/client/components/editor/editor-asciidoc.vue +++ b/client/components/editor/editor-asciidoc.vue @@ -151,6 +151,7 @@ import 'codemirror/addon/hint/show-hint.js' import 'codemirror/addon/fold/foldcode.js' import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/foldgutter.css' +import { getCodeMirrorInputStyle } from '../../helpers/codemirror' import cmFold from './common/cmFold' // ======================================== @@ -403,7 +404,7 @@ export default { annotateScrollbar: true }, viewportMargin: 50, - inputStyle: 'contenteditable', + inputStyle: getCodeMirrorInputStyle(), allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'], direction: siteConfig.rtl ? 'rtl' : 'ltr', foldGutter: true, diff --git a/client/components/editor/editor-code.vue b/client/components/editor/editor-code.vue index ee62aa182..b585b13c5 100644 --- a/client/components/editor/editor-code.vue +++ b/client/components/editor/editor-code.vue @@ -77,6 +77,7 @@ import 'codemirror/addon/display/fullscreen.js' import 'codemirror/addon/display/fullscreen.css' import 'codemirror/addon/selection/mark-selection.js' import 'codemirror/addon/search/searchcursor.js' +import { getCodeMirrorInputStyle } from '../../helpers/codemirror' // ======================================== // INIT @@ -193,7 +194,7 @@ export default { annotateScrollbar: true }, viewportMargin: 50, - inputStyle: 'contenteditable', + inputStyle: getCodeMirrorInputStyle(), allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'] }) this.cm.setValue(this.$store.get('editor/content')) diff --git a/client/components/editor/editor-markdown.vue b/client/components/editor/editor-markdown.vue index baee118d0..501697ad3 100644 --- a/client/components/editor/editor-markdown.vue +++ b/client/components/editor/editor-markdown.vue @@ -195,6 +195,7 @@ import 'codemirror/addon/hint/show-hint.js' import 'codemirror/addon/fold/foldcode.js' import 'codemirror/addon/fold/foldgutter.js' import 'codemirror/addon/fold/foldgutter.css' +import { getCodeMirrorInputStyle } from '../../helpers/codemirror' // Markdown-it import MarkdownIt from 'markdown-it' @@ -751,7 +752,7 @@ export default { annotateScrollbar: true }, viewportMargin: 50, - inputStyle: 'contenteditable', + inputStyle: getCodeMirrorInputStyle(), allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif'], direction: siteConfig.rtl ? 'rtl' : 'ltr', foldGutter: true, diff --git a/client/components/editor/editor-modal-properties.vue b/client/components/editor/editor-modal-properties.vue index a6ed1af3f..e753509d9 100644 --- a/client/components/editor/editor-modal-properties.vue +++ b/client/components/editor/editor-modal-properties.vue @@ -253,6 +253,7 @@ import CodeMirror from 'codemirror' import 'codemirror/lib/codemirror.css' import 'codemirror/mode/htmlmixed/htmlmixed.js' import 'codemirror/mode/css/css.js' +import { getCodeMirrorInputStyle } from '../../helpers/codemirror' /* global siteLangs, siteConfig */ const filenamePattern = /^(?![\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s])(?!.*[\#\/\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]$)[^\#\.\$\^\=\*\;\:\&\?\(\)\[\]\{\}\"\'\>\<\,\@\!\%\`\~\s]*$/ @@ -367,7 +368,7 @@ export default { line: true, styleActiveLine: true, viewportMargin: 50, - inputStyle: 'contenteditable', + inputStyle: getCodeMirrorInputStyle(), direction: 'ltr' }) switch (mode) { diff --git a/client/helpers/codemirror.js b/client/helpers/codemirror.js new file mode 100644 index 000000000..6f313b956 --- /dev/null +++ b/client/helpers/codemirror.js @@ -0,0 +1,21 @@ +const DEFAULT_CODEMIRROR_INPUT_STYLE = 'contenteditable' + +export function isIOSDevice({ userAgent = '', platform = '', maxTouchPoints = 0 } = {}) { + return /iP(?:ad|hone|od)/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1) +} + +export function getCodeMirrorInputStyle(nav = (typeof navigator !== 'undefined' ? navigator : null)) { + if (!nav) { + return DEFAULT_CODEMIRROR_INPUT_STYLE + } + + // iOS Korean IME uses a fragile contenteditable/beforeinput path in Safari/WebKit. + // Falling back to the textarea input style avoids broken Hangul composition. + return isIOSDevice({ + userAgent: nav.userAgent, + platform: nav.platform, + maxTouchPoints: nav.maxTouchPoints || 0 + }) ? 'textarea' : DEFAULT_CODEMIRROR_INPUT_STYLE +} + +export default getCodeMirrorInputStyle diff --git a/client/helpers/codemirror.test.js b/client/helpers/codemirror.test.js new file mode 100644 index 000000000..1436f695f --- /dev/null +++ b/client/helpers/codemirror.test.js @@ -0,0 +1,27 @@ +import { getCodeMirrorInputStyle, isIOSDevice } from './codemirror' + +describe('helpers/codemirror', () => { + it('detects classic iOS user agents', () => { + expect(isIOSDevice({ + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1', + platform: 'iPhone', + maxTouchPoints: 5 + })).toBe(true) + }) + + it('detects iPadOS devices that expose a desktop platform', () => { + expect(getCodeMirrorInputStyle({ + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1', + platform: 'MacIntel', + maxTouchPoints: 5 + })).toBe('textarea') + }) + + it('keeps the existing contenteditable path on desktop browsers', () => { + expect(getCodeMirrorInputStyle({ + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', + platform: 'MacIntel', + maxTouchPoints: 0 + })).toBe('contenteditable') + }) +})