mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-11 19:13:51 +00:00
feat(editor): 新增 alwaysMultiSelect 配置开启常驻多选模式
新增编辑器配置项 alwaysMultiSelect(默认 false),开启后无需按住 Ctrl/Meta 键,组件树与画布点击即多选;当 disabledMultiSelect=true 时本配置失效。同步 在 stage 层 ActionManager 暴露 setAlwaysMultiSelect 方法用于运行时切换,并 补充组件树/服务/画布的状态联动、文档与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2475a4f901
commit
aab73249d1
@ -3,7 +3,7 @@
|
||||
## get
|
||||
|
||||
- **参数:**
|
||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect'} name`
|
||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
||||
|
||||
- **返回:**
|
||||
- `{any} value`
|
||||
@ -36,6 +36,8 @@
|
||||
|
||||
'disabledMultiSelect': 是否禁用多选
|
||||
|
||||
'alwaysMultiSelect': 是否始终启用多选模式(无需按住 Ctrl/Meta)
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
@ -46,7 +48,7 @@ const node = editorService.get("node");
|
||||
|
||||
## set
|
||||
|
||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect'} name`
|
||||
- `{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name`
|
||||
- `{any} value`
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -1043,7 +1043,26 @@ const updateDragEl = (el, target) => {
|
||||
<m-editor :disabled-multi-select="true"></m-editor>
|
||||
</template>
|
||||
```
|
||||
|
||||
|
||||
## alwaysMultiSelect
|
||||
|
||||
- **详情:**
|
||||
|
||||
始终启用多选模式:开启后无需按住 `Ctrl/Meta` 键,组件树和画布上点击即多选。
|
||||
当 [`disabledMultiSelect`](#disabledmultiselect) 为 `true` 时本配置失效。
|
||||
|
||||
- **类型:** `boolean`
|
||||
|
||||
- **默认值:** `false`
|
||||
|
||||
- **示例:**
|
||||
|
||||
```html
|
||||
<template>
|
||||
<m-editor :always-multi-select="true"></m-editor>
|
||||
</template>
|
||||
```
|
||||
|
||||
## guidesOptions
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -113,6 +113,13 @@
|
||||
- **类型**:`() => void`
|
||||
- **详情**:启用多选能力
|
||||
|
||||
## setAlwaysMultiSelect
|
||||
|
||||
- **类型**:`(value: boolean) => void`
|
||||
- **参数**:
|
||||
- `value`:是否始终启用多选模式(无需按住 `Ctrl/Meta` 键)
|
||||
- **详情**:设置是否始终启用多选模式。当多选被 `disableMultiSelect` 禁用时,本方法不会启用多选
|
||||
|
||||
## reloadIframe
|
||||
|
||||
- **类型**:`(url: string) => void`
|
||||
|
||||
@ -219,6 +219,7 @@ const stageOptions: StageOptions = {
|
||||
renderType: props.renderType,
|
||||
guidesOptions: props.guidesOptions,
|
||||
disabledMultiSelect: props.disabledMultiSelect,
|
||||
alwaysMultiSelect: props.alwaysMultiSelect,
|
||||
beforeDblclick: props.beforeDblclick,
|
||||
};
|
||||
|
||||
|
||||
@ -77,6 +77,11 @@ export interface EditorProps {
|
||||
guidesOptions?: Partial<GuidesOptions>;
|
||||
/** 禁止多选 */
|
||||
disabledMultiSelect?: boolean;
|
||||
/**
|
||||
* 始终启用多选模式:开启后无需按住 Ctrl/Meta,点击即多选;
|
||||
* 默认 false。当 `disabledMultiSelect` 为 true 时本配置失效
|
||||
*/
|
||||
alwaysMultiSelect?: boolean;
|
||||
/** 禁用页面片 */
|
||||
disabledPageFragment?: boolean;
|
||||
/** 禁用双击在浮层中单独编辑选中组件 */
|
||||
@ -127,6 +132,7 @@ export interface EditorProps {
|
||||
export const defaultEditorProps = {
|
||||
renderType: RenderType.IFRAME,
|
||||
disabledMultiSelect: false,
|
||||
alwaysMultiSelect: false,
|
||||
disabledPageFragment: false,
|
||||
disabledStageOverlay: false,
|
||||
containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
|
||||
|
||||
@ -46,6 +46,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
updateDragEl: stageOptions.updateDragEl,
|
||||
guidesOptions: stageOptions.guidesOptions,
|
||||
disabledMultiSelect: stageOptions.disabledMultiSelect,
|
||||
alwaysMultiSelect: stageOptions.alwaysMultiSelect,
|
||||
disabledRule: stageOptions.disabledRule,
|
||||
});
|
||||
|
||||
@ -60,6 +61,13 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => editorService.get('alwaysMultiSelect'),
|
||||
(alwaysMultiSelect) => {
|
||||
stage.setAlwaysMultiSelect(Boolean(alwaysMultiSelect));
|
||||
},
|
||||
);
|
||||
|
||||
const hGuidesCache = getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY));
|
||||
const vGuidesCache = getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY));
|
||||
|
||||
|
||||
@ -72,6 +72,16 @@ export const initServiceState = (
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.alwaysMultiSelect,
|
||||
(alwaysMultiSelect) => {
|
||||
editorService.set('alwaysMultiSelect', alwaysMultiSelect || false);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.componentGroupList,
|
||||
(componentGroupList) => componentGroupList && componentListService.setList(componentGroupList),
|
||||
|
||||
@ -2,7 +2,7 @@ import { computed, type ComputedRef, nextTick, type Ref, type ShallowRef } from
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import { Id, MNode } from '@tmagic/core';
|
||||
import { isPage, isPageFragment } from '@tmagic/utils';
|
||||
import { getElById, isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import type { LayerNodeStatus, Services, TreeNodeData } from '@editor/type';
|
||||
import { UI_SELECT_MODE_EVENT_NAME } from '@editor/utils/const';
|
||||
@ -16,7 +16,9 @@ export const useClick = (
|
||||
nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
|
||||
menuRef: ShallowRef<InstanceType<typeof LayerMenu> | null>,
|
||||
) => {
|
||||
const isMultiSelect = computed(() => isCtrlKeyDown.value && !editorService.get('disabledMultiSelect'));
|
||||
const isMultiSelect = computed(
|
||||
() => !editorService.get('disabledMultiSelect') && (isCtrlKeyDown.value || editorService.get('alwaysMultiSelect')),
|
||||
);
|
||||
|
||||
// 触发画布选中
|
||||
const select = async (data: MNode) => {
|
||||
@ -81,6 +83,19 @@ export const useClick = (
|
||||
stageOverlayService.get('stage')?.highlight(data.id);
|
||||
};
|
||||
|
||||
const isNodeCanSelect = async (data: TreeNodeData) => {
|
||||
const canSelect = stageOverlayService.get('stageOptions')?.canSelect;
|
||||
if (!canSelect) return true;
|
||||
|
||||
const doc = editorService.get('stage')?.renderer?.contentWindow?.document;
|
||||
if (!doc) return true;
|
||||
|
||||
const el = getElById()(doc, data.id);
|
||||
if (!el) return true;
|
||||
|
||||
return Boolean(await canSelect(el));
|
||||
};
|
||||
|
||||
const nodeClickHandler = (event: MouseEvent, data: TreeNodeData): void => {
|
||||
if (!nodeStatusMap?.value) return;
|
||||
|
||||
@ -95,7 +110,9 @@ export const useClick = (
|
||||
});
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(async () => {
|
||||
if (!(await isNodeCanSelect(data))) return;
|
||||
|
||||
select(data);
|
||||
});
|
||||
};
|
||||
|
||||
@ -79,6 +79,7 @@ class Editor extends BaseService {
|
||||
pageLength: 0,
|
||||
pageFragmentLength: 0,
|
||||
disabledMultiSelect: false,
|
||||
alwaysMultiSelect: false,
|
||||
});
|
||||
private isHistoryStateChange = false;
|
||||
private selectionBeforeOp: Id[] | null = null;
|
||||
|
||||
@ -181,6 +181,11 @@ export interface StageOptions {
|
||||
renderType?: RenderType;
|
||||
guidesOptions?: Partial<GuidesOptions>;
|
||||
disabledMultiSelect?: boolean;
|
||||
/**
|
||||
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
|
||||
* 当 `disabledMultiSelect` 为 true 时本配置失效。
|
||||
*/
|
||||
alwaysMultiSelect?: boolean;
|
||||
disabledRule?: boolean;
|
||||
zoom?: number;
|
||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||
@ -200,6 +205,8 @@ export interface StoreState {
|
||||
pageLength: number;
|
||||
pageFragmentLength: number;
|
||||
disabledMultiSelect: boolean;
|
||||
/** 是否始终启用多选模式(无需按住 Ctrl/Meta) */
|
||||
alwaysMultiSelect: boolean;
|
||||
}
|
||||
|
||||
export type StoreStateKey = keyof StoreState;
|
||||
|
||||
@ -127,6 +127,23 @@ describe('get', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiSelect 标志位', () => {
|
||||
test('disabledMultiSelect 默认值为 false', () => {
|
||||
expect(editorService.get('disabledMultiSelect')).toBe(false);
|
||||
});
|
||||
|
||||
test('alwaysMultiSelect 默认值为 false', () => {
|
||||
expect(editorService.get('alwaysMultiSelect')).toBe(false);
|
||||
});
|
||||
|
||||
test('alwaysMultiSelect 可被 set 修改并通过 get 读取', () => {
|
||||
editorService.set('alwaysMultiSelect', true);
|
||||
expect(editorService.get('alwaysMultiSelect')).toBe(true);
|
||||
editorService.set('alwaysMultiSelect', false);
|
||||
expect(editorService.get('alwaysMultiSelect')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNodeInfo', () => {
|
||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||
|
||||
|
||||
@ -93,6 +93,8 @@ export default class ActionManager extends EventEmitter {
|
||||
private canDropIn?: CanDropIn;
|
||||
private getRenderDocument: GetRenderDocument;
|
||||
private disabledMultiSelect = false;
|
||||
/** 始终启用多选模式(无需按住 Ctrl/Meta),优先级低于 disabledMultiSelect */
|
||||
private alwaysMultiSelect = false;
|
||||
private config: ActionManagerConfig;
|
||||
|
||||
private mouseMoveHandler = throttle((event: MouseEvent): void => {
|
||||
@ -123,6 +125,7 @@ export default class ActionManager extends EventEmitter {
|
||||
this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration;
|
||||
this.containerHighlightType = config.containerHighlightType;
|
||||
this.disabledMultiSelect = config.disabledMultiSelect ?? false;
|
||||
this.alwaysMultiSelect = config.alwaysMultiSelect ?? false;
|
||||
this.getTargetElement = config.getTargetElement;
|
||||
this.getElementsFromPoint = config.getElementsFromPoint;
|
||||
this.canSelect = config.canSelect || ((el: HTMLElement) => Boolean(getIdFromEl()(el)));
|
||||
@ -134,6 +137,9 @@ export default class ActionManager extends EventEmitter {
|
||||
|
||||
if (!this.disabledMultiSelect) {
|
||||
this.multiDr = this.createMultiDr(config);
|
||||
if (this.alwaysMultiSelect) {
|
||||
this.isMultiSelectStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.highlightLayer = new StageHighlight({
|
||||
@ -148,6 +154,7 @@ export default class ActionManager extends EventEmitter {
|
||||
|
||||
public disableMultiSelect() {
|
||||
this.disabledMultiSelect = true;
|
||||
this.isMultiSelectStatus = false;
|
||||
if (this.multiDr) {
|
||||
this.multiDr.destroy();
|
||||
this.multiDr = null;
|
||||
@ -160,6 +167,20 @@ export default class ActionManager extends EventEmitter {
|
||||
if (!this.multiDr) {
|
||||
this.multiDr = this.createMultiDr(this.config);
|
||||
}
|
||||
|
||||
if (this.alwaysMultiSelect) {
|
||||
this.isMultiSelectStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否始终启用多选模式(无需按住 Ctrl/Meta),
|
||||
* 当 `disabledMultiSelect` 为 true 时本方法不会启用多选。
|
||||
*/
|
||||
public setAlwaysMultiSelect(value: boolean) {
|
||||
this.alwaysMultiSelect = value;
|
||||
if (this.disabledMultiSelect) return;
|
||||
this.isMultiSelectStatus = value;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -633,14 +654,14 @@ export default class ActionManager extends EventEmitter {
|
||||
});
|
||||
// ctrl+tab切到其他窗口,需要将多选状态置为false
|
||||
KeyController.global.on('blur', () => {
|
||||
if (!this.disabledMultiSelect) {
|
||||
if (!this.disabledMultiSelect && !this.alwaysMultiSelect) {
|
||||
this.isMultiSelectStatus = false;
|
||||
}
|
||||
this.isAltKeydown = false;
|
||||
});
|
||||
KeyController.global.keyup(ctrl, (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!this.disabledMultiSelect) {
|
||||
if (!this.disabledMultiSelect && !this.alwaysMultiSelect) {
|
||||
this.isMultiSelectStatus = false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -269,6 +269,14 @@ export default class StageCore extends EventEmitter {
|
||||
this.actionManager?.enableMultiSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否始终启用多选模式(无需按住 Ctrl/Meta)。
|
||||
* 当多选被 `disabledMultiSelect` 禁用时,本方法不会启用多选。
|
||||
*/
|
||||
public setAlwaysMultiSelect(value: boolean) {
|
||||
this.actionManager?.setAlwaysMultiSelect(value);
|
||||
}
|
||||
|
||||
public reloadIframe(url: string) {
|
||||
this.renderer?.reloadIframe(url);
|
||||
}
|
||||
@ -346,6 +354,7 @@ export default class StageCore extends EventEmitter {
|
||||
container: this.mask!.content,
|
||||
disabledDragStart: config.disabledDragStart,
|
||||
disabledMultiSelect: config.disabledMultiSelect,
|
||||
alwaysMultiSelect: config.alwaysMultiSelect,
|
||||
canSelect: config.canSelect,
|
||||
isContainer: config.isContainer,
|
||||
canDropIn: config.canDropIn,
|
||||
|
||||
@ -84,6 +84,11 @@ export interface StageCoreConfig {
|
||||
renderType?: RenderType;
|
||||
guidesOptions?: Partial<GuidesOptions>;
|
||||
disabledMultiSelect?: boolean;
|
||||
/**
|
||||
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
|
||||
* 当 `disabledMultiSelect` 为 true 时,本配置失效。
|
||||
*/
|
||||
alwaysMultiSelect?: boolean;
|
||||
disabledRule?: boolean;
|
||||
}
|
||||
|
||||
@ -95,6 +100,11 @@ export interface ActionManagerConfig {
|
||||
moveableOptions?: CustomizeMoveableOptions;
|
||||
disabledDragStart?: boolean;
|
||||
disabledMultiSelect?: boolean;
|
||||
/**
|
||||
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
|
||||
* 当 `disabledMultiSelect` 为 true 时,本配置失效。
|
||||
*/
|
||||
alwaysMultiSelect?: boolean;
|
||||
canSelect?: CanSelect;
|
||||
isContainer?: IsContainer;
|
||||
/** 见 StageCoreConfig.canDropIn */
|
||||
|
||||
132
packages/stage/tests/unit/ActionManager.spec.ts
Normal file
132
packages/stage/tests/unit/ActionManager.spec.ts
Normal file
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import ActionManager from '../../src/ActionManager';
|
||||
import type { ActionManagerConfig } from '../../src/types';
|
||||
|
||||
// 在 jsdom 环境下 `moveable-helper`/`moveable` 的 ESM 默认导出无法直接被
|
||||
// require/import 调用,这里仅测试 ActionManager 自身的多选状态管理,将其桩掉。
|
||||
// vi.mock 调用会被 vitest 自动提升到模块顶部,因此放在 import 之后无影响。
|
||||
vi.mock('moveable-helper', () => ({
|
||||
default: {
|
||||
create: () => ({ clear: vi.fn() }),
|
||||
},
|
||||
}));
|
||||
vi.mock('moveable', () => ({
|
||||
default: class MockMoveable {
|
||||
on() {
|
||||
return this;
|
||||
}
|
||||
off() {
|
||||
return this;
|
||||
}
|
||||
destroy() {}
|
||||
request() {}
|
||||
updateRect() {}
|
||||
updateTarget() {}
|
||||
},
|
||||
}));
|
||||
|
||||
const createConfig = (overrides: Partial<ActionManagerConfig> = {}): ActionManagerConfig => {
|
||||
const container = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(container);
|
||||
return {
|
||||
container,
|
||||
getTargetElement: () => null,
|
||||
getElementsFromPoint: () => [],
|
||||
getRenderDocument: () => globalThis.document,
|
||||
getRootContainer: () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
describe('ActionManager - alwaysMultiSelect', () => {
|
||||
let am: ActionManager | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
am?.destroy();
|
||||
am = null;
|
||||
});
|
||||
|
||||
test('默认配置下不会自动开启多选状态', () => {
|
||||
am = new ActionManager(createConfig());
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
expect((am as any).alwaysMultiSelect).toBe(false);
|
||||
});
|
||||
|
||||
test('alwaysMultiSelect=true 构造后即处于多选状态', () => {
|
||||
am = new ActionManager(createConfig({ alwaysMultiSelect: true }));
|
||||
expect((am as any).isMultiSelectStatus).toBe(true);
|
||||
expect((am as any).alwaysMultiSelect).toBe(true);
|
||||
});
|
||||
|
||||
test('disabledMultiSelect=true 时 alwaysMultiSelect=true 也不会开启多选', () => {
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
disabledMultiSelect: true,
|
||||
alwaysMultiSelect: true,
|
||||
}),
|
||||
);
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
expect((am as any).disabledMultiSelect).toBe(true);
|
||||
});
|
||||
|
||||
test('setAlwaysMultiSelect(true) 切换为多选状态', () => {
|
||||
am = new ActionManager(createConfig());
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
|
||||
am.setAlwaysMultiSelect(true);
|
||||
expect((am as any).alwaysMultiSelect).toBe(true);
|
||||
expect((am as any).isMultiSelectStatus).toBe(true);
|
||||
|
||||
am.setAlwaysMultiSelect(false);
|
||||
expect((am as any).alwaysMultiSelect).toBe(false);
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
});
|
||||
|
||||
test('setAlwaysMultiSelect 不能突破 disabledMultiSelect 限制', () => {
|
||||
am = new ActionManager(createConfig({ disabledMultiSelect: true }));
|
||||
am.setAlwaysMultiSelect(true);
|
||||
expect((am as any).alwaysMultiSelect).toBe(true);
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
});
|
||||
|
||||
test('disableMultiSelect() 会重置多选状态', () => {
|
||||
am = new ActionManager(createConfig({ alwaysMultiSelect: true }));
|
||||
expect((am as any).isMultiSelectStatus).toBe(true);
|
||||
|
||||
am.disableMultiSelect();
|
||||
expect((am as any).disabledMultiSelect).toBe(true);
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
});
|
||||
|
||||
test('enableMultiSelect() 后若 alwaysMultiSelect=true 恢复多选状态', () => {
|
||||
am = new ActionManager(createConfig({ alwaysMultiSelect: true }));
|
||||
am.disableMultiSelect();
|
||||
expect((am as any).isMultiSelectStatus).toBe(false);
|
||||
|
||||
am.enableMultiSelect();
|
||||
expect((am as any).disabledMultiSelect).toBe(false);
|
||||
expect((am as any).isMultiSelectStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user