mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 03:06:53 +08:00
DEV: Consolidate mobile positioning strategies on mobile and iPad (#30241)
This removes some longstanding Safari iOS positioning hacks and refactors the mobile positioning strategy across Safari, Chrome and Firefox. See PR descriptions for more details. Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
@ -3,7 +3,6 @@ import { cancel, schedule, throttle } from "@ember/runloop";
|
|||||||
import { classNameBindings } from "@ember-decorators/component";
|
import { classNameBindings } from "@ember-decorators/component";
|
||||||
import { observes } from "@ember-decorators/object";
|
import { observes } from "@ember-decorators/object";
|
||||||
import { headerOffset } from "discourse/lib/offset-calculator";
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||||
import positioningWorkaround from "discourse/lib/safari-hacks";
|
|
||||||
import { isiPad } from "discourse/lib/utilities";
|
import { isiPad } from "discourse/lib/utilities";
|
||||||
import Composer from "discourse/models/composer";
|
import Composer from "discourse/models/composer";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
@ -68,13 +67,6 @@ export default class ComposerBody extends Component {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@observes("composeState")
|
|
||||||
disableFullscreen() {
|
|
||||||
if (this.composeState !== Composer.OPEN && positioningWorkaround.blur) {
|
|
||||||
positioningWorkaround.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupComposerResizeEvents() {
|
setupComposerResizeEvents() {
|
||||||
this.origComposerSize = 0;
|
this.origComposerSize = 0;
|
||||||
this.lastMousePos = 0;
|
this.lastMousePos = 0;
|
||||||
@ -184,8 +176,6 @@ export default class ComposerBody extends Component {
|
|||||||
triggerOpen();
|
triggerOpen();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
positioningWorkaround(this.element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
|
@ -211,6 +211,7 @@
|
|||||||
{{/unless}}
|
{{/unless}}
|
||||||
</div>
|
</div>
|
||||||
</ComposerEditor>
|
</ComposerEditor>
|
||||||
|
<DComposerPosition />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<PluginOutlet
|
<PluginOutlet
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { later } from "@ember/runloop";
|
||||||
|
|
||||||
|
export default class DComposerPosition extends Component {
|
||||||
|
// This component contains two composer positioning adjustments
|
||||||
|
// for Safari iOS/iPad and Firefox on Android
|
||||||
|
// The fixes here go together with styling in base/compose.css
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
if (
|
||||||
|
html.classList.contains("mobile-device") ||
|
||||||
|
html.classList.contains("ipados-device")
|
||||||
|
) {
|
||||||
|
window.addEventListener("scroll", this._correctScrollPosition);
|
||||||
|
this._correctScrollPosition();
|
||||||
|
const editor = document.querySelector(".d-editor-input");
|
||||||
|
editor?.addEventListener("touchmove", this._textareaTouchMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
if (
|
||||||
|
html.classList.contains("mobile-device") ||
|
||||||
|
html.classList.contains("ipados-device")
|
||||||
|
) {
|
||||||
|
window.removeEventListener("scroll", this._correctScrollPosition);
|
||||||
|
const editor = document.querySelector(".d-editor-input");
|
||||||
|
editor?.removeEventListener("touchmove", this._textareaTouchMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_correctScrollPosition() {
|
||||||
|
// In some rare cases, when quoting a large text or
|
||||||
|
// when editing a long topic, Safari/Firefox will scroll
|
||||||
|
// the body so that the input/textarea is centered
|
||||||
|
// This pushes the fixed element offscreen
|
||||||
|
// Here we detect when the composer's top position is above the window's
|
||||||
|
// current scroll offset and correct it
|
||||||
|
later(() => {
|
||||||
|
const el = document.querySelector("#reply-control");
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.top < -1) {
|
||||||
|
const scrollAmount = window.scrollY + rect.top;
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: scrollAmount,
|
||||||
|
behavior: "instant",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
_textareaTouchMove(event) {
|
||||||
|
// This is an alternative to locking up the body
|
||||||
|
// It stops scrolling in the given element from bubbling up to the body
|
||||||
|
// when the textarea does not have any content to scroll
|
||||||
|
if (event.target) {
|
||||||
|
const notScrollable =
|
||||||
|
event.target.scrollHeight <= event.target.clientHeight;
|
||||||
|
if (notScrollable) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,17 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { cancel, scheduleOnce } from "@ember/runloop";
|
import { cancel, scheduleOnce } from "@ember/runloop";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { clearAllBodyScrollLocks } from "discourse/lib/body-scroll-lock";
|
|
||||||
import isZoomed from "discourse/lib/zoom-check";
|
import isZoomed from "discourse/lib/zoom-check";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
const KEYBOARD_DETECT_THRESHOLD = 150;
|
|
||||||
|
|
||||||
export default class DVirtualHeight extends Component {
|
export default class DVirtualHeight extends Component {
|
||||||
@service site;
|
@service site;
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
|
||||||
|
MIN_THRESHOLD = 120;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ export default class DVirtualHeight extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle device rotation
|
|
||||||
this.windowInnerHeight = window.innerHeight;
|
this.windowInnerHeight = window.innerHeight;
|
||||||
|
|
||||||
scheduleOnce("afterRender", this, this.debouncedOnViewportResize);
|
scheduleOnce("afterRender", this, this.debouncedOnViewportResize);
|
||||||
@ -33,13 +31,6 @@ export default class DVirtualHeight extends Component {
|
|||||||
"resize",
|
"resize",
|
||||||
this.debouncedOnViewportResize
|
this.debouncedOnViewportResize
|
||||||
);
|
);
|
||||||
if ("virtualKeyboard" in navigator) {
|
|
||||||
navigator.virtualKeyboard.overlaysContent = true;
|
|
||||||
navigator.virtualKeyboard.addEventListener(
|
|
||||||
"geometrychange",
|
|
||||||
this.debouncedOnViewportResize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
@ -51,13 +42,6 @@ export default class DVirtualHeight extends Component {
|
|||||||
"resize",
|
"resize",
|
||||||
this.debouncedOnViewportResize
|
this.debouncedOnViewportResize
|
||||||
);
|
);
|
||||||
if ("virtualKeyboard" in navigator) {
|
|
||||||
navigator.virtualKeyboard.overlaysContent = false;
|
|
||||||
navigator.virtualKeyboard.removeEventListener(
|
|
||||||
"geometrychange",
|
|
||||||
this.debouncedOnViewportResize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setVH() {
|
setVH() {
|
||||||
@ -65,18 +49,10 @@ export default class DVirtualHeight extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let height;
|
const height = Math.round(window.visualViewport.height);
|
||||||
if ("virtualKeyboard" in navigator) {
|
|
||||||
height =
|
|
||||||
window.visualViewport.height -
|
|
||||||
navigator.virtualKeyboard.boundingRect.height;
|
|
||||||
} else {
|
|
||||||
const activeWindow = window.visualViewport || window;
|
|
||||||
height = activeWindow?.height || window.innerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.previousHeight && Math.abs(this.previousHeight - height) <= 1) {
|
if (this.previousHeight && Math.abs(this.previousHeight - height) <= 1) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousHeight = height;
|
this.previousHeight = height;
|
||||||
@ -94,48 +70,27 @@ export default class DVirtualHeight extends Component {
|
|||||||
|
|
||||||
@bind
|
@bind
|
||||||
onViewportResize() {
|
onViewportResize() {
|
||||||
this.setVH();
|
const setVHresult = this.setVH();
|
||||||
|
|
||||||
|
if (setVHresult === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docEl = document.documentElement;
|
||||||
|
|
||||||
let keyboardVisible = false;
|
let keyboardVisible = false;
|
||||||
if ("virtualKeyboard" in navigator) {
|
|
||||||
if (navigator.virtualKeyboard.boundingRect.height > 0) {
|
|
||||||
keyboardVisible = true;
|
|
||||||
}
|
|
||||||
} else if (this.capabilities.isFirefox && this.capabilities.isAndroid) {
|
|
||||||
if (
|
|
||||||
Math.abs(
|
|
||||||
this.windowInnerHeight -
|
|
||||||
Math.min(window.innerHeight, window.visualViewport.height)
|
|
||||||
) > KEYBOARD_DETECT_THRESHOLD
|
|
||||||
) {
|
|
||||||
keyboardVisible = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let viewportWindowDiff =
|
let viewportWindowDiff =
|
||||||
this.windowInnerHeight - window.visualViewport.height;
|
this.windowInnerHeight - window.visualViewport.height;
|
||||||
const IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT = 71.5;
|
|
||||||
if (viewportWindowDiff > IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT) {
|
|
||||||
keyboardVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// adds bottom padding when using a hardware keyboard and the accessory bar is visible
|
if (viewportWindowDiff > this.MIN_THRESHOLD) {
|
||||||
// accessory bar height is 55px, using 75 allows a small buffer
|
keyboardVisible = true;
|
||||||
if (this.capabilities.isIpadOS) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--composer-ipad-padding",
|
|
||||||
`${viewportWindowDiff < 75 ? viewportWindowDiff : 0}px`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appEvents.trigger("keyboard-visibility-change", keyboardVisible);
|
this.appEvents.trigger("keyboard-visibility-change", keyboardVisible);
|
||||||
|
|
||||||
keyboardVisible
|
keyboardVisible
|
||||||
? document.documentElement.classList.add("keyboard-visible")
|
? docEl.classList.add("keyboard-visible")
|
||||||
: document.documentElement.classList.remove("keyboard-visible");
|
: docEl.classList.remove("keyboard-visible");
|
||||||
|
|
||||||
if (!keyboardVisible) {
|
|
||||||
clearAllBodyScrollLocks();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import { schedule, scheduleOnce } from "@ember/runloop";
|
|||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import MountWidget from "discourse/components/mount-widget";
|
import MountWidget from "discourse/components/mount-widget";
|
||||||
import offsetCalculator from "discourse/lib/offset-calculator";
|
import offsetCalculator from "discourse/lib/offset-calculator";
|
||||||
import { isWorkaroundActive } from "discourse/lib/safari-hacks";
|
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import { cloak, uncloak } from "discourse/widgets/post-stream";
|
import { cloak, uncloak } from "discourse/widgets/post-stream";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
@ -64,11 +63,7 @@ export default class ScrollingPostStream extends MountWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (document.webkitFullscreenElement || document.fullscreenElement) {
|
||||||
isWorkaroundActive() ||
|
|
||||||
document.webkitFullscreenElement ||
|
|
||||||
document.fullscreenElement
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ const MIN_HEIGHT_TIMELINE = 325;
|
|||||||
|
|
||||||
@classNameBindings(
|
@classNameBindings(
|
||||||
"info.topicProgressExpanded:topic-progress-expanded",
|
"info.topicProgressExpanded:topic-progress-expanded",
|
||||||
"info.renderTimeline:with-timeline:with-topic-progress"
|
"info.renderTimeline:with-timeline",
|
||||||
|
"info.withTopicProgress:with-topic-progress"
|
||||||
)
|
)
|
||||||
export default class TopicNavigation extends Component {
|
export default class TopicNavigation extends Component {
|
||||||
@service modal;
|
@service modal;
|
||||||
@ -33,7 +34,10 @@ export default class TopicNavigation extends Component {
|
|||||||
if (this._lastTopicId !== this.topic.id) {
|
if (this._lastTopicId !== this.topic.id) {
|
||||||
this._lastTopicId = this.topic.id;
|
this._lastTopicId = this.topic.id;
|
||||||
this.set("canRender", false);
|
this.set("canRender", false);
|
||||||
next(() => this.set("canRender", true));
|
next(() => {
|
||||||
|
this.set("canRender", true);
|
||||||
|
this._performCheckSize();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +61,11 @@ export default class TopicNavigation extends Component {
|
|||||||
this.mediaQuery.matches && verticalSpace > MIN_HEIGHT_TIMELINE
|
this.mediaQuery.matches && verticalSpace > MIN_HEIGHT_TIMELINE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.info.set(
|
||||||
|
"withTopicProgress",
|
||||||
|
!this.info.renderTimeline && this.topic.posts_count > 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -3,16 +3,12 @@ import { action } from "@ember/object";
|
|||||||
import { alias } from "@ember/object/computed";
|
import { alias } from "@ember/object/computed";
|
||||||
import { scheduleOnce } from "@ember/runloop";
|
import { scheduleOnce } from "@ember/runloop";
|
||||||
import { classNameBindings } from "@ember-decorators/component";
|
import { classNameBindings } from "@ember-decorators/component";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
|
||||||
|
|
||||||
const CSS_TRANSITION_DELAY = 500;
|
@classNameBindings("docked")
|
||||||
|
|
||||||
@classNameBindings("docked", "withTransitions")
|
|
||||||
export default class TopicProgress extends Component {
|
export default class TopicProgress extends Component {
|
||||||
elementId = "topic-progress-wrapper";
|
elementId = "topic-progress-wrapper";
|
||||||
docked = false;
|
docked = false;
|
||||||
withTransitions = null;
|
|
||||||
progressPosition = null;
|
progressPosition = null;
|
||||||
|
|
||||||
@alias("topic.postStream") postStream;
|
@alias("topic.postStream") postStream;
|
||||||
@ -69,107 +65,21 @@ export default class TopicProgress extends Component {
|
|||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
super.didInsertElement(...arguments);
|
super.didInsertElement(...arguments);
|
||||||
|
|
||||||
this.appEvents
|
this.appEvents.on("topic:current-post-scrolled", this, this._topicScrolled);
|
||||||
.on("composer:resized", this, this._composerEvent)
|
|
||||||
.on("topic:current-post-scrolled", this, this._topicScrolled);
|
|
||||||
|
|
||||||
if (this.prevEvent) {
|
if (this.prevEvent) {
|
||||||
scheduleOnce("afterRender", this, this._topicScrolled, this.prevEvent);
|
scheduleOnce("afterRender", this, this._topicScrolled, this.prevEvent);
|
||||||
}
|
}
|
||||||
scheduleOnce("afterRender", this, this._startObserver);
|
|
||||||
|
|
||||||
// start CSS transitions a tiny bit later
|
|
||||||
// to avoid jumpiness on initial topic load
|
|
||||||
discourseLater(this._addCssTransitions, CSS_TRANSITION_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
super.willDestroyElement(...arguments);
|
super.willDestroyElement(...arguments);
|
||||||
this._topicBottomObserver?.disconnect();
|
this.appEvents.off(
|
||||||
this.appEvents
|
"topic:current-post-scrolled",
|
||||||
.off("composer:resized", this, this._composerEvent)
|
this,
|
||||||
.off("topic:current-post-scrolled", this, this._topicScrolled);
|
this._topicScrolled
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_addCssTransitions() {
|
|
||||||
if (this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.set("withTransitions", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_startObserver() {
|
|
||||||
if ("IntersectionObserver" in window) {
|
|
||||||
this._topicBottomObserver = this._setupObserver();
|
|
||||||
this._topicBottomObserver.observe(
|
|
||||||
document.querySelector("#topic-bottom")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_setupObserver() {
|
|
||||||
// minimum 50px here ensures element is not docked when
|
|
||||||
// scrolling down quickly, it causes post stream refresh loop
|
|
||||||
// on Android
|
|
||||||
const bottomIntersectionMargin =
|
|
||||||
document.querySelector("#reply-control")?.clientHeight || 50;
|
|
||||||
|
|
||||||
return new IntersectionObserver(this._intersectionHandler, {
|
|
||||||
threshold: 1,
|
|
||||||
rootMargin: `0px 0px -${bottomIntersectionMargin}px 0px`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_composerEvent() {
|
|
||||||
// reinitializing needed to account for composer height
|
|
||||||
// might be no longer necessary if IntersectionObserver API supports dynamic rootMargin
|
|
||||||
// see https://github.com/w3c/IntersectionObserver/issues/428
|
|
||||||
if ("IntersectionObserver" in window) {
|
|
||||||
this._topicBottomObserver?.disconnect();
|
|
||||||
this._startObserver();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_intersectionHandler(entries) {
|
|
||||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const composerH =
|
|
||||||
document.querySelector("#reply-control")?.clientHeight || 0;
|
|
||||||
|
|
||||||
// on desktop, pin this element to the composer
|
|
||||||
// otherwise the grid layout will change too much when toggling the composer
|
|
||||||
// and jitter when the viewport is near the topic bottom
|
|
||||||
if (this.site.desktopView && composerH) {
|
|
||||||
this.set("docked", false);
|
|
||||||
this.element.style.setProperty("bottom", `${composerH}px`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entries[0].isIntersecting === true) {
|
|
||||||
this.set("docked", true);
|
|
||||||
this.element.style.removeProperty("bottom");
|
|
||||||
} else {
|
|
||||||
if (entries[0].boundingClientRect.top > 0) {
|
|
||||||
this.set("docked", false);
|
|
||||||
if (composerH === 0) {
|
|
||||||
const filteredPostsHeight =
|
|
||||||
document.querySelector(".posts-filtered-notice")?.clientHeight || 0;
|
|
||||||
filteredPostsHeight === 0
|
|
||||||
? this.element.style.removeProperty("bottom")
|
|
||||||
: this.element.style.setProperty(
|
|
||||||
"bottom",
|
|
||||||
`${filteredPostsHeight}px`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.element.style.setProperty("bottom", `${composerH}px`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
click(e) {
|
click(e) {
|
||||||
if (e.target.closest("#topic-progress")) {
|
if (e.target.closest("#topic-progress")) {
|
||||||
|
@ -8,5 +8,13 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
html.classList.add("no-touch", "discourse-no-touch");
|
html.classList.add("no-touch", "discourse-no-touch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (caps.isIpadOS) {
|
||||||
|
html.classList.add("ipados-device");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (caps.isIOS) {
|
||||||
|
html.classList.add("ios-device");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -41,7 +41,7 @@ const isIosDevice =
|
|||||||
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
|
(/iP(ad|hone|od)/.test(window.navigator.platform) ||
|
||||||
(window.navigator.platform === "MacIntel" &&
|
(window.navigator.platform === "MacIntel" &&
|
||||||
window.navigator.maxTouchPoints > 1));
|
window.navigator.maxTouchPoints > 1));
|
||||||
let locks = [];
|
export let locks = [];
|
||||||
let locksIndex = /* @__PURE__ */ new Map();
|
let locksIndex = /* @__PURE__ */ new Map();
|
||||||
let documentListenerAdded = false;
|
let documentListenerAdded = false;
|
||||||
let initialClientY = -1;
|
let initialClientY = -1;
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
import positioningWorkaround from "discourse/lib/safari-hacks";
|
|
||||||
import { helperContext } from "discourse-common/lib/helpers";
|
|
||||||
|
|
||||||
export default function (element) {
|
export default function (element) {
|
||||||
const caps = helperContext().capabilities;
|
|
||||||
|
|
||||||
if (caps.isApple && positioningWorkaround.touchstartEvent) {
|
|
||||||
positioningWorkaround.touchstartEvent(element);
|
|
||||||
} else {
|
|
||||||
element.focus();
|
element.focus();
|
||||||
}
|
|
||||||
|
|
||||||
const len = element.value.length;
|
const len = element.value.length;
|
||||||
element.setSelectionRange(len, len);
|
element.setSelectionRange(len, len);
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
import $ from "jquery";
|
|
||||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
|
||||||
import { helperContext } from "discourse-common/lib/helpers";
|
|
||||||
import discourseLater from "discourse-common/lib/later";
|
|
||||||
|
|
||||||
let workaroundActive = false;
|
|
||||||
|
|
||||||
export function isWorkaroundActive() {
|
|
||||||
return workaroundActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
|
|
||||||
function positioningWorkaround(fixedElement) {
|
|
||||||
let caps = helperContext().capabilities;
|
|
||||||
if (!caps.isIOS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("scroll", () => {
|
|
||||||
if (!caps.isIpadOS && workaroundActive) {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let originalScrollTop = 0;
|
|
||||||
let lastTouchedElement = null;
|
|
||||||
|
|
||||||
positioningWorkaround.blur = function (evt) {
|
|
||||||
if (workaroundActive) {
|
|
||||||
document.body.classList.remove("ios-safari-composer-hacks");
|
|
||||||
window.scrollTo(0, originalScrollTop);
|
|
||||||
evt?.target?.removeEventListener("blur", blurred);
|
|
||||||
|
|
||||||
workaroundActive = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let blurredNow = function (evt) {
|
|
||||||
// we cannot use evt.relatedTarget to get the last focused element in safari iOS
|
|
||||||
// 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 when:
|
|
||||||
// - switching to another iOS app
|
|
||||||
// - displaying title field
|
|
||||||
// - invoking a select-kit dropdown
|
|
||||||
// - invoking mentions
|
|
||||||
// - invoking emoji dropdown via : and hitting return
|
|
||||||
// - invoking a button in the editor toolbar
|
|
||||||
// - tapping on emoji in the emoji modal
|
|
||||||
// - tapping on the upload button
|
|
||||||
// - tapping on the edit reason icon/input
|
|
||||||
|
|
||||||
if (
|
|
||||||
lastTouchedElement &&
|
|
||||||
(document.visibilityState === "hidden" ||
|
|
||||||
fixedElement.classList.contains("edit-title") ||
|
|
||||||
lastTouchedElement.classList.contains("select-kit-header") ||
|
|
||||||
lastTouchedElement.closest(".autocomplete") ||
|
|
||||||
(lastTouchedElement.nodeName === "TEXTAREA" &&
|
|
||||||
document.activeElement === lastTouchedElement) ||
|
|
||||||
lastTouchedElement.closest(".d-editor-button-bar") ||
|
|
||||||
lastTouchedElement.classList.contains("emoji") ||
|
|
||||||
lastTouchedElement.closest(".mobile-file-upload") ||
|
|
||||||
lastTouchedElement.closest(".display-edit-reason"))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
positioningWorkaround.blur(evt);
|
|
||||||
};
|
|
||||||
|
|
||||||
let blurred = function (evt) {
|
|
||||||
discourseDebounce(this, blurredNow, evt, INPUT_DELAY);
|
|
||||||
};
|
|
||||||
|
|
||||||
let positioningHack = function (evt) {
|
|
||||||
if (evt === undefined) {
|
|
||||||
evt = new CustomEvent("no-op");
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need this, otherwise changing focus means we never clear
|
|
||||||
this.addEventListener("blur", blurred);
|
|
||||||
|
|
||||||
// resets focus out of select-kit elements
|
|
||||||
// might become redundant after select-kit refactoring
|
|
||||||
fixedElement
|
|
||||||
.querySelectorAll(".select-kit.is-expanded > button")
|
|
||||||
.forEach((el) => el.click());
|
|
||||||
fixedElement
|
|
||||||
.querySelectorAll(".select-kit > button.is-focused")
|
|
||||||
.forEach((el) => el.classList.remove("is-focused"));
|
|
||||||
|
|
||||||
if (window.pageYOffset > 0) {
|
|
||||||
originalScrollTop = window.pageYOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delay = caps.isIpadOS ? 350 : 150;
|
|
||||||
|
|
||||||
discourseLater(() => {
|
|
||||||
if (caps.isIpadOS) {
|
|
||||||
// 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)
|
|
||||||
let heightDiff = window.innerHeight - window.visualViewport.height;
|
|
||||||
if (heightDiff < 75) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't trigger keyboard on disabled element (happens when a category is required)
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.classList.add("ios-safari-composer-hacks");
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
evt.preventDefault();
|
|
||||||
this.focus();
|
|
||||||
workaroundActive = true;
|
|
||||||
}, delay);
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastTouched = function (evt) {
|
|
||||||
if (evt && evt.target) {
|
|
||||||
lastTouchedElement = evt.target;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function attachTouchStart(elem, fn) {
|
|
||||||
if (!$(elem).data("listening")) {
|
|
||||||
elem.addEventListener("touchstart", fn);
|
|
||||||
$(elem).data("listening", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForInputs() {
|
|
||||||
attachTouchStart(fixedElement, lastTouched);
|
|
||||||
|
|
||||||
fixedElement
|
|
||||||
.querySelectorAll("input[type=text], textarea")
|
|
||||||
.forEach((el) => {
|
|
||||||
attachTouchStart(el, positioningHack);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function debouncedCheckForInputs() {
|
|
||||||
discourseDebounce(checkForInputs, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
positioningWorkaround.touchstartEvent = function (element) {
|
|
||||||
let triggerHack = positioningHack.bind(element);
|
|
||||||
triggerHack();
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new MutationObserver(debouncedCheckForInputs);
|
|
||||||
observer.observe(fixedElement, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: false,
|
|
||||||
characterData: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default positioningWorkaround;
|
|
@ -5,6 +5,7 @@ import { modifier } from "ember-modifier";
|
|||||||
import {
|
import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
enableBodyScroll,
|
enableBodyScroll,
|
||||||
|
locks,
|
||||||
} from "discourse/lib/body-scroll-lock";
|
} from "discourse/lib/body-scroll-lock";
|
||||||
|
|
||||||
@tagName("")
|
@tagName("")
|
||||||
@ -16,20 +17,18 @@ export default class SelectKitCollection extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// when opened a modal will disable all scroll but itself
|
const isChildOfLock = locks.some((lock) =>
|
||||||
// this code is whitelisting the collection to ensure it can be scrolled in this case
|
lock.targetElement.contains(element)
|
||||||
// however we only want to do this if the modal is open to avoid breaking the scroll on the page
|
);
|
||||||
// eg: opening a combobox under a topic shouldn't prevent you to scroll the topic page
|
|
||||||
const isModalOpen =
|
if (isChildOfLock) {
|
||||||
document.documentElement.classList.contains("modal-open");
|
disableBodyScroll(element);
|
||||||
if (!isModalOpen) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disableBodyScroll(element);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (isChildOfLock) {
|
||||||
enableBodyScroll(element);
|
enableBodyScroll(element);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,18 +4,20 @@ html.composer-open {
|
|||||||
transition: padding-bottom 250ms ease;
|
transition: padding-bottom 250ms ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-control {
|
#reply-control {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
max-width: $reply-area-max-width;
|
max-width: $reply-area-max-width;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
&.hide-preview {
|
&.hide-preview {
|
||||||
max-width: 740px;
|
max-width: 740px;
|
||||||
@ -37,19 +39,14 @@ html.composer-open {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
z-index: z("composer", "content");
|
z-index: z("composer", "content");
|
||||||
transition: height 250ms ease, background 250ms ease, transform 250ms ease,
|
transition: height 0.2s, max-width 0.2s, padding-bottom 0.2s, top 0.2s,
|
||||||
max-width 250ms ease, padding-bottom 250ms ease;
|
transform 0.2s, min-height 0.2s;
|
||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
box-shadow: var(--shadow-composer);
|
box-shadow: var(--shadow-composer);
|
||||||
|
|
||||||
.reply-area {
|
.reply-area {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
&.with-form-template {
|
|
||||||
max-height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.saving-text,
|
.saving-text,
|
||||||
@ -68,11 +65,10 @@ html.composer-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
|
box-sizing: border-box;
|
||||||
height: var(--composer-height);
|
height: var(--composer-height);
|
||||||
}
|
max-height: calc(100vh - var(--header-offset, 4em));
|
||||||
|
padding-bottom: var(--composer-ipad-padding);
|
||||||
&.closed {
|
|
||||||
height: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.draft,
|
&.draft,
|
||||||
@ -104,6 +100,7 @@ html.composer-open {
|
|||||||
display: flex;
|
display: flex;
|
||||||
.draft-text {
|
.draft-text {
|
||||||
display: block;
|
display: block;
|
||||||
|
@include ellipsis;
|
||||||
}
|
}
|
||||||
.grippie,
|
.grippie,
|
||||||
.saving-text {
|
.saving-text {
|
||||||
@ -604,45 +601,6 @@ div.ac-wrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes transformer {
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body.ios-safari-composer-hacks {
|
|
||||||
#main-outlet,
|
|
||||||
header,
|
|
||||||
.grippie,
|
|
||||||
html:not(.fullscreen-composer) & .toggle-fullscreen {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#reply-control {
|
|
||||||
top: 0px;
|
|
||||||
&.open {
|
|
||||||
height: calc(var(--composer-vh, 1vh) * 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body:not(.ios-safari-composer-hacks) {
|
|
||||||
#reply-control.open {
|
|
||||||
--min-height: 255px;
|
|
||||||
min-height: var(--min-height);
|
|
||||||
max-height: calc(100vh - var(--header-offset, 4em));
|
|
||||||
&.composer-action-reply {
|
|
||||||
// we can let the reply composer get a little shorter
|
|
||||||
min-height: calc(var(--min-height) - 4em);
|
|
||||||
}
|
|
||||||
padding-bottom: var(--composer-ipad-padding);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-preview {
|
.toggle-preview {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
transition: all 0.33s ease-out;
|
transition: all 0.33s ease-out;
|
||||||
@ -659,3 +617,103 @@ body:not(.ios-safari-composer-hacks) {
|
|||||||
.draft-error {
|
.draft-error {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes blink_input_opacity_to_prevent_scrolling_when_focus {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The composer on mobile is fixed-positioned, same as on desktop because
|
||||||
|
// goes together with the interactive-widget=resizes-content in the viewport meta tag
|
||||||
|
// for maximum browser compatibility (especially Firefox and webviews)
|
||||||
|
// see https://developer.chrome.com/blog/viewport-resize-behavior for context
|
||||||
|
.ipados-device,
|
||||||
|
.mobile-device {
|
||||||
|
#reply-control {
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
z-index: z("mobile-composer");
|
||||||
|
}
|
||||||
|
|
||||||
|
&.draft,
|
||||||
|
&.saving {
|
||||||
|
z-index: z("ipad-header-nav") + 1;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-fullscreen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-panel,
|
||||||
|
.composer-fields,
|
||||||
|
.d-editor-button-bar {
|
||||||
|
// this prevents touch events (i.e. accidental scrolls) from bubbling up
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.keyboard-visible #reply-control.open {
|
||||||
|
height: calc(var(--composer-vh, 1vh) * 100);
|
||||||
|
|
||||||
|
.grippie {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.composer-open .with-topic-progress {
|
||||||
|
bottom: calc(var(--composer-height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-device {
|
||||||
|
#reply-control {
|
||||||
|
.grippie {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open.show-preview {
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ipados-device {
|
||||||
|
// this might be overkill
|
||||||
|
// but on iPad with a physical keyboard the composer is shifted up on scroll
|
||||||
|
// this adds a solid box shadow below, looks cleaner
|
||||||
|
#reply-control {
|
||||||
|
box-shadow: 0 150px 0px 0px var(--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safari in iOS/iPad does not handle well a bottom:0 fixed-positioned element,
|
||||||
|
// especially while the software keyboard is visible, so we top-anchor it here
|
||||||
|
// and shift it using transform
|
||||||
|
.ipados-device,
|
||||||
|
.ios-device {
|
||||||
|
#reply-control {
|
||||||
|
// the two properties below are equivalent to bottom: 0
|
||||||
|
top: calc(var(--composer-vh, 1vh) * 100);
|
||||||
|
transform: translateY(-100%);
|
||||||
|
bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an element (input, textearea) gets focus, iOS Safari tries to put it in the center
|
||||||
|
// by scrolling and zooming. We handle zooming with a meta tag. We used to handle scrolling
|
||||||
|
// using a complicated JS hack.
|
||||||
|
//
|
||||||
|
// However, iOS Safari doesn't scroll when the input has opacity of 0 or is clipped.
|
||||||
|
// We use this quirk and temporarily hide the element on focus
|
||||||
|
//
|
||||||
|
// Source https://gist.github.com/kiding/72721a0553fa93198ae2bb6eefaa3299
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
animation: blink_input_opacity_to_prevent_scrolling_when_focus 0.01s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -16,10 +16,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.keyboard-visible body.ios-safari-composer-hacks & {
|
|
||||||
height: calc(var(--composer-vh, 1vh) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -40,23 +40,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#topic-progress-wrapper {
|
.with-topic-progress {
|
||||||
position: fixed;
|
position: sticky;
|
||||||
&.docked {
|
bottom: calc(env(safe-area-inset-bottom) + var(--composer-height, 0px));
|
||||||
position: initial;
|
z-index: z("timeline");
|
||||||
|
}
|
||||||
|
|
||||||
|
#topic-progress-wrapper {
|
||||||
|
&.docked {
|
||||||
.toggle-admin-menu {
|
.toggle-admin-menu {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
html:not(.footer-nav-visible) & {
|
|
||||||
bottom: env(safe-area-inset-bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
right: 10px;
|
|
||||||
z-index: z("timeline");
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -65,14 +61,6 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.with-transitions {
|
|
||||||
transition: bottom 0.2s, margin-bottom 0.2s;
|
|
||||||
|
|
||||||
#topic-progress .bg {
|
|
||||||
transition: width 0.5s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.docked) {
|
&:not(.docked) {
|
||||||
@media screen and (min-width: $reply-area-max-width) {
|
@media screen and (min-width: $reply-area-max-width) {
|
||||||
right: calc(50%); // right side of composer
|
right: calc(50%); // right side of composer
|
||||||
|
@ -91,6 +91,9 @@
|
|||||||
grid-template-areas: "posts";
|
grid-template-areas: "posts";
|
||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
.topic-navigation {
|
.topic-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
grid-area: posts;
|
grid-area: posts;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -19,9 +19,13 @@ html.footer-nav-visible {
|
|||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topic-progress-wrapper:not(.docked) {
|
.with-topic-progress {
|
||||||
margin-bottom: calc(var(--footer-nav-height) + env(safe-area-inset-bottom));
|
bottom: calc(
|
||||||
|
var(--footer-nav-height) + env(safe-area-inset-bottom) +
|
||||||
|
var(--composer-height, 0px)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts-filtered-notice {
|
.posts-filtered-notice {
|
||||||
transition: all linear 0.1s;
|
transition: all linear 0.1s;
|
||||||
bottom: calc(var(--footer-nav-height) + env(safe-area-inset-bottom));
|
bottom: calc(var(--footer-nav-height) + env(safe-area-inset-bottom));
|
||||||
@ -86,11 +90,6 @@ html.footer-nav-ipad {
|
|||||||
padding-bottom: 0; // resets safe-area-inset-bottom
|
padding-bottom: 0; // resets safe-area-inset-bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-control,
|
|
||||||
#reply-control.fullscreen {
|
|
||||||
z-index: z("ipad-header-nav") + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d-header-wrap {
|
.d-header-wrap {
|
||||||
top: var(--footer-nav-height);
|
top: var(--footer-nav-height);
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#reply-control {
|
#reply-control {
|
||||||
z-index: z("mobile-composer");
|
|
||||||
|
|
||||||
.reply-area {
|
.reply-area {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
padding-bottom: unquote("max(env(safe-area-inset-bottom), 6px)");
|
padding-bottom: unquote("max(env(safe-area-inset-bottom), 6px)");
|
||||||
@ -16,19 +14,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
height: 250px;
|
z-index: z("mobile-composer");
|
||||||
&.edit-title {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyboard-visible &.open {
|
.keyboard-visible &.open .reply-area {
|
||||||
top: 0px;
|
|
||||||
height: calc(var(--composer-vh, 1vh) * 100);
|
|
||||||
.reply-area {
|
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.reply-to {
|
.reply-to {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -64,17 +55,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.draft {
|
&.draft {
|
||||||
z-index: z("footer-nav") + 1;
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
|
|
||||||
.toggle-toolbar,
|
.toggle-toolbar,
|
||||||
.toggle-minimize {
|
.toggle-minimize {
|
||||||
top: 6px;
|
top: 6px;
|
||||||
}
|
}
|
||||||
.draft-text {
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
@include ellipsis;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#reply-title {
|
#reply-title {
|
||||||
@ -143,8 +127,9 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-editor-preview-wrapper {
|
.d-editor-preview-wrapper {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
z-index: z("fullscreen");
|
z-index: z("fullscreen");
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -160,6 +145,10 @@
|
|||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hide-preview {
|
&.hide-preview {
|
||||||
|
@ -1,14 +1,3 @@
|
|||||||
.container.posts {
|
|
||||||
grid-template-areas: "posts";
|
|
||||||
.topic-navigation {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
grid-area: posts;
|
|
||||||
grid-row: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.anon) #topic-footer-buttons {
|
html:not(.anon) #topic-footer-buttons {
|
||||||
.topic-footer-main-buttons {
|
.topic-footer-main-buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -175,7 +175,6 @@ module ApplicationHelper
|
|||||||
list = []
|
list = []
|
||||||
list << (mobile_view? ? "mobile-view" : "desktop-view")
|
list << (mobile_view? ? "mobile-view" : "desktop-view")
|
||||||
list << (mobile_device? ? "mobile-device" : "not-mobile-device")
|
list << (mobile_device? ? "mobile-device" : "not-mobile-device")
|
||||||
list << "ios-device" if ios_device?
|
|
||||||
list << "rtl" if rtl?
|
list << "rtl" if rtl?
|
||||||
list << text_size_class
|
list << text_size_class
|
||||||
list << "anon" unless current_user
|
list << "anon" unless current_user
|
||||||
@ -446,10 +445,6 @@ module ApplicationHelper
|
|||||||
MobileDetection.mobile_device?(request.user_agent)
|
MobileDetection.mobile_device?(request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ios_device?
|
|
||||||
MobileDetection.ios_device?(request.user_agent)
|
|
||||||
end
|
|
||||||
|
|
||||||
def customization_disabled?
|
def customization_disabled?
|
||||||
request.env[ApplicationController::NO_THEMES]
|
request.env[ApplicationController::NO_THEMES]
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<%- end %>
|
<%- end %>
|
||||||
<%= discourse_theme_color_meta_tags %>
|
<%= discourse_theme_color_meta_tags %>
|
||||||
<%= discourse_color_scheme_meta_tag %>
|
<%= discourse_color_scheme_meta_tag %>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes, viewport-fit=cover, interactive-widget=resizes-content">
|
||||||
<%- if Discourse.base_path.present? %>
|
<%- if Discourse.base_path.present? %>
|
||||||
<meta name="discourse-base-uri" content="<%= Discourse.base_path %>">
|
<meta name="discourse-base-uri" content="<%= Discourse.base_path %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -20,10 +20,6 @@ module MobileDetection
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.ios_device?(user_agent)
|
|
||||||
user_agent =~ /iPad|iPhone|iPod/
|
|
||||||
end
|
|
||||||
|
|
||||||
MODERN_MOBILE_REGEX =
|
MODERN_MOBILE_REGEX =
|
||||||
%r{
|
%r{
|
||||||
\(.*iPhone\ OS\ 1[5-9].*\)|
|
\(.*iPhone\ OS\ 1[5-9].*\)|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<TopicPresenceDisplay @topic={{@outletArgs.model}} />
|
|
@ -1,5 +1,5 @@
|
|||||||
.topic-above-footer-buttons-outlet.presence {
|
.topic-above-footer-buttons-outlet.presence {
|
||||||
min-height: 1.8em; // height of the avatars, prevents layout shift
|
min-height: 2.5em; // height of the avatars, prevents layout shift
|
||||||
margin: var(--below-topic-margin) 0;
|
margin: var(--below-topic-margin) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,6 +7,9 @@
|
|||||||
background-color: var(--secondary);
|
background-color: var(--secondary);
|
||||||
color: var(--primary-medium);
|
color: var(--primary-medium);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 0.5em;
|
||||||
|
padding-left: 0;
|
||||||
|
border-radius: var(--d-border-radius);
|
||||||
|
|
||||||
span.presence-text {
|
span.presence-text {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -54,16 +57,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-fields .presence-users {
|
.reply-to .presence-users {
|
||||||
overflow: hidden;
|
padding: unset;
|
||||||
flex-shrink: 1;
|
|
||||||
.presence-avatars {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-view .composer-fields .presence-users .description {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TMP: RTL overrides
|
// TMP: RTL overrides
|
||||||
@ -71,18 +66,6 @@
|
|||||||
span.presence-text {
|
span.presence-text {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-fields .presence-users {
|
|
||||||
right: unset;
|
|
||||||
left: 95px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mobile-view {
|
|
||||||
.composer-fields .presence-users {
|
|
||||||
right: unset;
|
|
||||||
left: 65px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always hide the "Topic Presence" in the topic timeline
|
// Always hide the "Topic Presence" in the topic timeline
|
||||||
@ -90,14 +73,12 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the "Topic Presence" in the topic footer when the timeline is hidden
|
// When topic progress is visible in the posts grid area and is sticky,
|
||||||
body:has(.topic-navigation.with-topic-progress)
|
// adjust positioning so presence is on the same line
|
||||||
|
@media screen and (max-width: 924px) {
|
||||||
|
body:has(.topic-navigation.with-topic-progress)
|
||||||
.topic-above-footer-buttons-outlet.presence {
|
.topic-above-footer-buttons-outlet.presence {
|
||||||
display: none;
|
margin-top: -3.2em;
|
||||||
}
|
margin-right: 8em;
|
||||||
|
}
|
||||||
.topic-navigation-bottom-outlet.presence {
|
|
||||||
margin: var(--below-topic-margin) auto 0 0;
|
|
||||||
min-height: 1.8em; // height of the avatars, prevents layout shift
|
|
||||||
order: -1;
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user