Back

100% Statements 29/29
100% Branches 12/12
100% Functions 15/15
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132                                        206x                   47x                   193x                 4x                 36x     3x                 11x 11x               14x               35x           3x           33x 33x   133x   21x   12x 12x     3x                   7x 5x   2x   2x   2x 2x 2x    
import { VALID_SECTIONS } from '../constants';
 
// Gutenberg 23.6 removed the `core/guidelines` store (the Guidelines
// singleton was dissolved into per-scope wp_knowledge rows, #79263), so the
// page's live draft text now exists only in React component state. The DOM
// textareas are the one place that state is observable from this bundle:
// reads snapshot them, and reactivity comes from a document-level `input`
// listener plus a MutationObserver that re-checks the watched values on DOM
// churn — Gutenberg writes some values through React with no input event
// (e.g. Clear guidelines resetting the form), but those updates always come
// with sibling DOM activity (confirm dialog closing, snackbar, button state).
 
/**
 * The textarea holding a section's draft, or null while the section
 * isn't rendered.
 *
 * @param {string} slug - Section slug (see VALID_SECTIONS).
 * @return {HTMLTextAreaElement|null} The section's textarea.
 */
export function getSectionTextarea( slug ) {
	return document.querySelector( `.guidelines__list-item[data-slug="${ slug }"] form textarea` );
}
 
/**
 * The block guideline modal's textarea, or null when absent.
 *
 * @param {HTMLElement|null} blockModal - The block guideline modal element.
 * @return {HTMLTextAreaElement|null} The modal's textarea.
 */
export function getBlockModalTextarea( blockModal ) {
	return blockModal?.querySelector( '.components-textarea-control__input' ) ?? null;
}
 
/**
 * The current draft text of a section, '' when absent.
 *
 * @param {string} slug - Section slug.
 * @return {string} The draft text.
 */
export function readSectionDraft( slug ) {
	return getSectionTextarea( slug )?.value || '';
}
 
/**
 * Current draft text for every valid section, keyed by slug.
 *
 * @return {Object} Slug-keyed draft map.
 */
export function readAllSectionDrafts() {
	return Object.fromEntries( VALID_SECTIONS.map( slug => [ slug, readSectionDraft( slug ) ] ) );
}
 
/**
 * Whether every section's draft is empty.
 *
 * @return {boolean} True when no section has text.
 */
export function areAllSectionDraftsEmpty() {
	return VALID_SECTIONS.every( slug => ! readSectionDraft( slug ) );
}
 
const listeners = new Set();
 
/**
 * Subscribe to draft-change notifications.
 *
 * @param {Function} callback - Invoked on every potential draft change.
 * @return {Function} Unsubscribe.
 */
export function subscribeToDrafts( callback ) {
	listeners.add( callback );
	return () => listeners.delete( callback );
}
 
/**
 * Notify subscribers that drafts may have changed. Subscribers re-read the
 * DOM, so spurious notifications are cheap no-ops.
 */
export function notifyDraftChange() {
	listeners.forEach( listener => listener() );
}
 
// Every watched value, in a fixed order; lets the change checks skip
// notifications for events that didn't touch any draft. Kept as an array and
// compared element-wise — joining would copy up to ~25KB of guideline text
// per check (5000 chars per section, see wp_guideline_max_length()).
function watchedValues() {
	return [
		...VALID_SECTIONS.map( readSectionDraft ),
		getBlockModalTextarea( document.querySelector( '.block-guideline-modal' ) )?.value || '',
	];
}
 
let lastWatched = [];
 
// Notify only when a watched value actually changed. Both event sources
// funnel through here, so typing in unrelated fields (the block combobox,
// admin search) and non-draft DOM churn cost one read pass and no fan-out.
function checkAndNotify() {
	const current = watchedValues();
	if (
		current.length === lastWatched.length &&
		current.every( ( value, i ) => value === lastWatched[ i ] )
	) {
		return;
	}
	lastWatched = current;
	notifyDraftChange();
}
 
let tracking = false;
 
/**
 * Start observing draft edits. Typing and our own setTextareaValue() both
 * dispatch bubbling `input` events (captured so a stopPropagation in the
 * page can't hide them). Value changes Gutenberg makes without input events
 * are caught by re-checking the watched values whenever the DOM mutates.
 * Either way, subscribers are notified only when a watched value changed.
 */
export function startDraftTracking() {
	if ( tracking ) {
		return;
	}
	tracking = true;
 
	document.addEventListener( 'input', checkAndNotify, true );
 
	const observer = new MutationObserver( checkAndNotify );
	observer.observe( document.body, { childList: true, subtree: true, attributes: true } );
	lastWatched = watchedValues();
}