UX: Apply new quote-button spacing behavior to all mobile devices (#15608)
This expands cbf99f48
to apply to all mobile devices. It removes the old mobile positioning logic entirely, refactors the new system a little for robustness and readability, and removes some JQuery.
On Andoid, we also need to avoid the start selection handle. Therefore the logic for locating selection boundaries is abstracted into a function for easier re-use.
This commit is contained in:
parent
2bf3f6d549
commit
b2d45c592a
|
@ -46,6 +46,37 @@ function regexSafeStr(str) {
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRangeBoundaryRect(range, atEnd) {
|
||||||
|
// Don't mess with the original range as it results in weird behaviours
|
||||||
|
// where certain browsers will deselect the selection
|
||||||
|
const clone = range.cloneRange(range);
|
||||||
|
|
||||||
|
// create a marker element containing a single invisible character
|
||||||
|
const markerElement = document.createElement("span");
|
||||||
|
markerElement.appendChild(document.createTextNode("\ufeff"));
|
||||||
|
|
||||||
|
// on mobile, collapse the range at the end of the selection
|
||||||
|
if (atEnd) {
|
||||||
|
clone.collapse();
|
||||||
|
}
|
||||||
|
// insert the marker
|
||||||
|
clone.insertNode(markerElement);
|
||||||
|
|
||||||
|
// retrieve the position of the marker
|
||||||
|
const boundaryRect = markerElement.getBoundingClientRect();
|
||||||
|
boundaryRect.x += document.documentElement.scrollLeft;
|
||||||
|
boundaryRect.y += document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
// remove the marker
|
||||||
|
const parent = markerElement.parentNode;
|
||||||
|
parent.removeChild(markerElement);
|
||||||
|
|
||||||
|
// merge back all text nodes so they don't get messed up
|
||||||
|
parent.normalize();
|
||||||
|
|
||||||
|
return boundaryRect;
|
||||||
|
}
|
||||||
|
|
||||||
export default Component.extend(KeyEnterEscape, {
|
export default Component.extend(KeyEnterEscape, {
|
||||||
classNames: ["quote-button"],
|
classNames: ["quote-button"],
|
||||||
classNameBindings: [
|
classNameBindings: [
|
||||||
|
@ -184,42 +215,10 @@ export default Component.extend(KeyEnterEscape, {
|
||||||
// on Desktop, shows the button at the beginning of the selection
|
// on Desktop, shows the button at the beginning of the selection
|
||||||
// on Mobile, shows the button at the end of the selection
|
// on Mobile, shows the button at the end of the selection
|
||||||
const isMobileDevice = this.site.isMobileDevice;
|
const isMobileDevice = this.site.isMobileDevice;
|
||||||
const { isIOS, isAndroid, isSafari, isOpera } = this.capabilities;
|
const { isIOS, isAndroid, isOpera } = this.capabilities;
|
||||||
const showAtEnd = isMobileDevice || isIOS || isAndroid || isOpera;
|
const showAtEnd = isMobileDevice || isIOS || isAndroid || isOpera;
|
||||||
|
|
||||||
// Don't mess with the original range as it results in weird behaviours
|
const boundaryPosition = getRangeBoundaryRect(firstRange, showAtEnd);
|
||||||
// where certain browsers will deselect the selection
|
|
||||||
const clone = firstRange.cloneRange();
|
|
||||||
|
|
||||||
// create a marker element containing a single invisible character
|
|
||||||
const markerElement = document.createElement("span");
|
|
||||||
markerElement.appendChild(document.createTextNode("\ufeff"));
|
|
||||||
|
|
||||||
// on mobile, collapse the range at the end of the selection
|
|
||||||
if (showAtEnd) {
|
|
||||||
clone.collapse();
|
|
||||||
}
|
|
||||||
// insert the marker
|
|
||||||
clone.insertNode(markerElement);
|
|
||||||
|
|
||||||
// retrieve the position of the marker
|
|
||||||
const $markerElement = $(markerElement);
|
|
||||||
const markerOffset = $markerElement.offset();
|
|
||||||
const parentScrollLeft = $markerElement.parent().scrollLeft();
|
|
||||||
const $quoteButton = $(this.element);
|
|
||||||
|
|
||||||
// remove the marker
|
|
||||||
const parent = markerElement.parentNode;
|
|
||||||
parent.removeChild(markerElement);
|
|
||||||
// merge back all text nodes so they don't get messed up
|
|
||||||
parent.normalize();
|
|
||||||
|
|
||||||
// work around Safari that would sometimes lose the selection
|
|
||||||
if (isSafari) {
|
|
||||||
this._reselected = true;
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(clone);
|
|
||||||
}
|
|
||||||
|
|
||||||
// change the position of the button
|
// change the position of the button
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
|
@ -227,37 +226,59 @@ export default Component.extend(KeyEnterEscape, {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let top = markerOffset.top;
|
let top = 0;
|
||||||
let left = markerOffset.left + Math.max(0, parentScrollLeft);
|
let left = 0;
|
||||||
if (showAtEnd) {
|
const pxFromSelection = 5;
|
||||||
top = top + 25;
|
|
||||||
left = Math.min(
|
|
||||||
left + 10,
|
|
||||||
window.innerWidth - this.element.clientWidth - 10
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
top = top - $quoteButton.outerHeight() - 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIOS) {
|
if (showAtEnd) {
|
||||||
// The selection-handles on iOS have a hit area of ~50px radius
|
// The selection-handles on iOS have a hit area of ~50px radius
|
||||||
// so we need to make sure our buttons are outside that radius
|
// so we need to make sure our buttons are outside that radius
|
||||||
|
// Apply the same logic on all mobile devices for consistency
|
||||||
|
|
||||||
|
top = boundaryPosition.bottom + pxFromSelection;
|
||||||
|
left = boundaryPosition.left;
|
||||||
|
|
||||||
const safeRadius = 50;
|
const safeRadius = 50;
|
||||||
const endHandlePosition = markerOffset;
|
const viewportEdgeMargin = 10;
|
||||||
|
|
||||||
|
const endHandlePosition = boundaryPosition;
|
||||||
const width = this.element.clientWidth;
|
const width = this.element.clientWidth;
|
||||||
|
|
||||||
const possiblePositions = [
|
const possiblePositions = [
|
||||||
{ top, left },
|
{
|
||||||
{ top, left: endHandlePosition.left - width - safeRadius - 10 }, // move to left
|
// move to left
|
||||||
{ top, left: left + safeRadius }, // move to right
|
top,
|
||||||
{ top: top + safeRadius, left: endHandlePosition.left - width / 2 }, // centered below end handle
|
left: left - width - safeRadius,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// move to right
|
||||||
|
top,
|
||||||
|
left: left + safeRadius,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// centered below end handle
|
||||||
|
top: top + safeRadius,
|
||||||
|
left: left - width / 2,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let newPos;
|
|
||||||
for (const pos of possiblePositions) {
|
for (const pos of possiblePositions) {
|
||||||
if (pos.left < 0 || pos.left + width + 10 > window.innerWidth) {
|
// Ensure buttons are entirely within the viewport
|
||||||
continue; // Offscreen
|
pos.left = Math.max(viewportEdgeMargin, pos.left);
|
||||||
|
pos.left = Math.min(
|
||||||
|
window.innerWidth - viewportEdgeMargin - width,
|
||||||
|
pos.left
|
||||||
|
);
|
||||||
|
|
||||||
|
let clearOfStartHandle = true;
|
||||||
|
if (isAndroid) {
|
||||||
|
// On android, the start-selection handle extends below the line, so we need to avoid it as well:
|
||||||
|
const startHandlePosition = getRangeBoundaryRect(firstRange, false);
|
||||||
|
|
||||||
|
clearOfStartHandle =
|
||||||
|
pos.top - startHandlePosition.bottom >= safeRadius ||
|
||||||
|
pos.left + width <= startHandlePosition.left - safeRadius ||
|
||||||
|
pos.left >= startHandlePosition.left + safeRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearOfEndHandle =
|
const clearOfEndHandle =
|
||||||
|
@ -265,19 +286,20 @@ export default Component.extend(KeyEnterEscape, {
|
||||||
pos.left + width <= endHandlePosition.left - safeRadius ||
|
pos.left + width <= endHandlePosition.left - safeRadius ||
|
||||||
pos.left >= endHandlePosition.left + safeRadius;
|
pos.left >= endHandlePosition.left + safeRadius;
|
||||||
|
|
||||||
if (clearOfEndHandle) {
|
if (clearOfStartHandle && clearOfEndHandle) {
|
||||||
newPos = pos;
|
left = pos.left;
|
||||||
|
top = pos.top;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if (newPos) {
|
// Desktop
|
||||||
left = newPos.left;
|
top =
|
||||||
top = newPos.top;
|
boundaryPosition.top - this.element.clientHeight - pxFromSelection;
|
||||||
}
|
left = boundaryPosition.left;
|
||||||
}
|
}
|
||||||
|
|
||||||
$quoteButton.offset({ top, left });
|
Object.assign(this.element.style, { top: `${top}px`, left: `${left}px` });
|
||||||
|
|
||||||
if (!this.animated) {
|
if (!this.animated) {
|
||||||
// We only enable CSS transitions after the initial positioning
|
// We only enable CSS transitions after the initial positioning
|
||||||
|
|
Loading…
Reference in New Issue