diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9addddf9ef4..6a6cc8ac410 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -54,6 +54,7 @@ //= require ./discourse/lib/autocomplete //= require ./discourse/lib/after-transition //= require ./discourse/lib/safari-hacks +//= require ./discourse/lib/put-cursor-at-end //= require_tree ./discourse/adapters //= require ./discourse/models/post-action-type //= require ./discourse/models/post diff --git a/app/assets/javascripts/discourse/components/composer-body.js b/app/assets/javascripts/discourse/components/composer-body.js index e5a6e8261dc..83792801aae 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js +++ b/app/assets/javascripts/discourse/components/composer-body.js @@ -150,28 +150,27 @@ export default Component.extend(KeyEnterEscape, { }, viewportResize() { - const composerVH = window.visualViewport.height * 0.01; + const composerVH = window.visualViewport.height * 0.01, + doc = document.documentElement; - document.documentElement.style.setProperty( - "--composer-vh", - `${composerVH}px` - ); + doc.style.setProperty("--composer-vh", `${composerVH}px`); const viewportWindowDiff = window.innerHeight - window.visualViewport.height; + viewportWindowDiff + ? doc.classList.add("keyboard-visible") + : doc.classList.remove("keyboard-visible"); // adds bottom padding when using a hardware keyboard and the accessory bar is visible // accessory bar height is 55px, using 75 allows a small buffer - if (viewportWindowDiff > 0 && viewportWindowDiff < 75) { - document.documentElement.style.setProperty( + + if (viewportWindowDiff < 75) { + doc.style.setProperty( "--composer-ipad-padding", `${viewportWindowDiff}px` ); } else { - document.documentElement.style.setProperty( - "--composer-ipad-padding", - "0px" - ); + doc.style.setProperty("--composer-ipad-padding", "0px"); } }, diff --git a/app/assets/javascripts/discourse/components/composer-editor.js b/app/assets/javascripts/discourse/components/composer-editor.js index e6e000aa09c..ad34e961849 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js +++ b/app/assets/javascripts/discourse/components/composer-editor.js @@ -28,11 +28,10 @@ import { tinyAvatar, formatUsername, clipboardData, - safariHacksDisabled, caretPosition, - inCodeBlock, - putCursorAtEnd + inCodeBlock } from "discourse/lib/utilities"; +import putCursorAtEnd from "discourse/lib/put-cursor-at-end"; import { validateUploadedFiles, authorizesOneOrMoreImageExtensions, @@ -210,10 +209,7 @@ export default Component.extend({ } // Focus on the body unless we have a title - if ( - !this.get("composer.canEditTitle") && - (!this.capabilities.isIOS || safariHacksDisabled()) - ) { + if (!this.get("composer.canEditTitle")) { putCursorAtEnd(this.element.querySelector(".d-editor-input")); } diff --git a/app/assets/javascripts/discourse/components/composer-title.js b/app/assets/javascripts/discourse/components/composer-title.js index e036beef1ad..b4cb3ef3828 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js +++ b/app/assets/javascripts/discourse/components/composer-title.js @@ -7,7 +7,7 @@ import { lookupCache } from "pretty-text/oneboxer-cache"; import { ajax } from "discourse/lib/ajax"; import ENV from "discourse-common/config/environment"; import EmberObject from "@ember/object"; -import { putCursorAtEnd } from "discourse/lib/utilities"; +import putCursorAtEnd from "discourse/lib/put-cursor-at-end"; export default Component.extend({ classNames: ["title-input"], diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js b/app/assets/javascripts/discourse/components/composer-user-selector.js index 34186972076..efbc3aafdbd 100644 --- a/app/assets/javascripts/discourse/components/composer-user-selector.js +++ b/app/assets/javascripts/discourse/components/composer-user-selector.js @@ -1,7 +1,7 @@ import { schedule } from "@ember/runloop"; import Component from "@ember/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { putCursorAtEnd } from "discourse/lib/utilities"; +import putCursorAtEnd from "discourse/lib/put-cursor-at-end"; export default Component.extend({ showSelector: true, diff --git a/app/assets/javascripts/discourse/controllers/composer.js b/app/assets/javascripts/discourse/controllers/composer.js index 1706c82dd95..b7855d54b91 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js +++ b/app/assets/javascripts/discourse/controllers/composer.js @@ -13,7 +13,7 @@ import discourseComputed, { on } from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; -import { escapeExpression, safariHacksDisabled } from "discourse/lib/utilities"; +import { escapeExpression } from "discourse/lib/utilities"; import { authorizesOneOrMoreExtensions, uploadIcon @@ -136,10 +136,6 @@ export default Controller.extend({ "model.composeState" ) focusTarget(replyingToTopic, creatingPM, usernames, composeState) { - if (this.capabilities.isIOS && !safariHacksDisabled()) { - return "none"; - } - // Focus on usernames if it's blank or if it's just you usernames = usernames || ""; if ( diff --git a/app/assets/javascripts/discourse/lib/put-cursor-at-end.js b/app/assets/javascripts/discourse/lib/put-cursor-at-end.js new file mode 100644 index 00000000000..eec28b36a35 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/put-cursor-at-end.js @@ -0,0 +1,16 @@ +import positioningWorkaround from "discourse/lib/safari-hacks"; +import { isAppleDevice } from "discourse/lib/utilities"; + +export default function(element) { + if (isAppleDevice() && positioningWorkaround.touchstartEvent) { + positioningWorkaround.touchstartEvent(element); + } else { + element.focus(); + } + + const len = element.value.length; + element.setSelectionRange(len, len); + + // Scroll to the bottom, in case we're in a tall textarea + element.scrollTop = 999999; +} diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js b/app/assets/javascripts/discourse/lib/safari-hacks.js index 40d8ee4637d..d18d06fcc16 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js @@ -84,6 +84,12 @@ function positioningWorkaround($fixedElement) { return; } + document.addEventListener("scroll", () => { + if (!caps.isIpadOS && workaroundActive) { + document.documentElement.scrollTop = 0; + } + }); + const fixedElement = $fixedElement[0]; const oldHeight = fixedElement.style.height; @@ -94,17 +100,17 @@ function positioningWorkaround($fixedElement) { if (workaroundActive) { $("body").removeClass("ios-safari-composer-hacks"); + $(window).scrollTop(originalScrollTop); + if (evt && evt.target) { + evt.target.removeEventListener("blur", blurred); + } + + workaroundActive = false; + if (!iOSWithVisualViewport()) { fixedElement.style.height = oldHeight; later(() => $(fixedElement).removeClass("no-transition"), 500); } - - $(window).scrollTop(originalScrollTop); - - if (evt) { - evt.target.removeEventListener("blur", blurred); - } - workaroundActive = false; } }; @@ -113,8 +119,9 @@ function positioningWorkaround($fixedElement) { // document.activeElement is also unreliable (iOS does not mark buttons as focused) // so instead, we store the last touched element and check against it - // cancel blur event if user is: + // cancel blur event when: // - switching to another iOS app + // - displaying title field // - invoking a select-kit dropdown // - invoking mentions // - invoking emoji dropdown via : and hitting return @@ -123,6 +130,7 @@ function positioningWorkaround($fixedElement) { if ( lastTouchedElement && (document.visibilityState === "hidden" || + $fixedElement.hasClass("edit-title") || $(lastTouchedElement).hasClass("select-kit-header") || $(lastTouchedElement).closest(".autocomplete").length || (lastTouchedElement.nodeName.toLowerCase() === "textarea" && @@ -140,6 +148,12 @@ function positioningWorkaround($fixedElement) { var blurred = discourseDebounce(blurredNow, INPUT_DELAY); var positioningHack = function(evt) { + let _this = this; + + if (evt === undefined) { + evt = new CustomEvent("no-op"); + } + // we need this, otherwise changing focus means we never clear this.addEventListener("blur", blurred); @@ -150,12 +164,22 @@ function positioningWorkaround($fixedElement) { .find(".select-kit > button.is-focused") .removeClass("is-focused"); - if ($(window).scrollTop() > 0) { - originalScrollTop = $(window).scrollTop(); + originalScrollTop = $(window).scrollTop(); + + const elementRect = _this.getBoundingClientRect(); + if (elementRect.top > 100) { + // this tricks iOS safari into assuming input/textarea is at top of the viewport + // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input + _this.style.transform = "translateY(-400px)"; + setTimeout(function() { + _this.style.transform = "none"; + }, 30); } + let delay = caps.isIpadOS ? 350 : 150; + setTimeout(function() { - if (iOSWithVisualViewport()) { + if (caps.isIpadOS && iOSWithVisualViewport()) { // disable hacks when using a hardware keyboard // by default, a hardware keyboard will show the keyboard accessory bar // whose height is currently 55px (using 75 for a bit of a buffer) @@ -165,38 +189,14 @@ function positioningWorkaround($fixedElement) { } } - if (fixedElement.style.top === "0px") { - if (this !== document.activeElement) { - evt.preventDefault(); - - // this tricks safari into assuming current input is at top of the viewport - // via https://stackoverflow.com/questions/38017771/mobile-safari-prevent-scroll-page-when-focus-on-input - this.style.transform = "translateY(-200px)"; - this.focus(); - let _this = this; - setTimeout(function() { - _this.style.transform = "none"; - }, 30); - } - return; - } - // don't trigger keyboard on disabled element (happens when a category is required) - if (this.disabled) { + if (_this.disabled) { return; } $("body").addClass("ios-safari-composer-hacks"); $(window).scrollTop(0); - let i = 20; - let interval = setInterval(() => { - $(window).scrollTop(0); - if (i-- === 0) { - clearInterval(interval); - } - }, 10); - if (!iOSWithVisualViewport()) { const height = calcHeight(); fixedElement.style.height = height + "px"; @@ -204,9 +204,9 @@ function positioningWorkaround($fixedElement) { } evt.preventDefault(); - this.focus(); + _this.focus(); workaroundActive = true; - }, 350); + }, delay); }; var lastTouched = function(evt) { @@ -230,6 +230,11 @@ function positioningWorkaround($fixedElement) { }); }, 100); + positioningWorkaround.touchstartEvent = function(element) { + var triggerHack = positioningHack.bind(element); + triggerHack(); + }; + const config = { childList: true, subtree: true, diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index c753f4b0cf4..03ef200016b 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -444,11 +444,5 @@ export function inCodeBlock(text, pos) { return result; } -export function putCursorAtEnd(element) { - element.focus(); - const len = element.value.length; - element.setSelectionRange(len, len); -} - // This prevents a mini racer crash export default {}; diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index f4062f52414..e28806131b4 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -26,9 +26,10 @@ body.ios-safari-composer-hacks &.open { height: calc(var(--composer-vh, 1vh) * 100); - .reply-area { - padding-bottom: 0px; - } + } + + .keyboard-visible body.ios-safari-composer-hacks &.open .reply-area { + padding-bottom: 0px; } .reply-to {