• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / bd85d81a26932da045d4d1983aa26057e4220255

21 Aug 2024 10:40AM UTC coverage: 54.504% (-0.03%) from 54.53%
bd85d81a26932da045d4d1983aa26057e4220255

push

github

web-flow
Merge pull request #21548 from Yoast/partially-support-elementor-document-switching

Elementor: partially support document switching

7505 of 13560 branches covered (55.35%)

Branch coverage included in aggregate %.

93 of 333 new or added lines in 18 files covered. (27.93%)

1032 existing lines in 3 files now uncovered.

29831 of 54942 relevant lines covered (54.3%)

41563.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/packages/js/src/elementor/initializers/editor-watcher.js
1
/* global elementor, YoastSEO */
2
import { dispatch, select } from "@wordpress/data";
3
import { cleanForSlug } from "@wordpress/url";
4
import { debounce, get } from "lodash";
5
import { markers, Paper } from "yoastseo";
6
import { refreshDelay } from "../../analysis/constants";
7
import firstImageUrlInContent from "../../helpers/firstImageUrlInContent";
8
import { registerElementorUIHookAfter, registerElementorUIHookBefore } from "../helpers/hooks";
9
import { isFormId, isFormIdEqualToDocumentId } from "../helpers/is-form-id";
10

11
const editorData = {
×
12
        content: "",
13
        title: "",
14
        excerpt: "",
15
        slug: "",
16
        imageUrl: "",
17
};
18

19
const MARK_TAG = "yoastmark";
×
20

21
/**
22
 * Checks whether the given Elementor widget has Yoast marks.
23
 *
24
 * @param {Object} widget The widget.
25
 * @returns {boolean} Whether there are marks in the HTML of the widget.
26
 */
27
function widgetHasMarks( widget ) {
28
        return widget.innerHTML.indexOf( "<" + MARK_TAG ) !== -1;
×
29
}
30

31
/**
32
 * Retrieves all Elementor widget containers.
33
 * @returns {jQuery[]} Elementor widget containers.
34
 */
35
function getWidgetContainers() {
NEW
36
        return elementor.documents.getCurrent().$element.find( ".elementor-widget-container" );
×
37
}
38

39
/**
40
 * Removes all marks from Elementor widgets.
41
 *
42
 * @returns {void}
43
 */
44
function removeMarks() {
45
        getWidgetContainers().each( ( index, element ) => {
×
46
                if ( widgetHasMarks( element ) ) {
×
47
                        element.innerHTML = markers.removeMarks( element.innerHTML );
×
48
                }
49
        } );
50
}
51

52
/**
53
 * Gets the post content.
54
 *
55
 * @param {Document} editorDocument The current document.
56
 *
57
 * @returns {string} The post's content.
58
 */
59
function getContent( editorDocument ) {
60
        const content = [];
×
61

62
        editorDocument.$element.find( ".elementor-widget-container" ).each( ( index, element ) => {
×
63
                // We remove \n and \t from the HTML as Elementor formats the HTML after saving.
64
                // As this spacing is purely cosmetic, we can remove it for analysis purposes.
65
                // When we apply the marks, we do need to make the same amendments.
66
                const rawHtml = element.innerHTML.replace( /[\n\t]/g, "" ).trim();
×
67
                content.push( rawHtml );
×
68
        } );
69

70
        return content.join( "" );
×
71
}
72

73
/**
74
 * Gets the image URL. Searches for the first image in the content as fallback.
75
 *
76
 * @param {string} content The content to get an image URL as fallback.
77
 *
78
 * @returns {string} The image URL.
79
 */
80
function getImageUrl( content ) {
NEW
81
        const featuredImage = elementor.settings.page.model.get( "post_featured_image" );
×
82
        const url = get( featuredImage, "url", "" );
×
83

84
        if ( url === "" ) {
×
85
                return firstImageUrlInContent( content );
×
86
        }
87

88
        return url;
×
89
}
90

91
/**
92
 * Gets the data that is specific to this editor.
93
 *
94
 * @param {Document} editorDocument The current document.
95
 *
96
 * @returns {Object} The editorData object.
97
 */
98
function getEditorData( editorDocument ) {
99
        const content = getContent( editorDocument );
×
100

101
        return {
×
102
                content,
103
                title: elementor.settings.page.model.get( "post_title" ),
104
                excerpt: elementor.settings.page.model.get( "post_excerpt" ) || "",
×
105
                imageUrl: getImageUrl( content ),
106
                status: elementor.settings.page.model.get( "post_status" ),
107
        };
108
}
109

110
/* eslint-disable complexity */
111
/**
112
 * Dispatches new data when the editor is dirty.
113
 *
114
 * @returns {void}
115
 */
116
function handleEditorChange() {
NEW
117
        const currentDocument = elementor.documents.getCurrent();
×
NEW
118
        if ( ! currentDocument.$element ) {
×
NEW
119
                return;
×
120
        }
121

122
        // Quit early if the change was caused by switching out of the wp-post/page document.
123
        // This can happen when users go to Site Settings, for example.
124
        if ( ! [ "wp-post", "wp-page" ].includes( currentDocument.config.type ) ) {
×
125
                return;
×
126
        }
127

128
        // Quit early if the highlighting functionality is on.
129
        if ( select( "yoast-seo/editor" ).getActiveMarker() ) {
×
130
                return;
×
131
        }
132

133
        const data = getEditorData( currentDocument );
×
134

135
        if ( data.content !== editorData.content ) {
×
136
                editorData.content = data.content;
×
137
                dispatch( "yoast-seo/editor" ).setEditorDataContent( editorData.content );
×
138
        }
139

140
        if ( data.title !== editorData.title ) {
×
141
                editorData.title = data.title;
×
142
                dispatch( "yoast-seo/editor" ).setEditorDataTitle( editorData.title );
×
143

144
                if ( data.status === "draft" || data.status === "auto-draft" ) {
×
145
                        dispatch( "yoast-seo/editor" ).updateData( { slug: cleanForSlug( editorData.title ) } );
×
146
                }
147
        }
148

149
        if ( data.excerpt !== editorData.excerpt ) {
×
150
                editorData.excerpt = data.excerpt;
×
151
                dispatch( "yoast-seo/editor" ).setEditorDataExcerpt( editorData.excerpt );
×
152
        }
153

154
        if ( data.imageUrl !== editorData.imageUrl ) {
×
155
                editorData.imageUrl = data.imageUrl;
×
156
                dispatch( "yoast-seo/editor" ).setEditorDataImageUrl( editorData.imageUrl );
×
157
        }
158
}
159

160
/* eslint-enable complexity */
161

162

163
/**
164
 * Removes highlighting from Elementor widgets and reset the highlighting button.
165
 *
166
 * @returns {void}
167
 */
168
function resetMarks() {
169
        removeMarks();
×
170

171
        dispatch( "yoast-seo/editor" ).setActiveMarker( null );
×
172
        dispatch( "yoast-seo/editor" ).setMarkerPauseStatus( false );
×
173

NEW
174
        YoastSEO.analysis.applyMarks( new Paper( "", {} ), [] );
×
175
}
176

177
const debouncedHandleEditorChange = debounce( handleEditorChange, refreshDelay );
×
178

179
/**
180
 * Observes changes to the whole document through a MutationObserver.
181
 *
182
 * @returns {void}
183
 */
184
function observeChanges() {
NEW
185
        const observer = new MutationObserver( () => {
×
NEW
186
                if ( isFormIdEqualToDocumentId() ) {
×
NEW
187
                        debouncedHandleEditorChange();
×
188
                }
189
        } );
NEW
190
        observer.observe( document, { attributes: true, childList: true, subtree: true, characterData: true } );
×
191
}
192

193
/**
194
 * Initializes the watcher by coupling the change handlers to the change events.
195
 *
196
 * @returns {void}
197
 */
198
export default function initialize() {
199
        // This hook will fire 500ms after a widget is edited -- this allows Elementor to set the cursor at the end of the widget.
NEW
200
        registerElementorUIHookBefore( "panel/editor/open", "yoast-seo/marks/reset-on-edit", debounce( resetMarks, refreshDelay ), isFormIdEqualToDocumentId );
×
201
        // This hook will fire just before the document is saved.
NEW
202
        registerElementorUIHookBefore( "document/save/save", "yoast-seo/marks/reset-on-save", resetMarks, ( { document } ) => isFormId( document?.id || elementor.documents.getCurrent().id ) );
×
203

204
        // This hook will fire when the Elementor preview becomes available.
NEW
205
        registerElementorUIHookAfter( "editor/documents/attach-preview", "yoast-seo/content-scraper/initial", debouncedHandleEditorChange, isFormIdEqualToDocumentId );
×
NEW
206
        registerElementorUIHookAfter( "editor/documents/attach-preview", "yoast-seo/content-scraper", debounce( observeChanges, refreshDelay ), isFormIdEqualToDocumentId );
×
207

208
        // This hook will fire when the contents of the editor are modified.
NEW
209
        registerElementorUIHookAfter( "document/save/set-is-modified", "yoast-seo/content-scraper/on-modified", debouncedHandleEditorChange, ( { document } ) => isFormId( document?.id || elementor.documents.getCurrent().id ) );
×
210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc