@elementor/editor-canvas 4.4.0-1137 → 4.4.0-1139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +238 -132
- package/dist/index.mjs +135 -29
- package/package.json +20 -20
- package/src/form-structure/__tests__/honeypot-hint.test.ts +137 -0
- package/src/form-structure/__tests__/prevent-duplicate-honeypot-commands.test.ts +133 -0
- package/src/form-structure/honeypot-hint.ts +29 -0
- package/src/form-structure/prevent-duplicate-honeypot-commands.ts +87 -0
- package/src/form-structure/utils.ts +29 -0
- package/src/init.tsx +4 -0
package/dist/index.mjs
CHANGED
|
@@ -1750,6 +1750,7 @@ import { __ } from "@wordpress/i18n";
|
|
|
1750
1750
|
import { getAllDescendants } from "@elementor/editor-elements";
|
|
1751
1751
|
var FORM_ELEMENT_TYPE = "e-form";
|
|
1752
1752
|
var FORM_HIDDEN_FIELD_ELEMENT_TYPE = "e-form-hidden";
|
|
1753
|
+
var FORM_HONEYPOT_ELEMENT_TYPE = "e-form-honeypot";
|
|
1753
1754
|
var FORM_FIELD_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
1754
1755
|
"e-form-input",
|
|
1755
1756
|
"e-form-textarea",
|
|
@@ -1761,11 +1762,31 @@ var FORM_FIELD_ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
|
1761
1762
|
"e-form-file-upload",
|
|
1762
1763
|
"e-form-date-picker",
|
|
1763
1764
|
"e-form-time-picker",
|
|
1764
|
-
FORM_HIDDEN_FIELD_ELEMENT_TYPE
|
|
1765
|
+
FORM_HIDDEN_FIELD_ELEMENT_TYPE,
|
|
1766
|
+
FORM_HONEYPOT_ELEMENT_TYPE
|
|
1765
1767
|
]);
|
|
1766
1768
|
function getArgsElementType(args) {
|
|
1767
1769
|
return args.model?.widgetType || args.model?.elType;
|
|
1768
1770
|
}
|
|
1771
|
+
function hasModelElementType(model, type) {
|
|
1772
|
+
if (!model) {
|
|
1773
|
+
return false;
|
|
1774
|
+
}
|
|
1775
|
+
if ((model.widgetType || model.elType) === type) {
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
return (model.elements ?? []).some((child) => hasModelElementType(child, type));
|
|
1779
|
+
}
|
|
1780
|
+
function getFormContainer(element) {
|
|
1781
|
+
let current = element;
|
|
1782
|
+
while (current) {
|
|
1783
|
+
if (getElementType(current) === FORM_ELEMENT_TYPE) {
|
|
1784
|
+
return current;
|
|
1785
|
+
}
|
|
1786
|
+
current = current.parent;
|
|
1787
|
+
}
|
|
1788
|
+
return void 0;
|
|
1789
|
+
}
|
|
1769
1790
|
function getElementType(element) {
|
|
1770
1791
|
return element?.model.get("widgetType") || element?.model.get("elType");
|
|
1771
1792
|
}
|
|
@@ -1902,25 +1923,108 @@ function initHiddenFieldHint() {
|
|
|
1902
1923
|
});
|
|
1903
1924
|
}
|
|
1904
1925
|
|
|
1905
|
-
// src/form-structure/
|
|
1926
|
+
// src/form-structure/honeypot-hint.ts
|
|
1906
1927
|
import { notify as notify3 } from "@elementor/editor-notifications";
|
|
1907
|
-
import {
|
|
1928
|
+
import { registerDataHook as registerDataHook4 } from "@elementor/editor-v1-adapters";
|
|
1908
1929
|
import { __ as __3 } from "@wordpress/i18n";
|
|
1930
|
+
var MOVE_COMMAND2 = "document/elements/move";
|
|
1931
|
+
var notificationCounter2 = 0;
|
|
1932
|
+
function createHoneypotNotification() {
|
|
1933
|
+
return {
|
|
1934
|
+
type: "default",
|
|
1935
|
+
message: __3("Honeypot added - invisible on your form. Reorder it in the Structure panel.", "elementor"),
|
|
1936
|
+
id: `honeypot-added-${++notificationCounter2}`
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function initHoneypotHint() {
|
|
1940
|
+
registerDataHook4("after", "document/elements/create", (args, _result, options) => {
|
|
1941
|
+
if (options?.commandsCurrentTrace?.includes(MOVE_COMMAND2)) {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (getArgsElementType(args) === FORM_HONEYPOT_ELEMENT_TYPE) {
|
|
1945
|
+
notify3(createHoneypotNotification());
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/form-structure/prevent-duplicate-honeypot-commands.ts
|
|
1951
|
+
import { getAllDescendants as getAllDescendants2 } from "@elementor/editor-elements";
|
|
1952
|
+
import { notify as notify4 } from "@elementor/editor-notifications";
|
|
1953
|
+
import { blockCommand as blockCommand2, registerDataHook as registerDataHook5 } from "@elementor/editor-v1-adapters";
|
|
1954
|
+
import { __ as __4 } from "@wordpress/i18n";
|
|
1955
|
+
var MOVE_COMMAND3 = "document/elements/move";
|
|
1956
|
+
var DUPLICATE_HONEYPOT_ALERT = {
|
|
1957
|
+
type: "default",
|
|
1958
|
+
message: __4("Only one Honeypot can be added per form.", "elementor"),
|
|
1959
|
+
id: "duplicate-honeypot-blocked"
|
|
1960
|
+
};
|
|
1961
|
+
function initDuplicateHoneypotPrevention() {
|
|
1962
|
+
registerDataHook5("dependency", "document/elements/create", (args, options) => {
|
|
1963
|
+
return !blockHoneypotCreate(args, options);
|
|
1964
|
+
});
|
|
1965
|
+
blockCommand2({
|
|
1966
|
+
command: "document/elements/move",
|
|
1967
|
+
condition: blockHoneypotMove
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
function blockHoneypotCreate(args, options) {
|
|
1971
|
+
if (options?.commandsCurrentTrace?.includes(MOVE_COMMAND3)) {
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
if (!hasModelElementType(args.model, FORM_HONEYPOT_ELEMENT_TYPE)) {
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
const containers = args.containers ?? [args.container];
|
|
1978
|
+
if (containers.some((container) => formHasHoneypot(getFormContainer(container)))) {
|
|
1979
|
+
handleBlockedHoneypot();
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
function blockHoneypotMove(args) {
|
|
1985
|
+
const { containers = [args.container], target } = args;
|
|
1986
|
+
const movedContainers = containers.filter((container) => !!container);
|
|
1987
|
+
if (!movedContainers.some((container) => hasElementType(container, FORM_HONEYPOT_ELEMENT_TYPE))) {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
const movedElements = new Set(movedContainers.flatMap((container) => getAllDescendants2(container)));
|
|
1991
|
+
if (formHasHoneypot(getFormContainer(target), movedElements)) {
|
|
1992
|
+
handleBlockedHoneypot();
|
|
1993
|
+
return true;
|
|
1994
|
+
}
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
function formHasHoneypot(form, excluded = /* @__PURE__ */ new Set()) {
|
|
1998
|
+
if (!form) {
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
return getAllDescendants2(form).some(
|
|
2002
|
+
(element) => !excluded.has(element) && getElementType(element) === FORM_HONEYPOT_ELEMENT_TYPE
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
function handleBlockedHoneypot() {
|
|
2006
|
+
notify4(DUPLICATE_HONEYPOT_ALERT);
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/form-structure/prevent-form-nesting-commands.ts
|
|
2010
|
+
import { notify as notify5 } from "@elementor/editor-notifications";
|
|
2011
|
+
import { blockCommand as blockCommand3 } from "@elementor/editor-v1-adapters";
|
|
2012
|
+
import { __ as __5 } from "@wordpress/i18n";
|
|
1909
2013
|
var FORM_NESTING_ALERT = {
|
|
1910
2014
|
type: "default",
|
|
1911
|
-
message:
|
|
2015
|
+
message: __5("Forms can't be nested. Create separate forms instead.", "elementor"),
|
|
1912
2016
|
id: "form-nesting-blocked"
|
|
1913
2017
|
};
|
|
1914
2018
|
function initFormNestingPrevention() {
|
|
1915
|
-
|
|
2019
|
+
blockCommand3({
|
|
1916
2020
|
command: "document/elements/create",
|
|
1917
2021
|
condition: blockFormCreate
|
|
1918
2022
|
});
|
|
1919
|
-
|
|
2023
|
+
blockCommand3({
|
|
1920
2024
|
command: "document/elements/move",
|
|
1921
2025
|
condition: blockFormMove
|
|
1922
2026
|
});
|
|
1923
|
-
|
|
2027
|
+
blockCommand3({
|
|
1924
2028
|
command: "document/elements/paste",
|
|
1925
2029
|
condition: blockFormPaste
|
|
1926
2030
|
});
|
|
@@ -1964,7 +2068,7 @@ function blockFormPaste(args) {
|
|
|
1964
2068
|
return false;
|
|
1965
2069
|
}
|
|
1966
2070
|
function handleBlockedFormField2() {
|
|
1967
|
-
|
|
2071
|
+
notify5(FORM_NESTING_ALERT);
|
|
1968
2072
|
}
|
|
1969
2073
|
|
|
1970
2074
|
// src/settings-transformers-registry.ts
|
|
@@ -3422,7 +3526,7 @@ import {
|
|
|
3422
3526
|
stringPropTypeUtil as stringPropTypeUtil2
|
|
3423
3527
|
} from "@elementor/editor-props";
|
|
3424
3528
|
import { __privateRunCommandSync as runCommandSync, getCurrentEditMode, undoable } from "@elementor/editor-v1-adapters";
|
|
3425
|
-
import { __ as
|
|
3529
|
+
import { __ as __6 } from "@wordpress/i18n";
|
|
3426
3530
|
|
|
3427
3531
|
// src/legacy/replacements/base.ts
|
|
3428
3532
|
var TRIGGER_TIMING = {
|
|
@@ -3844,7 +3948,7 @@ var InlineEditingReplacement = class extends ReplacementBase {
|
|
|
3844
3948
|
{
|
|
3845
3949
|
title: getElementLabel(this.id),
|
|
3846
3950
|
// translators: %s is the name of the property that was edited.
|
|
3847
|
-
subtitle:
|
|
3951
|
+
subtitle: __6("%s edited", "elementor").replace(
|
|
3848
3952
|
"%s",
|
|
3849
3953
|
this.getInlineEditablePropTypeKey() ?? "Inline editing"
|
|
3850
3954
|
),
|
|
@@ -5616,15 +5720,15 @@ import {
|
|
|
5616
5720
|
getAnchoredDescendantId,
|
|
5617
5721
|
isElementAnchored
|
|
5618
5722
|
} from "@elementor/editor-elements";
|
|
5619
|
-
import { notify as
|
|
5620
|
-
import { blockCommand as
|
|
5621
|
-
import { __ as
|
|
5723
|
+
import { notify as notify6 } from "@elementor/editor-notifications";
|
|
5724
|
+
import { blockCommand as blockCommand4 } from "@elementor/editor-v1-adapters";
|
|
5725
|
+
import { __ as __7 } from "@wordpress/i18n";
|
|
5622
5726
|
function initLinkInLinkPrevention() {
|
|
5623
|
-
|
|
5727
|
+
blockCommand4({
|
|
5624
5728
|
command: "document/elements/paste",
|
|
5625
5729
|
condition: blockLinkInLinkPaste
|
|
5626
5730
|
});
|
|
5627
|
-
|
|
5731
|
+
blockCommand4({
|
|
5628
5732
|
command: "document/elements/move",
|
|
5629
5733
|
condition: blockLinkInLinkMove
|
|
5630
5734
|
});
|
|
@@ -5652,7 +5756,7 @@ function blockLinkInLinkPaste(args) {
|
|
|
5652
5756
|
const sourceElements = data.clipboard.elements;
|
|
5653
5757
|
const notification = {
|
|
5654
5758
|
type: "default",
|
|
5655
|
-
message:
|
|
5759
|
+
message: __7(
|
|
5656
5760
|
"To paste a link to this element, first remove the link from it's parent container.",
|
|
5657
5761
|
"elementor"
|
|
5658
5762
|
),
|
|
@@ -5661,7 +5765,7 @@ function blockLinkInLinkPaste(args) {
|
|
|
5661
5765
|
};
|
|
5662
5766
|
const blocked = shouldBlock(sourceElements, targetElements);
|
|
5663
5767
|
if (blocked) {
|
|
5664
|
-
|
|
5768
|
+
notify6(notification);
|
|
5665
5769
|
}
|
|
5666
5770
|
return blocked;
|
|
5667
5771
|
}
|
|
@@ -5671,13 +5775,13 @@ function blockLinkInLinkMove(args) {
|
|
|
5671
5775
|
const targetElement = target;
|
|
5672
5776
|
const notification = {
|
|
5673
5777
|
type: "default",
|
|
5674
|
-
message:
|
|
5778
|
+
message: __7("To drag a link to this element, first remove the link from it's parent container.", "elementor"),
|
|
5675
5779
|
id: "move-in-link-blocked",
|
|
5676
5780
|
additionalActionProps: [learnMoreActionProps]
|
|
5677
5781
|
};
|
|
5678
5782
|
const isBlocked = shouldBlock(sourceElements, [targetElement]);
|
|
5679
5783
|
if (isBlocked) {
|
|
5680
|
-
|
|
5784
|
+
notify6(notification);
|
|
5681
5785
|
}
|
|
5682
5786
|
return isBlocked;
|
|
5683
5787
|
}
|
|
@@ -5702,14 +5806,14 @@ import { getContainer as getContainer5, getElementSetting, updateElementSettings
|
|
|
5702
5806
|
import { classesPropTypeUtil } from "@elementor/editor-props";
|
|
5703
5807
|
import {
|
|
5704
5808
|
__privateListenTo as listenTo5,
|
|
5705
|
-
blockCommand as
|
|
5809
|
+
blockCommand as blockCommand5,
|
|
5706
5810
|
commandStartEvent
|
|
5707
5811
|
} from "@elementor/editor-v1-adapters";
|
|
5708
5812
|
|
|
5709
5813
|
// src/utils/command-utils.ts
|
|
5710
5814
|
import { getElementLabel as getElementLabel2, getWidgetsCache as getWidgetsCache7 } from "@elementor/editor-elements";
|
|
5711
5815
|
import { CLASSES_PROP_KEY } from "@elementor/editor-props";
|
|
5712
|
-
import { __ as
|
|
5816
|
+
import { __ as __8 } from "@wordpress/i18n";
|
|
5713
5817
|
function hasAtomicWidgets(args) {
|
|
5714
5818
|
const { containers = [args.container] } = args;
|
|
5715
5819
|
return containers.some(isAtomicWidget);
|
|
@@ -5745,7 +5849,7 @@ function getClipboardElements(storageKey = "clipboard") {
|
|
|
5745
5849
|
}
|
|
5746
5850
|
}
|
|
5747
5851
|
function getTitleForContainers(containers) {
|
|
5748
|
-
return containers.length > 1 ?
|
|
5852
|
+
return containers.length > 1 ? __8("Elements", "elementor") : getElementLabel2(containers[0].id);
|
|
5749
5853
|
}
|
|
5750
5854
|
|
|
5751
5855
|
// src/style-commands/undoable-actions/paste-element-style.ts
|
|
@@ -5757,7 +5861,7 @@ import {
|
|
|
5757
5861
|
} from "@elementor/editor-elements";
|
|
5758
5862
|
import { ELEMENTS_STYLES_RESERVED_LABEL } from "@elementor/editor-styles-repository";
|
|
5759
5863
|
import { undoable as undoable2 } from "@elementor/editor-v1-adapters";
|
|
5760
|
-
import { __ as
|
|
5864
|
+
import { __ as __9 } from "@wordpress/i18n";
|
|
5761
5865
|
var undoablePasteElementStyle = () => undoable2(
|
|
5762
5866
|
{
|
|
5763
5867
|
do: ({ containers, newStyle }) => {
|
|
@@ -5827,14 +5931,14 @@ var undoablePasteElementStyle = () => undoable2(
|
|
|
5827
5931
|
},
|
|
5828
5932
|
{
|
|
5829
5933
|
title: ({ containers }) => getTitleForContainers(containers),
|
|
5830
|
-
subtitle:
|
|
5934
|
+
subtitle: __9("Style Pasted", "elementor")
|
|
5831
5935
|
}
|
|
5832
5936
|
);
|
|
5833
5937
|
|
|
5834
5938
|
// src/style-commands/paste-style.ts
|
|
5835
5939
|
function initPasteStyleCommand() {
|
|
5836
5940
|
const pasteElementStyleCommand = undoablePasteElementStyle();
|
|
5837
|
-
|
|
5941
|
+
blockCommand5({
|
|
5838
5942
|
command: "document/elements/paste-style",
|
|
5839
5943
|
condition: hasAtomicWidgets
|
|
5840
5944
|
});
|
|
@@ -5892,7 +5996,7 @@ function pasteClasses(containers, classes) {
|
|
|
5892
5996
|
// src/style-commands/reset-style.ts
|
|
5893
5997
|
import {
|
|
5894
5998
|
__privateListenTo as listenTo6,
|
|
5895
|
-
blockCommand as
|
|
5999
|
+
blockCommand as blockCommand6,
|
|
5896
6000
|
commandStartEvent as commandStartEvent2
|
|
5897
6001
|
} from "@elementor/editor-v1-adapters";
|
|
5898
6002
|
|
|
@@ -5900,7 +6004,7 @@ import {
|
|
|
5900
6004
|
import { createElementStyle as createElementStyle3, deleteElementStyle as deleteElementStyle2, getElementStyles as getElementStyles3 } from "@elementor/editor-elements";
|
|
5901
6005
|
import { ELEMENTS_STYLES_RESERVED_LABEL as ELEMENTS_STYLES_RESERVED_LABEL2 } from "@elementor/editor-styles-repository";
|
|
5902
6006
|
import { undoable as undoable3 } from "@elementor/editor-v1-adapters";
|
|
5903
|
-
import { __ as
|
|
6007
|
+
import { __ as __10 } from "@wordpress/i18n";
|
|
5904
6008
|
var undoableResetElementStyle = () => undoable3(
|
|
5905
6009
|
{
|
|
5906
6010
|
do: ({ containers }) => {
|
|
@@ -5938,14 +6042,14 @@ var undoableResetElementStyle = () => undoable3(
|
|
|
5938
6042
|
},
|
|
5939
6043
|
{
|
|
5940
6044
|
title: ({ containers }) => getTitleForContainers(containers),
|
|
5941
|
-
subtitle:
|
|
6045
|
+
subtitle: __10("Style Reset", "elementor")
|
|
5942
6046
|
}
|
|
5943
6047
|
);
|
|
5944
6048
|
|
|
5945
6049
|
// src/style-commands/reset-style.ts
|
|
5946
6050
|
function initResetStyleCommand() {
|
|
5947
6051
|
const resetElementStyles = undoableResetElementStyle();
|
|
5948
|
-
|
|
6052
|
+
blockCommand6({
|
|
5949
6053
|
command: "document/elements/reset-style",
|
|
5950
6054
|
condition: hasAtomicWidgets
|
|
5951
6055
|
});
|
|
@@ -5977,6 +6081,8 @@ function init() {
|
|
|
5977
6081
|
initFormNestingPrevention();
|
|
5978
6082
|
initFormAncestorEnforcement();
|
|
5979
6083
|
initHiddenFieldHint();
|
|
6084
|
+
initDuplicateHoneypotPrevention();
|
|
6085
|
+
initHoneypotHint();
|
|
5980
6086
|
initViewReplacements();
|
|
5981
6087
|
initLegacyViews();
|
|
5982
6088
|
initSettingsTransformers();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elementor/editor-canvas",
|
|
3
3
|
"description": "Elementor Editor Canvas",
|
|
4
|
-
"version": "4.4.0-
|
|
4
|
+
"version": "4.4.0-1139",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Elementor Team",
|
|
7
7
|
"homepage": "https://elementor.com/",
|
|
@@ -37,26 +37,26 @@
|
|
|
37
37
|
"react-dom": "^18.3.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@elementor/editor": "4.4.0-
|
|
41
|
-
"@elementor/editor-controls": "4.4.0-
|
|
42
|
-
"@elementor/editor-documents": "4.4.0-
|
|
43
|
-
"@elementor/editor-elements": "4.4.0-
|
|
44
|
-
"@elementor/editor-interactions": "4.4.0-
|
|
45
|
-
"@elementor/editor-mcp": "4.4.0-
|
|
46
|
-
"@elementor/editor-notifications": "4.4.0-
|
|
47
|
-
"@elementor/editor-props": "4.4.0-
|
|
48
|
-
"@elementor/editor-responsive": "4.4.0-
|
|
49
|
-
"@elementor/editor-styles": "4.4.0-
|
|
50
|
-
"@elementor/editor-styles-repository": "4.4.0-
|
|
51
|
-
"@elementor/editor-ui": "4.4.0-
|
|
52
|
-
"@elementor/editor-v1-adapters": "4.4.0-
|
|
53
|
-
"@elementor/events": "4.4.0-
|
|
54
|
-
"@elementor/http-client": "4.4.0-
|
|
55
|
-
"@elementor/schema": "4.4.0-
|
|
56
|
-
"@elementor/twing": "4.4.0-
|
|
40
|
+
"@elementor/editor": "4.4.0-1139",
|
|
41
|
+
"@elementor/editor-controls": "4.4.0-1139",
|
|
42
|
+
"@elementor/editor-documents": "4.4.0-1139",
|
|
43
|
+
"@elementor/editor-elements": "4.4.0-1139",
|
|
44
|
+
"@elementor/editor-interactions": "4.4.0-1139",
|
|
45
|
+
"@elementor/editor-mcp": "4.4.0-1139",
|
|
46
|
+
"@elementor/editor-notifications": "4.4.0-1139",
|
|
47
|
+
"@elementor/editor-props": "4.4.0-1139",
|
|
48
|
+
"@elementor/editor-responsive": "4.4.0-1139",
|
|
49
|
+
"@elementor/editor-styles": "4.4.0-1139",
|
|
50
|
+
"@elementor/editor-styles-repository": "4.4.0-1139",
|
|
51
|
+
"@elementor/editor-ui": "4.4.0-1139",
|
|
52
|
+
"@elementor/editor-v1-adapters": "4.4.0-1139",
|
|
53
|
+
"@elementor/events": "4.4.0-1139",
|
|
54
|
+
"@elementor/http-client": "4.4.0-1139",
|
|
55
|
+
"@elementor/schema": "4.4.0-1139",
|
|
56
|
+
"@elementor/twing": "4.4.0-1139",
|
|
57
57
|
"@elementor/ui": "1.41.3",
|
|
58
|
-
"@elementor/utils": "4.4.0-
|
|
59
|
-
"@elementor/wp-media": "4.4.0-
|
|
58
|
+
"@elementor/utils": "4.4.0-1139",
|
|
59
|
+
"@elementor/wp-media": "4.4.0-1139",
|
|
60
60
|
"@floating-ui/react": "^0.27.5",
|
|
61
61
|
"@wordpress/i18n": "^5.13.0",
|
|
62
62
|
"dompurify": "^3.2.6"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createHooksRegistry, setupHooksRegistry } from 'test-utils';
|
|
2
|
+
import { notify } from '@elementor/editor-notifications';
|
|
3
|
+
|
|
4
|
+
import { initHoneypotHint } from '../honeypot-hint';
|
|
5
|
+
|
|
6
|
+
jest.mock( '@elementor/editor-notifications', () => ( {
|
|
7
|
+
notify: jest.fn(),
|
|
8
|
+
} ) );
|
|
9
|
+
|
|
10
|
+
const CREATE_COMMAND = 'document/elements/create';
|
|
11
|
+
const MOVE_COMMAND = 'document/elements/move';
|
|
12
|
+
const HONEYPOT_TYPE = 'e-form-honeypot';
|
|
13
|
+
|
|
14
|
+
describe( 'honeypot hint', () => {
|
|
15
|
+
const hooksRegistry = createHooksRegistry();
|
|
16
|
+
|
|
17
|
+
beforeEach( () => {
|
|
18
|
+
jest.mocked( notify ).mockClear();
|
|
19
|
+
setupHooksRegistry( hooksRegistry );
|
|
20
|
+
initHoneypotHint();
|
|
21
|
+
} );
|
|
22
|
+
|
|
23
|
+
it( 'should notify when a honeypot is added', () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
26
|
+
|
|
27
|
+
// Act
|
|
28
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
expect( notify ).toHaveBeenCalledWith(
|
|
32
|
+
expect.objectContaining( {
|
|
33
|
+
message: 'Honeypot added - invisible on your form. Reorder it in the Structure panel.',
|
|
34
|
+
} )
|
|
35
|
+
);
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'should use unique notification ids for back-to-back adds (not collapsed by the notification system)', () => {
|
|
39
|
+
// Arrange
|
|
40
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
41
|
+
|
|
42
|
+
// Act
|
|
43
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
44
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect( notify ).toHaveBeenCalledTimes( 2 );
|
|
48
|
+
const firstCallId = jest.mocked( notify ).mock.calls[ 0 ][ 0 ].id;
|
|
49
|
+
const secondCallId = jest.mocked( notify ).mock.calls[ 1 ][ 0 ].id;
|
|
50
|
+
expect( firstCallId ).not.toBe( secondCallId );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'should notify again after remove and re-add (simulated by separate create calls)', () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
56
|
+
|
|
57
|
+
// Act - first add
|
|
58
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
59
|
+
|
|
60
|
+
// simulate removal (no hook for delete affects this)
|
|
61
|
+
|
|
62
|
+
// Act - re-add
|
|
63
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
66
|
+
expect( notify ).toHaveBeenCalledTimes( 2 );
|
|
67
|
+
} );
|
|
68
|
+
|
|
69
|
+
it( 'should not notify when another element type is created', () => {
|
|
70
|
+
// Arrange
|
|
71
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
72
|
+
|
|
73
|
+
// Act
|
|
74
|
+
hook?.apply( { model: { widgetType: 'e-form-input' } } );
|
|
75
|
+
|
|
76
|
+
// Assert
|
|
77
|
+
expect( notify ).not.toHaveBeenCalled();
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'should not notify when the honeypot is created as part of a move', () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
83
|
+
const eWindow = window as unknown as { $e: { commands?: { currentTrace?: string[] } } };
|
|
84
|
+
eWindow.$e.commands = { currentTrace: [ MOVE_COMMAND, CREATE_COMMAND ] };
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect( notify ).not.toHaveBeenCalled();
|
|
91
|
+
} );
|
|
92
|
+
|
|
93
|
+
it( 'should not notify on selection (selection does not trigger create command)', () => {
|
|
94
|
+
// Arrange - selection uses a different command path, not document/elements/create
|
|
95
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
96
|
+
|
|
97
|
+
// Act - no create call is made for selection
|
|
98
|
+
// Selection triggers commands like document/elements/toggle-selection, not create
|
|
99
|
+
|
|
100
|
+
// Assert
|
|
101
|
+
expect( hook ).toBeDefined();
|
|
102
|
+
expect( notify ).not.toHaveBeenCalled();
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'should notify on paste when it triggers create (current behavior: paste triggers toast)', () => {
|
|
106
|
+
// Arrange
|
|
107
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
108
|
+
|
|
109
|
+
// Act - paste internally calls create, so it will trigger the notification
|
|
110
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect( notify ).toHaveBeenCalledTimes( 1 );
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
it( 'should notify on duplicate when it triggers create (current behavior: duplicate triggers toast)', () => {
|
|
117
|
+
// Arrange
|
|
118
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
119
|
+
|
|
120
|
+
// Act - duplicate internally calls create, so it will trigger the notification
|
|
121
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
122
|
+
|
|
123
|
+
// Assert
|
|
124
|
+
expect( notify ).toHaveBeenCalledTimes( 1 );
|
|
125
|
+
} );
|
|
126
|
+
|
|
127
|
+
it( 'should notify on undo of delete (undo-delete triggers create, so toast shows)', () => {
|
|
128
|
+
// Arrange
|
|
129
|
+
const hook = hooksRegistry.getByCommand( CREATE_COMMAND );
|
|
130
|
+
|
|
131
|
+
// Act - undo of a delete restores via create command
|
|
132
|
+
hook?.apply( { model: { widgetType: HONEYPOT_TYPE } } );
|
|
133
|
+
|
|
134
|
+
// Assert
|
|
135
|
+
expect( notify ).toHaveBeenCalledTimes( 1 );
|
|
136
|
+
} );
|
|
137
|
+
} );
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHooksRegistry, setupHooksRegistry } from 'test-utils';
|
|
2
|
+
import { type V1Element } from '@elementor/editor-elements';
|
|
3
|
+
import { notify } from '@elementor/editor-notifications';
|
|
4
|
+
|
|
5
|
+
import { initDuplicateHoneypotPrevention } from '../prevent-duplicate-honeypot-commands';
|
|
6
|
+
|
|
7
|
+
jest.mock( '@elementor/editor-notifications', () => ( {
|
|
8
|
+
notify: jest.fn(),
|
|
9
|
+
} ) );
|
|
10
|
+
|
|
11
|
+
const CREATE_COMMAND = 'document/elements/create';
|
|
12
|
+
const MOVE_COMMAND = 'document/elements/move';
|
|
13
|
+
const HONEYPOT_TYPE = 'e-form-honeypot';
|
|
14
|
+
|
|
15
|
+
function createElement( type: string, children: V1Element[] = [] ): V1Element {
|
|
16
|
+
const element = {
|
|
17
|
+
id: type,
|
|
18
|
+
model: { get: () => type },
|
|
19
|
+
children,
|
|
20
|
+
} as unknown as V1Element;
|
|
21
|
+
|
|
22
|
+
children.forEach( ( child ) => {
|
|
23
|
+
child.parent = element;
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
return element;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe( 'prevent duplicate honeypot', () => {
|
|
30
|
+
const hooksRegistry = createHooksRegistry();
|
|
31
|
+
|
|
32
|
+
const isAllowed = ( command: string, args: unknown ) => hooksRegistry.getByCommand( command )?.apply( args );
|
|
33
|
+
|
|
34
|
+
beforeEach( () => {
|
|
35
|
+
jest.mocked( notify ).mockClear();
|
|
36
|
+
setupHooksRegistry( hooksRegistry );
|
|
37
|
+
initDuplicateHoneypotPrevention();
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
it( 'should allow adding the first honeypot to a form', () => {
|
|
41
|
+
// Arrange
|
|
42
|
+
const flexbox = createElement( 'e-flexbox' );
|
|
43
|
+
createElement( 'e-form', [ flexbox ] );
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
const result = isAllowed( CREATE_COMMAND, { container: flexbox, model: { widgetType: HONEYPOT_TYPE } } );
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect( result ).toBe( true );
|
|
50
|
+
expect( notify ).not.toHaveBeenCalled();
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'should block a second honeypot in the same form and show a toast', () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const flexbox = createElement( 'e-flexbox' );
|
|
56
|
+
createElement( 'e-form', [ createElement( HONEYPOT_TYPE ), flexbox ] );
|
|
57
|
+
|
|
58
|
+
// Act
|
|
59
|
+
const result = isAllowed( CREATE_COMMAND, { container: flexbox, model: { widgetType: HONEYPOT_TYPE } } );
|
|
60
|
+
|
|
61
|
+
// Assert
|
|
62
|
+
expect( result ).toBe( false );
|
|
63
|
+
expect( notify ).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining( { message: 'Only one Honeypot can be added per form.' } )
|
|
65
|
+
);
|
|
66
|
+
} );
|
|
67
|
+
|
|
68
|
+
it( 'should block pasting a container that holds a honeypot into a form that already has one', () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const form = createElement( 'e-form', [ createElement( HONEYPOT_TYPE ) ] );
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
const result = isAllowed( CREATE_COMMAND, {
|
|
74
|
+
container: form,
|
|
75
|
+
model: { elType: 'e-flexbox', elements: [ { widgetType: HONEYPOT_TYPE } ] },
|
|
76
|
+
} );
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
79
|
+
expect( result ).toBe( false );
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'should allow a honeypot in a second form on the same page', () => {
|
|
83
|
+
// Arrange
|
|
84
|
+
const secondForm = createElement( 'e-form' );
|
|
85
|
+
createElement( 'document', [ createElement( 'e-form', [ createElement( HONEYPOT_TYPE ) ] ), secondForm ] );
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const result = isAllowed( CREATE_COMMAND, { container: secondForm, model: { widgetType: HONEYPOT_TYPE } } );
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
expect( result ).toBe( true );
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
it( 'should ignore the create that runs as part of a move', () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const form = createElement( 'e-form', [ createElement( HONEYPOT_TYPE ) ] );
|
|
97
|
+
const eWindow = window as unknown as { $e: { commands?: { currentTrace?: string[] } } };
|
|
98
|
+
eWindow.$e.commands = { currentTrace: [ MOVE_COMMAND, CREATE_COMMAND ] };
|
|
99
|
+
|
|
100
|
+
// Act
|
|
101
|
+
const result = isAllowed( CREATE_COMMAND, { container: form, model: { widgetType: HONEYPOT_TYPE } } );
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
104
|
+
expect( result ).toBe( true );
|
|
105
|
+
} );
|
|
106
|
+
|
|
107
|
+
it( 'should allow reordering a honeypot inside its own form', () => {
|
|
108
|
+
// Arrange
|
|
109
|
+
const honeypot = createElement( HONEYPOT_TYPE );
|
|
110
|
+
const flexbox = createElement( 'e-flexbox' );
|
|
111
|
+
createElement( 'e-form', [ honeypot, flexbox ] );
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const result = isAllowed( MOVE_COMMAND, { containers: [ honeypot ], target: flexbox } );
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect( result ).toBe( true );
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
it( 'should block moving a honeypot into another form that already has one', () => {
|
|
121
|
+
// Arrange
|
|
122
|
+
const honeypot = createElement( HONEYPOT_TYPE );
|
|
123
|
+
const targetForm = createElement( 'e-form', [ createElement( HONEYPOT_TYPE ) ] );
|
|
124
|
+
createElement( 'document', [ createElement( 'e-form', [ honeypot ] ), targetForm ] );
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
const result = isAllowed( MOVE_COMMAND, { containers: [ honeypot ], target: targetForm } );
|
|
128
|
+
|
|
129
|
+
// Assert
|
|
130
|
+
expect( result ).toBe( false );
|
|
131
|
+
expect( notify ).toHaveBeenCalledTimes( 1 );
|
|
132
|
+
} );
|
|
133
|
+
} );
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type NotificationData, notify } from '@elementor/editor-notifications';
|
|
2
|
+
import { type HookOptions, registerDataHook } from '@elementor/editor-v1-adapters';
|
|
3
|
+
import { __ } from '@wordpress/i18n';
|
|
4
|
+
|
|
5
|
+
import { type CreateArgs, FORM_HONEYPOT_ELEMENT_TYPE, getArgsElementType } from './utils';
|
|
6
|
+
|
|
7
|
+
const MOVE_COMMAND = 'document/elements/move';
|
|
8
|
+
|
|
9
|
+
let notificationCounter = 0;
|
|
10
|
+
|
|
11
|
+
function createHoneypotNotification(): NotificationData {
|
|
12
|
+
return {
|
|
13
|
+
type: 'default',
|
|
14
|
+
message: __( 'Honeypot added - invisible on your form. Reorder it in the Structure panel.', 'elementor' ),
|
|
15
|
+
id: `honeypot-added-${ ++notificationCounter }`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function initHoneypotHint() {
|
|
20
|
+
registerDataHook( 'after', 'document/elements/create', ( args: CreateArgs, _result, options?: HookOptions ) => {
|
|
21
|
+
if ( options?.commandsCurrentTrace?.includes( MOVE_COMMAND ) ) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ( getArgsElementType( args ) === FORM_HONEYPOT_ELEMENT_TYPE ) {
|
|
26
|
+
notify( createHoneypotNotification() );
|
|
27
|
+
}
|
|
28
|
+
} );
|
|
29
|
+
}
|