diff --git a/.mapping.json b/.mapping.json
index df929d8..1c66f4b 100644
--- a/.mapping.json
+++ b/.mapping.json
@@ -419,6 +419,7 @@
"src/utils/dom/__tests__/nodeName.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/nodeName.spec.ts",
"src/utils/dom/__tests__/nodeText.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/nodeText.spec.ts",
"src/utils/dom/__tests__/waitForBody.spec.ts":"metrika/frontend/watch/public/src/utils/dom/__tests__/waitForBody.spec.ts",
+ "src/utils/dom/block.ts":"metrika/frontend/watch/public/src/utils/dom/block.ts",
"src/utils/dom/button.ts":"metrika/frontend/watch/public/src/utils/dom/button.ts",
"src/utils/dom/closest.ts":"metrika/frontend/watch/public/src/utils/dom/closest.ts",
"src/utils/dom/dom.ts":"metrika/frontend/watch/public/src/utils/dom/dom.ts",
diff --git a/features.json b/features.json
index 26edcd9..d98a53e 100644
--- a/features.json
+++ b/features.json
@@ -193,6 +193,10 @@
"path": "remoteControl",
"desc": "Enables metrika inpage scripts work (i.g. selecting buttons for quick goals in the metrika interface)"
},
+ {
+ "code": "REMOTE_CONTROL_BLOCK_HELPERS_FEATURE",
+ "desc": "Enables metrika block selection helpers for inpage scripts to work"
+ },
{
"code": "CSRF_TOKEN_FEATURE",
"desc": "Add API token required for authorized traffic",
diff --git a/src/providers/remoteControl/remoteControl.ts b/src/providers/remoteControl/remoteControl.ts
index e483d1d..d7c6b24 100644
--- a/src/providers/remoteControl/remoteControl.ts
+++ b/src/providers/remoteControl/remoteControl.ts
@@ -13,6 +13,7 @@ import {
SUBMIT_TRACKING_FEATURE,
CHECK_STATUS_FEATURE,
REMOTE_CONTROL_FEATURE,
+ REMOTE_CONTROL_BLOCK_HELPERS_FEATURE,
} from 'generated/features';
import {
memo,
@@ -34,6 +35,11 @@ import { closestForm, getFormData, selectForms } from 'src/utils/dom/form';
import { checkStatusFn } from 'src/providers/statusCheck/statusCheckFn';
import { isNumber, parseDecimalInt } from 'src/utils/number';
import { AnyFunc } from 'src/utils/function/types';
+import {
+ getClosestTextContainer,
+ getTextContainerData,
+ selectTextContainer,
+} from 'src/utils/dom/block';
/* eslint-disable camelcase */
type ExtendedWindow = Window & {
@@ -144,6 +150,8 @@ export type Message = {
inpageMode: string;
/** Feature init data */
initMessage: string;
+ /** Are block utils needed */
+ isBlockUtilsEnabled?: boolean;
} & InlineMessageProps;
export const UTILS_KEY = '_u';
@@ -200,6 +208,7 @@ export const setupUtilsAndLoadScript = (
ctx: ExtendedWindow,
src?: string,
counterId = '',
+ isBlockUtilsEnabled?: boolean,
) => {
if (
flags[CLICK_TRACKING_FEATURE] ||
@@ -221,6 +230,19 @@ export const setupUtilsAndLoadScript = (
[UTILS_GET_DATA_KEY]: bindArg(ctx, getFormData),
};
}
+ if (
+ flags[REMOTE_CONTROL_BLOCK_HELPERS_FEATURE] &&
+ (flags[REMOTE_CONTROL_FEATURE] ||
+ flags[CLICK_TRACKING_FEATURE] ||
+ flags[SUBMIT_TRACKING_FEATURE]) &&
+ isBlockUtilsEnabled
+ ) {
+ utils['block'] = {
+ [UTILS_CLOSEST_KEY]: bindArg(ctx, getClosestTextContainer),
+ [UTILS_SELECT_KEY]: selectTextContainer,
+ [UTILS_GET_DATA_KEY]: bindArg(ctx, getTextContainerData),
+ };
+ }
if (flags[CLICK_TRACKING_FEATURE] || flags[REMOTE_CONTROL_FEATURE]) {
utils['button'] = {
[UTILS_CLOSEST_KEY]: bindArg(ctx, closestButton),
@@ -256,7 +278,12 @@ export const handleMessage = memo(
if (message['inline']) {
const src = getResourceUrl(ctx, message);
const { id = '' } = message;
- setupUtilsAndLoadScript(ctx, src, id);
+ setupUtilsAndLoadScript(
+ ctx,
+ src,
+ id,
+ message['isBlockUtilsEnabled'],
+ );
} else if (
message['resource'] &&
isAllowedResource(message['resource'])
diff --git a/src/utils/dom/__tests__/element.spec.ts b/src/utils/dom/__tests__/element.spec.ts
index 4ae8fd6..fb42f8c 100644
--- a/src/utils/dom/__tests__/element.spec.ts
+++ b/src/utils/dom/__tests__/element.spec.ts
@@ -1,27 +1,65 @@
-import { getElementPath } from 'src/utils/dom';
+import { getElementPath, getElementCSSSelector } from 'src/utils/dom';
import { JSDOMWrapper } from 'src/__tests__/utils/jsdom';
import chai from 'chai';
describe('Element', () => {
- const { window } = new JSDOMWrapper(`
-
-
- Hello!
-
- `);
+ describe('getElementPath', () => {
+ const { window } = new JSDOMWrapper(`
+
+
+ Hello!
+
+ `);
- it('get path', () => {
- const el = window.document.querySelector('#id') as HTMLElement;
+ it('get path', () => {
+ const el = window.document.querySelector('#id') as HTMLElement;
- chai.expect(getElementPath(window, el)).eq(' {
+ const el = window.document.querySelector('#id') as HTMLElement;
+ const ignored = window.document.querySelector(
+ '#ignored',
+ ) as HTMLElement;
+
+ chai.expect(getElementPath(window, el, ignored)).eq(' {
- const el = window.document.querySelector('#id') as HTMLElement;
- const ignored = window.document.querySelector(
- '#ignored',
- ) as HTMLElement;
+ describe('getElementCSSSelector', () => {
+ it('returns null if it is impossible to get unique selector', () => {
+ const { window } = new JSDOMWrapper(`
+
+ `);
+
+ const element = window.document.querySelector(
+ '.non-unique',
+ ) as HTMLElement;
+ const result = getElementCSSSelector(window, element);
+ chai.expect(result).to.be.null;
+ });
- chai.expect(getElementPath(window, el, ignored)).eq(' {
+ const { window } = new JSDOMWrapper(`
+
+ `);
+ const element = window.document.querySelector(
+ '#unique .non-unique',
+ ) as HTMLElement;
+ const result = getElementCSSSelector(window, element);
+ chai.expect(result).to.equal('#unique .non-unique');
+ });
});
});
diff --git a/src/utils/dom/block.ts b/src/utils/dom/block.ts
new file mode 100644
index 0000000..5520257
--- /dev/null
+++ b/src/utils/dom/block.ts
@@ -0,0 +1,15 @@
+import { bindArg } from '../function';
+import { closest } from './closest';
+import { CSS, PATH, getData } from './identifiers';
+import { select } from './select';
+
+const BLOCK_SELECTOR =
+ 'div,span,main,section,p,b,h1,h2,h3,h4,h5,h6,td,small,a,i,td,li,q';
+
+export const getClosestTextContainer = bindArg(BLOCK_SELECTOR, closest);
+export const selectTextContainer = bindArg(BLOCK_SELECTOR, select);
+export const getTextContainerData = (
+ ctx: Window,
+ form: HTMLElement,
+ ignored?: HTMLElement,
+) => getData(ctx, form, [PATH, CSS], undefined, ignored);
diff --git a/src/utils/dom/element.ts b/src/utils/dom/element.ts
index 3c921b0..220a6d0 100644
--- a/src/utils/dom/element.ts
+++ b/src/utils/dom/element.ts
@@ -23,6 +23,8 @@ import {
hasClass,
} from './dom';
import { isRemovedFromDoc } from './isRemovedFromDoc';
+import { arrayFrom } from '../array/arrayFrom';
+import { select } from './select';
/* eslint-disable */
export const getElementXY = (ctx: Window, el: HTMLElement | null) => {
@@ -141,6 +143,55 @@ export const getCachedTags = memo(() => {
return cacheTags;
});
+export const getElementCSSSelector = (ctx: Window, el: HTMLElement | null) => {
+ let selector = '';
+ let element = el;
+ const body = getBody(ctx)!;
+ let elementSelector: string[] = [];
+ const getFullSelector = () => {
+ const lastComponentSelector = arrayJoin('', elementSelector);
+ return selector
+ ? `${lastComponentSelector} ${selector}`
+ : lastComponentSelector;
+ };
+ const getUniqueSelector = () => {
+ const fullSelector = getFullSelector();
+ const selectedNodes = select(fullSelector, body);
+ if (selectedNodes.length === 1) {
+ return fullSelector;
+ }
+ return null;
+ };
+
+ while (element && element.parentElement) {
+ if (element.id) {
+ elementSelector.push(`#${element.id}`);
+ const uniqueSelector = getUniqueSelector();
+ if (uniqueSelector) {
+ return uniqueSelector;
+ }
+ }
+ if (element.classList.length) {
+ cForEach(
+ (className) => elementSelector.push(`.${className}`),
+ arrayFrom(element.classList),
+ );
+ const uniqueSelector = getUniqueSelector();
+ if (uniqueSelector) {
+ return uniqueSelector;
+ }
+ }
+
+ if (elementSelector.length) {
+ selector = getFullSelector();
+ }
+ elementSelector = [];
+ element = element.parentElement;
+ }
+
+ return null;
+};
+
export const getElementPath = (
ctx: Window,
el: HTMLElement | null,
diff --git a/src/utils/dom/identifiers.ts b/src/utils/dom/identifiers.ts
index 71e047b..92e708c 100644
--- a/src/utils/dom/identifiers.ts
+++ b/src/utils/dom/identifiers.ts
@@ -1,6 +1,9 @@
import { cReduce, cSome } from 'src/utils/array';
import { ctxPath, getPath } from 'src/utils/object';
-import { getElementPathCached } from 'src/utils/dom/element';
+import {
+ getElementCSSSelector,
+ getElementPathCached,
+} from 'src/utils/dom/element';
import { convertToString } from 'src/utils/string';
import { trimText } from 'src/utils/string/remove';
import { isInputElement } from 'src/utils/dom/dom';
@@ -9,6 +12,7 @@ import {
CLICK_TRACKING_FEATURE,
LOCAL_FEATURE,
PREPROD_FEATURE,
+ REMOTE_CONTROL_BLOCK_HELPERS_FEATURE,
REMOTE_CONTROL_FEATURE,
SUBMIT_TRACKING_FEATURE,
} from 'generated/features';
@@ -22,8 +26,9 @@ export const PATH = 'p';
export const CONTENT = 'c';
export const HREF = 'h';
export const TYPE = 'ty';
+export const CSS = 'cs';
-const IDENTIFIERS = [ID, NAME, HREF, PATH, CONTENT, TYPE] as const;
+const IDENTIFIERS = [ID, NAME, HREF, PATH, CONTENT, TYPE, CSS] as const;
export type Identifier = typeof IDENTIFIERS[number];
type GenericGetter = (ctx: Window, element: HTMLElement) => string | null;
@@ -84,6 +89,15 @@ if (
GETTERS_MAP[PATH] = getElementPathCached;
}
+if (
+ flags[REMOTE_CONTROL_BLOCK_HELPERS_FEATURE] &&
+ (flags[REMOTE_CONTROL_FEATURE] ||
+ flags[CLICK_TRACKING_FEATURE] ||
+ flags[SUBMIT_TRACKING_FEATURE])
+) {
+ GETTERS_MAP[CSS] = getElementCSSSelector;
+}
+
if (flags[CLICK_TRACKING_FEATURE] || flags[REMOTE_CONTROL_FEATURE]) {
GETTERS_MAP[CONTENT] = (ctx, element, selectFn) => {
let result = trimText(getPath(element, 'textContent'));