@griddo/ax 11.11.8-rc.1 → 11.12.0
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/config/jest/componentsMock.js +5 -7
- package/package.json +2 -2
- package/src/__tests__/components/Browser/Browser.test.tsx +87 -438
- package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +3 -1
- package/src/__tests__/components/Fields/Button/Button.test.tsx +27 -29
- package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
- package/src/components/Browser/index.tsx +149 -294
- package/src/components/Browser/style.tsx +6 -75
- package/src/components/Button/index.tsx +1 -2
- package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +4 -2
- package/src/components/Fields/AsyncSelect/style.tsx +0 -13
- package/src/components/Fields/FieldGroup/index.tsx +2 -5
- package/src/components/Fields/FieldGroup/style.tsx +7 -32
- package/src/components/Fields/HeadingField/index.tsx +2 -2
- package/src/components/Fields/HiddenField/style.tsx +1 -1
- package/src/components/Fields/NumberField/index.tsx +16 -15
- package/src/components/Fields/NumberField/style.tsx +0 -2
- package/src/components/Fields/ReferenceField/index.tsx +1 -1
- package/src/components/Fields/Select/index.tsx +1 -5
- package/src/components/Fields/Select/style.tsx +0 -56
- package/src/components/Fields/SummaryButton/index.tsx +9 -18
- package/src/components/Fields/SummaryButton/style.tsx +2 -1
- package/src/components/Fields/TagsField/index.tsx +9 -8
- package/src/components/Fields/UrlField/index.tsx +27 -26
- package/src/components/Fields/index.tsx +0 -2
- package/src/components/FloatingPanel/index.tsx +2 -5
- package/src/components/FloatingPanel/style.tsx +1 -2
- package/src/components/IconAction/index.tsx +1 -1
- package/src/components/MainWrapper/AppBar/index.tsx +1 -8
- package/src/components/MainWrapper/index.tsx +1 -7
- package/src/components/Notification/index.tsx +2 -2
- package/src/components/PageFinder/index.tsx +1 -1
- package/src/components/ResizePanel/index.tsx +3 -4
- package/src/components/ResizePanel/style.tsx +1 -1
- package/src/components/SearchField/style.tsx +2 -2
- package/src/components/SideModal/index.tsx +1 -2
- package/src/components/Tabs/index.tsx +4 -13
- package/src/components/Tabs/style.tsx +8 -7
- package/src/components/Toast/index.tsx +2 -4
- package/src/components/Tooltip/index.tsx +3 -4
- package/src/components/index.tsx +0 -8
- package/src/forms/fields.tsx +68 -70
- package/src/hooks/forms.tsx +1 -22
- package/src/hooks/index.tsx +3 -13
- package/src/hooks/modals.tsx +15 -103
- package/src/hooks/users.tsx +8 -25
- package/src/modules/Forms/atoms.tsx +2 -2
- package/src/modules/FramePreview/index.tsx +16 -55
- package/src/modules/FramePreview/style.tsx +2 -34
- package/src/modules/GlobalEditor/Editor/index.tsx +3 -37
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +2 -19
- package/src/modules/GlobalEditor/Preview/index.tsx +2 -0
- package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
- package/src/modules/GlobalEditor/index.tsx +57 -119
- package/src/modules/PageEditor/Editor/index.tsx +2 -33
- package/src/modules/PageEditor/PageBrowser/index.tsx +2 -20
- package/src/modules/PageEditor/Preview/index.tsx +2 -0
- package/src/modules/PageEditor/Preview/style.tsx +1 -1
- package/src/modules/PageEditor/atoms.tsx +1 -1
- package/src/modules/PageEditor/index.tsx +66 -130
- package/src/modules/PublicPreview/index.tsx +2 -5
- package/src/schemas/pages/GlobalPage.ts +70 -87
- package/src/schemas/pages/Page.ts +70 -87
- package/src/types/index.tsx +0 -12
- package/src/__tests__/components/Browser/Browser.utils.test.ts +0 -55
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +0 -158
- package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +0 -90
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +0 -178
- package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +0 -150
- package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +0 -91
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +0 -122
- package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +0 -15
- package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +0 -101
- package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +0 -318
- package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +0 -242
- package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +0 -185
- package/src/components/Browser/utils.tsx +0 -13
- package/src/components/Fields/SEOPreview/index.tsx +0 -36
- package/src/components/Fields/SEOPreview/style.tsx +0 -24
- package/src/components/FloatingNote/index.tsx +0 -35
- package/src/components/FloatingNote/style.tsx +0 -26
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +0 -85
- package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +0 -80
- package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +0 -57
- package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +0 -82
- package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +0 -71
- package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +0 -77
- package/src/components/HeadingsPreviewModal/index.tsx +0 -146
- package/src/components/HeadingsPreviewModal/style.tsx +0 -82
- package/src/components/HeadingsPreviewModal/utils.tsx +0 -257
- package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +0 -46
- package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +0 -64
- package/src/components/KeywordsPreviewModal/atoms.tsx +0 -96
- package/src/components/KeywordsPreviewModal/index.tsx +0 -99
- package/src/components/KeywordsPreviewModal/style.tsx +0 -87
- package/src/components/KeywordsPreviewModal/utils.tsx +0 -22
- package/src/modules/FramePreview/HeadingsOverlay/index.tsx +0 -113
- package/src/modules/FramePreview/HeadingsOverlay/style.tsx +0 -24
- package/src/modules/FramePreview/utils.tsx +0 -140
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
|
|
3
|
-
import type { IModal } from "@ax/types";
|
|
4
|
-
import { Modal, FieldsBehavior } from "@ax/components";
|
|
5
|
-
|
|
6
|
-
import * as S from "./style";
|
|
7
|
-
|
|
8
|
-
const AddKeywordsModal = (props: IAddKeywordsModal) => {
|
|
9
|
-
const { isOpen, toggleModal, addNewKeyword } = props;
|
|
10
|
-
|
|
11
|
-
const [value, setValue] = useState<string[]>([]);
|
|
12
|
-
|
|
13
|
-
const handleChange = (newValue: string[]) => setValue(newValue);
|
|
14
|
-
|
|
15
|
-
const handleClick = () => {
|
|
16
|
-
addNewKeyword(value);
|
|
17
|
-
setValue([]);
|
|
18
|
-
toggleModal();
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const handleClose = () => {
|
|
22
|
-
isOpen && toggleModal();
|
|
23
|
-
setValue([]);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const mainModalAction = {
|
|
27
|
-
title: "Create keyword",
|
|
28
|
-
onClick: handleClick,
|
|
29
|
-
disabled: !value.length,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const secondaryModalAction = { title: "Cancel", onClick: handleClose };
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Modal
|
|
36
|
-
isOpen={isOpen}
|
|
37
|
-
hide={handleClose}
|
|
38
|
-
title="New keyword"
|
|
39
|
-
secondaryAction={secondaryModalAction}
|
|
40
|
-
mainAction={mainModalAction}
|
|
41
|
-
size="S"
|
|
42
|
-
height={282}
|
|
43
|
-
>
|
|
44
|
-
<S.ModalContent>
|
|
45
|
-
<FieldsBehavior
|
|
46
|
-
fieldType="TagsField"
|
|
47
|
-
name="keywords"
|
|
48
|
-
title="Keywords"
|
|
49
|
-
value={value}
|
|
50
|
-
onChange={handleChange}
|
|
51
|
-
placeholder="Type a keyword..."
|
|
52
|
-
helptext="Type a tag and press enter to create it"
|
|
53
|
-
/>
|
|
54
|
-
</S.ModalContent>
|
|
55
|
-
</Modal>
|
|
56
|
-
);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
interface IAddKeywordsModal extends IModal {
|
|
60
|
-
addNewKeyword: (value: string[]) => void;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const DeleteKeywordsModal = (props: IDeleteKeywordsModal) => {
|
|
64
|
-
const { isOpen, toggleModal, deleteKeyword } = props;
|
|
65
|
-
|
|
66
|
-
const mainModalAction = {
|
|
67
|
-
title: "Delete keyword",
|
|
68
|
-
onClick: deleteKeyword,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const secondaryModalAction = { title: "Cancel", onClick: toggleModal };
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<Modal
|
|
75
|
-
isOpen={isOpen}
|
|
76
|
-
hide={toggleModal}
|
|
77
|
-
title="Delete keyword"
|
|
78
|
-
secondaryAction={secondaryModalAction}
|
|
79
|
-
mainAction={mainModalAction}
|
|
80
|
-
size="S"
|
|
81
|
-
height={240}
|
|
82
|
-
>
|
|
83
|
-
<S.ModalContent>
|
|
84
|
-
You are about to <strong>delete a keyword that is currently in use.</strong>
|
|
85
|
-
<br />
|
|
86
|
-
Please make sure you want to delete it.
|
|
87
|
-
</S.ModalContent>
|
|
88
|
-
</Modal>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
interface IDeleteKeywordsModal extends IModal {
|
|
93
|
-
deleteKeyword: () => void;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export { AddKeywordsModal, DeleteKeywordsModal };
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import { FloatingPanel, Icon, Toast } from "@ax/components";
|
|
4
|
-
import { useModal, useToast } from "@ax/hooks";
|
|
5
|
-
|
|
6
|
-
import { AddKeywordsModal } from "./atoms";
|
|
7
|
-
import KeywordItem from "./KeywordItem";
|
|
8
|
-
import { countKeywords } from "./utils";
|
|
9
|
-
|
|
10
|
-
import * as S from "./style";
|
|
11
|
-
|
|
12
|
-
const KeywordsPreviewModal = (props: IKeywordsPreviewProps) => {
|
|
13
|
-
const { isOpen, browserRef, keywords, keywordsFilter, toggleModal, setKeywordsFilter, addKeywords, deleteKeyword } =
|
|
14
|
-
props;
|
|
15
|
-
const [keywordCounts, setKeywordCounts] = useState<Record<string, number>>({});
|
|
16
|
-
const [deletedKeyword, setDeletedKeyword] = useState<string | null>(null);
|
|
17
|
-
const { isOpen: isAddOpen, toggleModal: toggleAddModal } = useModal();
|
|
18
|
-
const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
if (isOpen && browserRef.current) {
|
|
22
|
-
const keywordCount = countKeywords(browserRef.current, keywords);
|
|
23
|
-
setKeywordCounts(keywordCount);
|
|
24
|
-
}
|
|
25
|
-
}, [isOpen, browserRef, keywords]);
|
|
26
|
-
|
|
27
|
-
const handleDeleteTag = () => setKeywordsFilter([]);
|
|
28
|
-
|
|
29
|
-
const handleAddTag = (tag: string) => () =>
|
|
30
|
-
keywordsFilter.includes(tag) ? setKeywordsFilter([]) : setKeywordsFilter([tag]);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (deletedKeyword && !keywords.includes(deletedKeyword)) {
|
|
34
|
-
toggleToast("1 Keyword deleted");
|
|
35
|
-
setDeletedKeyword(null);
|
|
36
|
-
}
|
|
37
|
-
}, [keywords, deletedKeyword, toggleToast]);
|
|
38
|
-
|
|
39
|
-
const handleDeleteKeyword = (value: string) => {
|
|
40
|
-
setDeletedKeyword(value);
|
|
41
|
-
deleteKeyword(value);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<S.Wrapper>
|
|
46
|
-
<FloatingPanel title="Keywords" toggleModal={toggleModal} closeOnOutsideClick={false} isOpen={isOpen} width={358}>
|
|
47
|
-
{isOpen && (
|
|
48
|
-
<S.KeywordsWrapper>
|
|
49
|
-
{keywordsFilter.length > 0 && (
|
|
50
|
-
<S.FilterWrapper>
|
|
51
|
-
<S.FilterText>Show keyword:</S.FilterText>
|
|
52
|
-
<S.TagList>
|
|
53
|
-
{keywordsFilter.map((tag: string) => {
|
|
54
|
-
return <S.StyledTag key={tag} text={tag} color="#FFFFFF" onDeleteAction={handleDeleteTag} />;
|
|
55
|
-
})}
|
|
56
|
-
</S.TagList>
|
|
57
|
-
</S.FilterWrapper>
|
|
58
|
-
)}
|
|
59
|
-
<S.KeywordsListWrapper>
|
|
60
|
-
{keywords.length === 0 && <S.StyledSummaryButton />}
|
|
61
|
-
{Object.keys(keywordCounts).map((key, index) => {
|
|
62
|
-
const isSelected = keywordsFilter.includes(key);
|
|
63
|
-
return (
|
|
64
|
-
<KeywordItem
|
|
65
|
-
keyword={key}
|
|
66
|
-
count={keywordCounts[key]}
|
|
67
|
-
isSelected={isSelected}
|
|
68
|
-
onClick={handleAddTag(key)}
|
|
69
|
-
deleteKeyword={handleDeleteKeyword}
|
|
70
|
-
key={`${key}-${index}`}
|
|
71
|
-
/>
|
|
72
|
-
);
|
|
73
|
-
})}
|
|
74
|
-
<S.AddKeywordButton onClick={toggleAddModal}>
|
|
75
|
-
<Icon name="add" size="16" />
|
|
76
|
-
Add new keyword
|
|
77
|
-
</S.AddKeywordButton>
|
|
78
|
-
</S.KeywordsListWrapper>
|
|
79
|
-
</S.KeywordsWrapper>
|
|
80
|
-
)}
|
|
81
|
-
</FloatingPanel>
|
|
82
|
-
<AddKeywordsModal isOpen={isAddOpen} toggleModal={toggleAddModal} addNewKeyword={addKeywords} />
|
|
83
|
-
{isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
|
|
84
|
-
</S.Wrapper>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
interface IKeywordsPreviewProps {
|
|
89
|
-
isOpen: boolean;
|
|
90
|
-
browserRef: React.RefObject<HTMLDivElement>;
|
|
91
|
-
toggleModal: () => void;
|
|
92
|
-
keywords: string[];
|
|
93
|
-
keywordsFilter: string[];
|
|
94
|
-
setKeywordsFilter: (value: string[]) => void;
|
|
95
|
-
addKeywords: (value: string[]) => void;
|
|
96
|
-
deleteKeyword: (value: string) => void;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export default KeywordsPreviewModal;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import styled from "styled-components";
|
|
2
|
-
|
|
3
|
-
import SummaryButton from "../Fields/SummaryButton";
|
|
4
|
-
import Tag from "../Tag";
|
|
5
|
-
|
|
6
|
-
const Wrapper = styled.div``;
|
|
7
|
-
|
|
8
|
-
const KeywordsWrapper = styled.div`
|
|
9
|
-
padding: ${(p) => `${p.theme.spacing.m} ${p.theme.spacing.m} 80px ${p.theme.spacing.s}`};
|
|
10
|
-
overflow-y: auto;
|
|
11
|
-
height: 100%;
|
|
12
|
-
width: 100%;
|
|
13
|
-
position: relative;
|
|
14
|
-
|
|
15
|
-
::-webkit-scrollbar {
|
|
16
|
-
-webkit-appearance: none;
|
|
17
|
-
width: 4px;
|
|
18
|
-
height: 100%;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
::-webkit-scrollbar-thumb {
|
|
22
|
-
border-radius: 4px;
|
|
23
|
-
background-color: ${(p) => p.theme.colors.iconNonActive};
|
|
24
|
-
}
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
const KeywordsListWrapper = styled.div``;
|
|
28
|
-
|
|
29
|
-
const FilterWrapper = styled.div`
|
|
30
|
-
display: flex;
|
|
31
|
-
background-color: ${(p) => p.theme.color.uiBackground03};
|
|
32
|
-
width: 100%;
|
|
33
|
-
padding: ${(p) => `${p.theme.spacing.xs} ${p.theme.spacing.xs}`};
|
|
34
|
-
align-items: center;
|
|
35
|
-
border-radius: ${(p) => p.theme.radii.s};
|
|
36
|
-
margin-bottom: ${(p) => p.theme.spacing.xs};
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
const FilterText = styled.div`
|
|
40
|
-
${(p) => p.theme.textStyle.uiS};
|
|
41
|
-
color: ${(p) => p.theme.color.textHighEmphasis};
|
|
42
|
-
margin-right: ${(p) => p.theme.spacing.xs};
|
|
43
|
-
flex-shrink: 0;
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
|
-
const StyledTag = styled(Tag)`
|
|
47
|
-
margin-right: ${(p) => p.theme.spacing.xs};
|
|
48
|
-
margin-bottom: 0;
|
|
49
|
-
`;
|
|
50
|
-
|
|
51
|
-
const TagList = styled.div``;
|
|
52
|
-
|
|
53
|
-
const AddKeywordButton = styled.button`
|
|
54
|
-
display: flex;
|
|
55
|
-
${(p) => p.theme.textStyle.fieldLabel};
|
|
56
|
-
color: ${(p) => p.theme.colors.interactive01};
|
|
57
|
-
min-height: 40px;
|
|
58
|
-
width: 100%;
|
|
59
|
-
border: ${(p) => `1px dashed ${p.theme.colors.interactive01}`};
|
|
60
|
-
border-radius: ${(p) => p.theme.radii.s};
|
|
61
|
-
align-items: center;
|
|
62
|
-
justify-content: center;
|
|
63
|
-
svg {
|
|
64
|
-
margin-right: ${(p) => p.theme.spacing.xxs};
|
|
65
|
-
}
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
const ModalContent = styled.div`
|
|
69
|
-
padding: ${(p) => p.theme.spacing.m};
|
|
70
|
-
`;
|
|
71
|
-
|
|
72
|
-
const StyledSummaryButton = styled(SummaryButton)`
|
|
73
|
-
margin-bottom: ${(p) => p.theme.spacing.xs};
|
|
74
|
-
`;
|
|
75
|
-
|
|
76
|
-
export {
|
|
77
|
-
Wrapper,
|
|
78
|
-
KeywordsWrapper,
|
|
79
|
-
KeywordsListWrapper,
|
|
80
|
-
FilterWrapper,
|
|
81
|
-
FilterText,
|
|
82
|
-
StyledTag,
|
|
83
|
-
TagList,
|
|
84
|
-
AddKeywordButton,
|
|
85
|
-
ModalContent,
|
|
86
|
-
StyledSummaryButton,
|
|
87
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const countKeywords = (html: HTMLDivElement, keywords: string[]) => {
|
|
2
|
-
const frameObject = html.querySelector<HTMLIFrameElement>(".frame-content");
|
|
3
|
-
const frameContent = frameObject?.contentWindow?.document.getElementById("___griddo") as HTMLElement;
|
|
4
|
-
|
|
5
|
-
if (!frameContent) {
|
|
6
|
-
return {};
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const htmlContent = frameContent.innerText.toLowerCase();
|
|
10
|
-
const keywordCounts: Record<string, number> = {};
|
|
11
|
-
|
|
12
|
-
keywords.forEach((keyword) => {
|
|
13
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
14
|
-
const regex = new RegExp(lowerKeyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
15
|
-
const matches = htmlContent.match(regex);
|
|
16
|
-
keywordCounts[keyword] = matches ? matches.length : 0;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return keywordCounts;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export { countKeywords };
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
|
-
|
|
4
|
-
import * as S from "./style";
|
|
5
|
-
|
|
6
|
-
const headColors: Record<string, string> = {
|
|
7
|
-
h1: "#FFF06D",
|
|
8
|
-
h2: "#FFB8F8",
|
|
9
|
-
h3: "#73F8C8",
|
|
10
|
-
h4: "#9BEDFF",
|
|
11
|
-
h5: "#C6C1FF",
|
|
12
|
-
h6: "#FFCE95",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const isEffectivelyVisible = (el: HTMLElement): boolean => {
|
|
16
|
-
let node: HTMLElement | null = el;
|
|
17
|
-
while (node && node !== document.body) {
|
|
18
|
-
const style = window.getComputedStyle(node);
|
|
19
|
-
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
20
|
-
if (parseFloat(style.opacity) === 0) return false;
|
|
21
|
-
node = node.parentElement;
|
|
22
|
-
}
|
|
23
|
-
return true;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
|
|
27
|
-
const [boxes, setBoxes] = useState<HeadingBox[]>([]);
|
|
28
|
-
const rafRef = useRef<number>(0);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const update = () => {
|
|
32
|
-
cancelAnimationFrame(rafRef.current);
|
|
33
|
-
rafRef.current = requestAnimationFrame(() => {
|
|
34
|
-
const selector = headingFilter || "h1, h2, h3, h4, h5, h6";
|
|
35
|
-
const headings = Array.from(document.querySelectorAll<HTMLElement>(selector));
|
|
36
|
-
const scrollX = window.scrollX;
|
|
37
|
-
const scrollY = window.scrollY;
|
|
38
|
-
const boxes: HeadingBox[] = [];
|
|
39
|
-
for (let i = 0; i < headings.length; i++) {
|
|
40
|
-
const el = headings[i];
|
|
41
|
-
const rect = el.getBoundingClientRect();
|
|
42
|
-
if (rect.width === 0 && rect.height === 0) continue;
|
|
43
|
-
if (!isEffectivelyVisible(el)) continue;
|
|
44
|
-
boxes.push({
|
|
45
|
-
id: el.dataset.griddoid || `heading-${i}`,
|
|
46
|
-
tag: el.tagName.toLowerCase(),
|
|
47
|
-
rect,
|
|
48
|
-
scrollX,
|
|
49
|
-
scrollY,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
setBoxes(boxes);
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
update();
|
|
57
|
-
|
|
58
|
-
window.addEventListener("resize", update);
|
|
59
|
-
window.addEventListener("scroll", update, true);
|
|
60
|
-
document.addEventListener("animationend", update, true);
|
|
61
|
-
document.addEventListener("transitionend", update, true);
|
|
62
|
-
|
|
63
|
-
const observer = new MutationObserver(update);
|
|
64
|
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
65
|
-
|
|
66
|
-
return () => {
|
|
67
|
-
cancelAnimationFrame(rafRef.current);
|
|
68
|
-
window.removeEventListener("resize", update);
|
|
69
|
-
window.removeEventListener("scroll", update, true);
|
|
70
|
-
document.removeEventListener("animationend", update, true);
|
|
71
|
-
document.removeEventListener("transitionend", update, true);
|
|
72
|
-
observer.disconnect();
|
|
73
|
-
};
|
|
74
|
-
}, [headingFilter]);
|
|
75
|
-
|
|
76
|
-
return createPortal(
|
|
77
|
-
<div data-testid="headings-overlay">
|
|
78
|
-
{boxes.map(({ id, tag, rect, scrollX, scrollY }) => {
|
|
79
|
-
const color = headColors[tag];
|
|
80
|
-
if (!color) return null;
|
|
81
|
-
return (
|
|
82
|
-
<S.Box
|
|
83
|
-
key={id}
|
|
84
|
-
$color={color}
|
|
85
|
-
style={{
|
|
86
|
-
top: rect.top + scrollY,
|
|
87
|
-
left: rect.left + scrollX,
|
|
88
|
-
width: rect.width,
|
|
89
|
-
height: rect.height,
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
<S.Label $color={color}>{tag.toUpperCase()}</S.Label>
|
|
93
|
-
</S.Box>
|
|
94
|
-
);
|
|
95
|
-
})}
|
|
96
|
-
</div>,
|
|
97
|
-
document.body,
|
|
98
|
-
);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
interface HeadingBox {
|
|
102
|
-
id: string;
|
|
103
|
-
tag: string;
|
|
104
|
-
rect: DOMRect;
|
|
105
|
-
scrollX: number;
|
|
106
|
-
scrollY: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface IHeadingsOverlayProps {
|
|
110
|
-
headingFilter: string | null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export default HeadingsOverlay;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import styled from "styled-components";
|
|
2
|
-
|
|
3
|
-
const Box = styled.div<{ $color: string }>`
|
|
4
|
-
position: absolute;
|
|
5
|
-
outline: 2px solid ${(p) => p.$color};
|
|
6
|
-
z-index: 1001;
|
|
7
|
-
pointer-events: none;
|
|
8
|
-
`;
|
|
9
|
-
|
|
10
|
-
const Label = styled.span<{ $color: string }>`
|
|
11
|
-
position: absolute;
|
|
12
|
-
background-color: ${(p) => p.$color};
|
|
13
|
-
color: ${(p) => p.theme.colors.textHighEmphasis};
|
|
14
|
-
${(p) => p.theme.textStyle.uiS};
|
|
15
|
-
padding: ${(p) => `0 ${p.theme.spacing.xxs}`};
|
|
16
|
-
transform: rotate(-90deg);
|
|
17
|
-
top: 1px;
|
|
18
|
-
left: -23px;
|
|
19
|
-
font-family: Source Sans Pro;
|
|
20
|
-
font-style: normal;
|
|
21
|
-
text-decoration: none;
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
export { Box, Label };
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
const addIdsToHeadings = (container: HTMLElement, headingFilter: string | null): void => {
|
|
2
|
-
const selected = headingFilter ? headingFilter : "h1, h2, h3, h4, h5, h6";
|
|
3
|
-
const headings = container.querySelectorAll<HTMLElement>(selected);
|
|
4
|
-
|
|
5
|
-
headings.forEach((heading, index) => {
|
|
6
|
-
const text = heading.textContent?.trim();
|
|
7
|
-
if (!text) return;
|
|
8
|
-
|
|
9
|
-
heading.dataset.griddoid = `heading-${index + 1}`;
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const observeAndAddIdsToHeadings = (container: HTMLElement, headingFilter: string | null): (() => void) => {
|
|
14
|
-
addIdsToHeadings(container, headingFilter);
|
|
15
|
-
|
|
16
|
-
const observer = new MutationObserver(() => {
|
|
17
|
-
addIdsToHeadings(container, headingFilter);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
observer.observe(container, {
|
|
21
|
-
childList: true,
|
|
22
|
-
subtree: true,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
return () => observer.disconnect();
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const KEYWORD_HIGHLIGHT_CLASS = "gdd-keyword";
|
|
29
|
-
|
|
30
|
-
const removeKeywordHighlights = (container: HTMLElement): void => {
|
|
31
|
-
const marks = container.querySelectorAll<HTMLElement>(`mark.${KEYWORD_HIGHLIGHT_CLASS}`);
|
|
32
|
-
|
|
33
|
-
marks.forEach((mark) => {
|
|
34
|
-
const parent = mark.parentNode;
|
|
35
|
-
if (!parent) return;
|
|
36
|
-
parent.replaceChild(document.createTextNode(mark.textContent || ""), mark);
|
|
37
|
-
parent.normalize();
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const buildKeywordsRegex = (keywords: string[]): RegExp | null => {
|
|
42
|
-
const escapedKeywords = keywords
|
|
43
|
-
.filter((k) => k.trim())
|
|
44
|
-
.sort((a, b) => b.length - a.length)
|
|
45
|
-
.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
46
|
-
|
|
47
|
-
if (!escapedKeywords.length) return null;
|
|
48
|
-
|
|
49
|
-
return new RegExp(`(${escapedKeywords.join("|")})`, "gi");
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const collectTextNodes = (container: HTMLElement): Text[] => {
|
|
53
|
-
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
|
|
54
|
-
acceptNode: (node) => {
|
|
55
|
-
const parent = node.parentElement;
|
|
56
|
-
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
57
|
-
if (["SCRIPT", "STYLE"].includes(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
|
58
|
-
if (parent.classList.contains(KEYWORD_HIGHLIGHT_CLASS)) return NodeFilter.FILTER_REJECT;
|
|
59
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const textNodes: Text[] = [];
|
|
64
|
-
let current = walker.nextNode();
|
|
65
|
-
while (current) {
|
|
66
|
-
textNodes.push(current as Text);
|
|
67
|
-
current = walker.nextNode();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return textNodes;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const wrapMatches = (text: string, regex: RegExp): DocumentFragment => {
|
|
74
|
-
const fragment = document.createDocumentFragment();
|
|
75
|
-
let lastIndex = 0;
|
|
76
|
-
let match = regex.exec(text);
|
|
77
|
-
|
|
78
|
-
while (match !== null) {
|
|
79
|
-
if (match.index > lastIndex) {
|
|
80
|
-
fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const mark = document.createElement("mark");
|
|
84
|
-
mark.className = KEYWORD_HIGHLIGHT_CLASS;
|
|
85
|
-
mark.textContent = match[0];
|
|
86
|
-
fragment.appendChild(mark);
|
|
87
|
-
|
|
88
|
-
lastIndex = regex.lastIndex;
|
|
89
|
-
match = regex.exec(text);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (lastIndex < text.length) {
|
|
93
|
-
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return fragment;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const highlightKeywords = (container: HTMLElement, keywords: string[]): void => {
|
|
100
|
-
removeKeywordHighlights(container);
|
|
101
|
-
|
|
102
|
-
const regex = buildKeywordsRegex(keywords);
|
|
103
|
-
if (!regex) return;
|
|
104
|
-
|
|
105
|
-
const textNodes = collectTextNodes(container);
|
|
106
|
-
|
|
107
|
-
for (const textNode of textNodes) {
|
|
108
|
-
const text = textNode.textContent || "";
|
|
109
|
-
if (!regex.test(text)) continue;
|
|
110
|
-
regex.lastIndex = 0;
|
|
111
|
-
|
|
112
|
-
const fragment = wrapMatches(text, regex);
|
|
113
|
-
textNode.parentNode?.replaceChild(fragment, textNode);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const observeAndHighlightKeywords = (container: HTMLElement, keywords: string[]): (() => void) => {
|
|
118
|
-
const observer = new MutationObserver(() => {
|
|
119
|
-
observer.disconnect();
|
|
120
|
-
highlightKeywords(container, keywords);
|
|
121
|
-
observer.observe(container, { childList: true, subtree: true });
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
highlightKeywords(container, keywords);
|
|
125
|
-
observer.observe(container, { childList: true, subtree: true });
|
|
126
|
-
|
|
127
|
-
return () => {
|
|
128
|
-
observer.disconnect();
|
|
129
|
-
removeKeywordHighlights(container);
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
export {
|
|
134
|
-
addIdsToHeadings,
|
|
135
|
-
observeAndAddIdsToHeadings,
|
|
136
|
-
highlightKeywords,
|
|
137
|
-
removeKeywordHighlights,
|
|
138
|
-
observeAndHighlightKeywords,
|
|
139
|
-
KEYWORD_HIGHLIGHT_CLASS,
|
|
140
|
-
};
|