Skip to content
Merged
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 packages/EdgeTranslate/src/common/scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const DEFAULT_SETTINGS = {
RTL: false,
FoldLongContent: true,
AutoClosePanelOnPageScroll: true,
SelectTranslatePosition: "TopRight",
SelectTranslatePosition: "AutoAvoid",
},
// Default settings of source language and target language
languageSetting: { sl: "auto", tl: BROWSER_LANGUAGES_MAP[chrome.i18n.getUILanguage()] },
Expand Down
11 changes: 9 additions & 2 deletions packages/EdgeTranslate/src/content/select/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,11 @@ function initSelectTranslate() {

function createSelectState() {
return {
buttonPositionSetting: "TopRight",
buttonPositionSetting: "AutoAvoid",
buttonAnchor: null,
buttonPlacementDirection: null,
buttonSelection: null,
buttonShownAt: null,
channel: new Channel(),
hasButtonShown: false,
longPressEnabled: false,
Expand All @@ -59,6 +62,7 @@ function createSelectState() {
originScrollX: 0,
originScrollY: 0,
screenshotSelector: createScreenshotSelector(),
selectionActionGeneration: 0,
scrollPropertyX: "pageXOffset",
scrollPropertyY: "pageYOffset",
scrollingElement: window,
Expand Down Expand Up @@ -102,6 +106,7 @@ function initializeDomListeners(state) {
}
state.scrollingElement.addEventListener("scroll", () => scrollHandler(state));
document.addEventListener("mousedown", () => {
state.selectionActionGeneration += 1;
disappearButton(state);
detectSelect(document, (event) => selectTranslate(state, event));
});
Expand All @@ -119,9 +124,11 @@ function initializeDomListeners(state) {

function selectTranslate(state, event, isDoubleClick = false) {
if (!shouldTranslate()) return;
const actionGeneration = ++state.selectionActionGeneration;
isInBlacklist().then((inBlacklist) => {
if (inBlacklist) return;
if (inBlacklist || actionGeneration !== state.selectionActionGeneration) return;
getOrSetDefaultSettings("OtherSettings", DEFAULT_SETTINGS).then((result) => {
if (actionGeneration !== state.selectionActionGeneration) return;
const otherSettings = result.OtherSettings;
if (!otherSettings) return;
if (
Expand Down
274 changes: 62 additions & 212 deletions packages/EdgeTranslate/src/content/select/select_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@ import { IMAGE_DATA } from "./select_constants.js";
import {
applyButtonImageStyle,
applyButtonStyle,
getButtonPosition,
getInnerParent,
getSelection,
} from "./select_helpers.js";
import { cancelAutoAvoidRechecks, scheduleAutoAvoidRechecks } from "./select_button_auto_avoid.js";
import { getButtonAnchor } from "./select_button_anchor.js";
import { closeButtonHost, createButtonHost, showButtonLayer } from "./select_button_layer.js";
import { getButtonPlacement } from "./select_button_position.js";
import {
clearPendingDirectionChange,
isDuplicateButtonRequest,
shiftButtonAnchor,
shouldDeferDirectionChange,
} from "./select_button_session.js";

const BUTTON_HOST_ID = "edge-translate-button-host";
const BUTTON_ID = "edge-translate-button";
const PANEL_ROOT_ID = "edge-translate-root";
const SCREENSHOT_OVERLAY_ID = "edge-translate-screenshot-overlay";
const BACKDROP_STYLE_ID = "edge-translate-button-host-backdrop-style";

const LayerMode = {
Modal: "modal",
Normal: "normal",
Popover: "popover",
};

export function initializeButtonContainer(state, onMouseDown) {
const iframeContainer = state.translationButtonContainer;
Expand All @@ -31,45 +30,82 @@ export function initializeButtonContainer(state, onMouseDown) {
state.translationButtonContainer.id = BUTTON_ID;
state.translationButtonContainer.style.backgroundColor = "white";
state.translationButtonContainer.style.pointerEvents = "auto";
state.translationButtonHost = createButtonHost(state);
state.translationButtonHost = createButtonHost(state, () => disappearButton(state));
state.translationButtonContainer.addEventListener("load", () =>
renderButton(state, onMouseDown)
);
}

export function showButton(state, event) {
state.buttonSelection = getSelection();
const selection = getSelection();
const anchor = getButtonAnchor(event);
if (isDuplicateButtonRequest(state, selection, anchor)) return;

state.buttonSelection = selection;
state.buttonAnchor = anchor;
state.buttonPlacementDirection = null;
state.buttonShownAt = Date.now();
showButtonLayer(state);
const position = getButtonPosition(
state.buttonPositionSetting,
state.translationButtonContainer,
event
);
state.translationButtonContainer.style.top = `${position.top}px`;
state.translationButtonContainer.style.left = `${position.left}px`;
state.originScrollX = state.scrollingElement[state.scrollPropertyX];
state.originScrollY = state.scrollingElement[state.scrollPropertyY];
state.originPositionX = position.left;
state.originPositionY = position.top;
positionButton(state);
state.hasButtonShown = true;
scheduleAutoAvoidRechecks(state, () => {
showButtonLayer(state);
positionButton(state, { isRecheck: true });
});
}

export function scrollHandler(state) {
if (!state.hasButtonShown) return;
const distanceX = state.originScrollX - state.scrollingElement[state.scrollPropertyX];
const distanceY = state.originScrollY - state.scrollingElement[state.scrollPropertyY];
state.translationButtonContainer.style.left = `${state.originPositionX + distanceX}px`;
state.translationButtonContainer.style.top = `${state.originPositionY + distanceY}px`;
const left = state.originPositionX + distanceX;
const top = state.originPositionY + distanceY;
state.translationButtonContainer.style.left = `${left}px`;
state.translationButtonContainer.style.top = `${top}px`;
shiftButtonAnchor(state.buttonAnchor, distanceX, distanceY);
state.originScrollX = state.scrollingElement[state.scrollPropertyX];
state.originScrollY = state.scrollingElement[state.scrollPropertyY];
state.originPositionX = left;
state.originPositionY = top;
}

export function disappearButton(state) {
if (!state.hasButtonShown) return;
cancelAutoAvoidRechecks(state);
closeButtonHost(state.translationButtonHost);
if (document.documentElement.contains(state.translationButtonContainer)) {
document.documentElement.removeChild(state.translationButtonContainer);
}
state.hasButtonShown = false;
state.buttonAnchor = null;
state.buttonPlacementDirection = null;
state.buttonSelection = null;
state.buttonShownAt = null;
}

function positionButton(state, { isRecheck = false } = {}) {
const placement = getButtonPlacement(
state.buttonPositionSetting,
state.translationButtonContainer,
state.buttonAnchor,
{ currentDirection: state.buttonPlacementDirection }
);
if (
shouldDeferDirectionChange(state, placement, isRecheck, () =>
positionButton(state, { isRecheck: true })
)
) {
return;
}

clearPendingDirectionChange(state);
state.buttonPlacementDirection = placement.direction;
state.translationButtonContainer.style.top = `${placement.top}px`;
state.translationButtonContainer.style.left = `${placement.left}px`;
state.originScrollX = state.scrollingElement[state.scrollPropertyX];
state.originScrollY = state.scrollingElement[state.scrollPropertyY];
state.originPositionX = placement.left;
state.originPositionY = placement.top;
}

function renderButton(state, onMouseDown) {
Expand All @@ -90,189 +126,3 @@ function renderButton(state, onMouseDown) {
translationButton.addEventListener("mousedown", onMouseDown);
translationButton.addEventListener("contextmenu", (event) => event.preventDefault());
}

function createButtonHost(state) {
const host = document.createElement("dialog");
host.id = BUTTON_HOST_ID;
host.popover = "manual";
host.dataset.edgeTranslateLayerMode = LayerMode.Normal;
Object.assign(host.style, {
position: "fixed",
display: "block",
top: 0,
left: 0,
zIndex: 2147483647,
border: "none",
padding: 0,
margin: 0,
background: "transparent",
width: 0,
height: 0,
minWidth: 0,
minHeight: 0,
maxWidth: "none",
maxHeight: "none",
pointerEvents: "none",
overflow: "visible",
});
host.addEventListener("cancel", (event) => {
event.preventDefault();
disappearButton(state);
});
host.addEventListener("mousedown", (event) => dismissFromHost(event, state), true);
ensureTransparentBackdropStyle();
return host;
}

function showButtonLayer(state) {
const host = state.translationButtonHost;
if (hasModalTopLayerBlocker(host) || hasOpenPopover(host)) {
showButtonHost(state);
return;
}

showNormalButton(state);
}

function showNormalButton(state) {
closeButtonHost(state.translationButtonHost);
if (!document.documentElement.contains(state.translationButtonContainer)) {
document.documentElement.appendChild(state.translationButtonContainer);
}
}

function showButtonHost(state) {
const host = state.translationButtonHost;
if (!host.contains(state.translationButtonContainer)) {
host.appendChild(state.translationButtonContainer);
}
if (!document.documentElement.contains(host)) {
document.documentElement.appendChild(host);
}

if (hasModalTopLayerBlocker(host)) {
showPopoverButtonHost(state);
return;
}
if (hasOpenPopover(host)) {
showPopoverButtonHost(state);
return;
}
showNormalButton(state);
}

function showPopoverButtonHost(state) {
const host = state.translationButtonHost;
if (typeof host.showPopover !== "function") {
showNormalButton(state);
return;
}

if (getLayerMode(host) === LayerMode.Modal) closeDialog(host);
try {
host.showPopover();
setLayerMode(host, LayerMode.Popover);
} catch {
showNormalButton(state);
}
}

function closeButtonHost(host) {
if (!host) return;
if (getLayerMode(host) === LayerMode.Popover) hidePopover(host);
if (host.open) closeDialog(host);
setLayerMode(host, LayerMode.Normal);
if (document.documentElement.contains(host)) {
document.documentElement.removeChild(host);
}
}

function hasModalTopLayerBlocker(host) {
if (document.fullscreenElement) return true;

return Array.from(document.querySelectorAll("dialog")).some(
(dialog) => !isExtensionLayer(dialog, host) && isModalDialog(dialog)
);
}

function hasOpenPopover(host) {
return Array.from(document.querySelectorAll("[popover]")).some(
(element) => !isExtensionLayer(element, host) && isPopoverOpen(element)
);
}

function isModalDialog(dialog) {
try {
return dialog.matches(":modal");
} catch {
return dialog.open;
}
}

function isPopoverOpen(element) {
try {
return element.matches(":popover-open");
} catch {
return false;
}
}

function isExtensionLayer(element, host) {
return (
element === host ||
element.id === BUTTON_HOST_ID ||
element.id === BUTTON_ID ||
element.id === PANEL_ROOT_ID ||
element.id === SCREENSHOT_OVERLAY_ID
);
}

function closeDialog(host) {
if (typeof host.close === "function") {
try {
host.close();
return;
} catch {
// Fall back to removing the open attribute below.
}
}

host.removeAttribute("open");
}

function hidePopover(host) {
if (typeof host.hidePopover !== "function") return;

try {
host.hidePopover();
} catch {
// The host may already be hidden or the browser may not support popovers.
}
}

function getLayerMode(host) {
return host.dataset.edgeTranslateLayerMode || LayerMode.Normal;
}

function setLayerMode(host, mode) {
host.dataset.edgeTranslateLayerMode = mode;
host.style.pointerEvents = "none";
}

function dismissFromHost(event, state) {
if (event.target !== state.translationButtonHost) return;

event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation?.();
disappearButton(state);
}

function ensureTransparentBackdropStyle() {
if (document.getElementById(BACKDROP_STYLE_ID)) return;

const style = document.createElement("style");
style.id = BACKDROP_STYLE_ID;
style.textContent = `#${BUTTON_HOST_ID}::backdrop { background: transparent; }`;
(document.head || document.documentElement).appendChild(style);
}
Loading
Loading