Skip to content

Commit 40df6f9

Browse files
committed
Add Gutenberg editor support and cache fix for dots in license slugs
1 parent 8e07f06 commit 40df6f9

2 files changed

Lines changed: 147 additions & 2 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Preserve dots in license post slugs within the Gutenberg editor.
3+
*
4+
* Gutenberg's cleanForSlug (in wp.url) replaces dots with dashes,
5+
* which breaks SPDX identifiers like "Apache-2.0". This script:
6+
*
7+
* 1. Watches title changes via wp.data.subscribe and corrects the
8+
* slug when the title contains dots.
9+
* 2. Listens for focusout on the slug input (capture phase) to
10+
* preserve dots when a user manually edits the slug.
11+
*
12+
* This only runs on the license post-type editor screen (enqueue
13+
* is gated in PHP), so other post types are unaffected.
14+
*/
15+
( function() {
16+
var data = window.wp && window.wp.data;
17+
if ( ! data ) {
18+
return;
19+
}
20+
21+
var lastTitle = '';
22+
var lastSlug = '';
23+
var isUpdating = false;
24+
var pendingDottedSlug = '';
25+
26+
/**
27+
* Sanitize a string into a slug while preserving dots.
28+
*
29+
* @param {string} raw The raw string to slugify.
30+
* @return {string} The slugified string with dots intact.
31+
*/
32+
function slugifyWithDots( raw ) {
33+
return raw
34+
.toLowerCase()
35+
.replace( / |–|—/g, '-' )
36+
.replace( /[^\w.\s-]/g, '' )
37+
.replace( /[\s]+/g, '-' )
38+
.replace( /-+/g, '-' )
39+
.replace( /(^-+)|(-+$)/g, '' );
40+
}
41+
42+
/*
43+
* Capture-phase focusout on the document.
44+
*
45+
* Fires before React's synthetic onBlur, so we can read the raw
46+
* input value (with dots) before cleanForSlug strips them.
47+
* After a short delay (to let Gutenberg commit its sanitized slug),
48+
* we overwrite the store with the dotted version.
49+
*/
50+
document.addEventListener( 'focusout', function( e ) {
51+
var target = e.target;
52+
if ( ! target || 'INPUT' !== target.tagName ) {
53+
return;
54+
}
55+
56+
var urlWrapper = target.closest( '.editor-post-url__input' );
57+
if ( ! urlWrapper ) {
58+
return;
59+
}
60+
61+
var val = target.value || '';
62+
if ( val.indexOf( '.' ) === -1 ) {
63+
return;
64+
}
65+
66+
pendingDottedSlug = slugifyWithDots( val );
67+
68+
// Let Gutenberg commit its sanitized version, then overwrite.
69+
setTimeout( function() {
70+
if ( ! pendingDottedSlug ) {
71+
return;
72+
}
73+
isUpdating = true;
74+
data.dispatch( 'core/editor' ).editPost( { slug: pendingDottedSlug } );
75+
isUpdating = false;
76+
lastSlug = pendingDottedSlug;
77+
pendingDottedSlug = '';
78+
}, 100 );
79+
}, true );
80+
81+
/*
82+
* Store subscriber: fix title-driven slug generation.
83+
*
84+
* When the title changes and contains a dot, Gutenberg generates
85+
* a slug via cleanForSlug which strips the dot. We detect this
86+
* and dispatch the corrected dotted slug.
87+
*/
88+
data.subscribe( function() {
89+
if ( isUpdating ) {
90+
return;
91+
}
92+
93+
var editor = data.select( 'core/editor' );
94+
if ( ! editor ) {
95+
return;
96+
}
97+
98+
var title = editor.getEditedPostAttribute( 'title' ) || '';
99+
var slug = editor.getEditedPostAttribute( 'slug' ) || '';
100+
101+
// Title changed and contains a dot — fix the slug.
102+
if ( title !== lastTitle && title.indexOf( '.' ) !== -1 ) {
103+
lastTitle = title;
104+
105+
var dottedSlug = slugifyWithDots( title );
106+
107+
if ( slug !== dottedSlug && dottedSlug.indexOf( '.' ) !== -1 ) {
108+
isUpdating = true;
109+
data.dispatch( 'core/editor' ).editPost( { slug: dottedSlug } );
110+
isUpdating = false;
111+
lastSlug = dottedSlug;
112+
return;
113+
}
114+
}
115+
116+
lastTitle = title;
117+
lastSlug = slug;
118+
} );
119+
}() );

plugins/osi-features/inc/classes/post-types/class-post-type-license.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ protected function setup_hooks() {
5252
add_action( 'save_post_' . self::SLUG, array( $this, 'clear_slug_cache' ) );
5353
add_action( 'before_delete_post', array( $this, 'clear_slug_cache' ) );
5454
add_action( 'wp_trash_post', array( $this, 'clear_slug_cache' ) );
55+
56+
// Gutenberg: override cleanForSlug to preserve dots in the editor UI.
57+
add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_slug_script' ) );
5558
}
5659

5760
/**
@@ -163,13 +166,13 @@ public function restore_dots_on_query( string $title, string $raw_title, string
163166

164167
// Check if a license post with this dotted slug exists.
165168
global $wpdb;
166-
$exists = (bool) $wpdb->get_var(
169+
$exists = $wpdb->get_var(
167170
$wpdb->prepare(
168171
"SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = %s LIMIT 1",
169172
$dotted_slug,
170173
self::SLUG
171174
)
172-
);
175+
) ? 1 : 0;
173176

174177
wp_cache_set( $cache_key, $exists, self::CACHE_GROUP );
175178

@@ -197,6 +200,29 @@ public function clear_slug_cache( int $post_id ) {
197200
wp_cache_delete( $cache_key, self::CACHE_GROUP );
198201
}
199202

203+
/**
204+
* Enqueue the Gutenberg slug override script on the license editor screen.
205+
*
206+
* Subscribes to the editor data store and restores dots in the slug
207+
* when the post title contains dots (e.g., SPDX identifiers).
208+
*
209+
* @return void
210+
*/
211+
public function enqueue_editor_slug_script() {
212+
$screen = get_current_screen();
213+
if ( ! $screen || self::SLUG !== $screen->post_type ) {
214+
return;
215+
}
216+
217+
wp_enqueue_script(
218+
'osi-license-slug-dots',
219+
OSI_URL . '/assets/src/js/license-slug-dots.js',
220+
array( 'wp-data', 'wp-editor' ),
221+
filemtime( OSI_PATH . '/assets/src/js/license-slug-dots.js' ),
222+
true
223+
);
224+
}
225+
200226
/**
201227
* Sanitize a string as a slug while preserving dots.
202228
*

0 commit comments

Comments
 (0)