@pixelersagency/okno-bridge 1.0.0-beta.1
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/LICENSE +338 -0
- package/README.md +121 -0
- package/dist/okno-bridge.js +592 -0
- package/package.json +56 -0
- package/src/index.js +565 -0
- package/src/react.js +38 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* okno-bridge v5 — standalone (auto-init).
|
|
3
|
+
* Généré par bridge/build.js depuis src/index.js — ne pas éditer à la main.
|
|
4
|
+
*
|
|
5
|
+
* Chargement recommandé (le bridge ne sert qu'à l'éditeur) :
|
|
6
|
+
* <script>
|
|
7
|
+
* if (window.self !== window.top) {
|
|
8
|
+
* var s = document.createElement('script');
|
|
9
|
+
* s.src = '/okno-bridge.js';
|
|
10
|
+
* s.setAttribute('data-wp-origin', 'https://admin.monsite.fr');
|
|
11
|
+
* document.head.appendChild(s);
|
|
12
|
+
* }
|
|
13
|
+
* </script>
|
|
14
|
+
*/
|
|
15
|
+
( function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @pixelersagency/okno-bridge v5 — pont postMessage entre un site et l'éditeur Okno.
|
|
20
|
+
*
|
|
21
|
+
* Fonctionne avec n'importe quel front : HTML statique, Astro, Next.js, Remix,
|
|
22
|
+
* React (SPA ou rendu serveur), Vue/Nuxt, Svelte/SvelteKit.
|
|
23
|
+
*
|
|
24
|
+
* Usage (ESM) :
|
|
25
|
+
* if ( window.self !== window.top ) {
|
|
26
|
+
* const { initOknoBridge } = await import( '@pixelersagency/okno-bridge' );
|
|
27
|
+
* initOknoBridge( { wpOrigin: 'https://admin.monsite.fr' } );
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* React : import { OknoBridge } from '@pixelersagency/okno-bridge/react' — voir src/react.js.
|
|
31
|
+
*
|
|
32
|
+
* Le bridge ne fait RIEN hors iframe. Il n'appelle jamais l'API WordPress :
|
|
33
|
+
* les valeurs arrivent de wp-admin par postMessage, avec vérification stricte
|
|
34
|
+
* de l'origine des deux côtés.
|
|
35
|
+
*
|
|
36
|
+
* Annotations :
|
|
37
|
+
* data-wp-post="12" conteneur : ID du contenu WordPress
|
|
38
|
+
* data-wp-field="chemin" élément éditable (chemins pointés : cartes.0.titre)
|
|
39
|
+
* data-okno-apply="html" la valeur est du HTML (balisage interne conservé)
|
|
40
|
+
* data-okno-section="Libellé" section logique (arbre de structure)
|
|
41
|
+
* data-okno-layout="sections.2" section portée par une ligne de flexible content
|
|
42
|
+
* data-okno-refresh="save" pas d'aperçu instantané, rechargement après enregistrement
|
|
43
|
+
* data-okno-style="prop" champ couleur : propriété CSS ciblée
|
|
44
|
+
* data-okno-managed="menu" contenu géré ailleurs : l'éditeur explique où le modifier
|
|
45
|
+
* data-okno-managed-label="…" libellé de la région gérée
|
|
46
|
+
* data-okno-edit-url="…" écran wp-admin où modifier ce contenu
|
|
47
|
+
*
|
|
48
|
+
* Sites React / SPA — ce qui change par rapport à une page statique :
|
|
49
|
+
* - aucun écouteur n'est posé sur les éléments : tout passe par délégation
|
|
50
|
+
* sur document, donc un nœud recréé par un re-render reste cliquable ;
|
|
51
|
+
* - les valeurs d'aperçu sont gardées en mémoire et réappliquées après chaque
|
|
52
|
+
* mutation du DOM : un re-render qui remet la valeur serveur ne l'efface pas ;
|
|
53
|
+
* - le texte est écrit dans le nœud texte existant (nodeValue) plutôt qu'en
|
|
54
|
+
* remplaçant les enfants, pour ne pas casser la réconciliation de React ;
|
|
55
|
+
* - la navigation client (pushState, popstate) déclenche un nouveau handshake.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const NS = 'okno';
|
|
59
|
+
const VERSION = 5;
|
|
60
|
+
const HOVER_CLASS = 'okno-hover';
|
|
61
|
+
const MANAGED_CLASS = 'okno-managed-hover';
|
|
62
|
+
const CLOAK_CLASS = 'okno-cloak';
|
|
63
|
+
|
|
64
|
+
/** Attente de stabilisation du DOM avant re-collecte (rendus successifs de React). */
|
|
65
|
+
const SETTLE_MS = 120;
|
|
66
|
+
|
|
67
|
+
/** Noms lisibles des fournisseurs de contenu courants. */
|
|
68
|
+
const PROVIDERS = {
|
|
69
|
+
wordpress: 'WordPress',
|
|
70
|
+
menu: 'Menus WordPress',
|
|
71
|
+
acf: 'ACF',
|
|
72
|
+
woocommerce: 'WooCommerce',
|
|
73
|
+
jetengine: 'JetEngine',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Balises retirées de tout HTML injecté dans l'aperçu. */
|
|
77
|
+
const BANNED_TAGS = [ 'script', 'style', 'iframe', 'object', 'embed', 'link', 'meta', 'base', 'form' ];
|
|
78
|
+
|
|
79
|
+
function initOknoBridge( options ) {
|
|
80
|
+
if ( typeof window === 'undefined' || window.self === window.top ) {
|
|
81
|
+
return; // Jamais actif en navigation publique ni côté serveur.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Idempotent : React StrictMode monte les effets deux fois en développement.
|
|
85
|
+
if ( window.__oknoBridge ) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const wpOrigin = options && options.wpOrigin;
|
|
90
|
+
if ( ! wpOrigin ) {
|
|
91
|
+
console.warn( '[okno] wpOrigin manquant : bridge inactif.' );
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ( document.referrer ) {
|
|
96
|
+
try {
|
|
97
|
+
if ( new URL( document.referrer ).origin !== wpOrigin ) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
} catch ( e ) {
|
|
101
|
+
/* referrer illisible : les contrôles d'origine suffisent. */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
window.__oknoBridge = { version: VERSION };
|
|
106
|
+
|
|
107
|
+
/* ------------------------------------------------------------------ *
|
|
108
|
+
* État
|
|
109
|
+
* ------------------------------------------------------------------ */
|
|
110
|
+
|
|
111
|
+
/** Valeurs d'aperçu en cours : "postId:path" -> { value, fieldType }. */
|
|
112
|
+
const overrides = new Map();
|
|
113
|
+
|
|
114
|
+
/** Dernière collecte : champs et sections présents dans le DOM. */
|
|
115
|
+
let fields = new Map();
|
|
116
|
+
let sections = [];
|
|
117
|
+
let signature = '';
|
|
118
|
+
let currentUrl = window.location.href;
|
|
119
|
+
let applying = false;
|
|
120
|
+
let settleTimer = null;
|
|
121
|
+
|
|
122
|
+
/* ------------------------------------------------------------------ *
|
|
123
|
+
* Cloak : page masquée jusqu'aux valeurs de l'éditeur (chargement initial)
|
|
124
|
+
* ------------------------------------------------------------------ */
|
|
125
|
+
|
|
126
|
+
const style = document.createElement( 'style' );
|
|
127
|
+
style.textContent =
|
|
128
|
+
'.' + CLOAK_CLASS + '{opacity:0 !important}' +
|
|
129
|
+
'.' + HOVER_CLASS + '{outline:2px solid #5b5bd6 !important;outline-offset:2px;cursor:pointer !important}' +
|
|
130
|
+
'.' + MANAGED_CLASS + '{outline:2px dashed #b7791f !important;outline-offset:2px}';
|
|
131
|
+
document.head.appendChild( style );
|
|
132
|
+
|
|
133
|
+
let cloakTimer = null;
|
|
134
|
+
const cloakDelay = options && 'cloakTimeout' in options ? Number( options.cloakTimeout ) : 4000;
|
|
135
|
+
|
|
136
|
+
function uncloak() {
|
|
137
|
+
clearTimeout( cloakTimer );
|
|
138
|
+
document.documentElement.classList.remove( CLOAK_CLASS );
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if ( cloakDelay > 0 ) {
|
|
142
|
+
document.documentElement.classList.add( CLOAK_CLASS );
|
|
143
|
+
cloakTimer = setTimeout( () => {
|
|
144
|
+
uncloak();
|
|
145
|
+
send( { type: 'cloakTimeout' } );
|
|
146
|
+
console.warn( '[okno] wp-admin n’a pas répondu : aperçu affiché sans hydratation.' );
|
|
147
|
+
}, cloakDelay );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ------------------------------------------------------------------ *
|
|
151
|
+
* Protocole
|
|
152
|
+
* ------------------------------------------------------------------ */
|
|
153
|
+
|
|
154
|
+
function send( message ) {
|
|
155
|
+
message.ns = NS;
|
|
156
|
+
window.parent.postMessage( message, wpOrigin );
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ------------------------------------------------------------------ *
|
|
160
|
+
* Collecte
|
|
161
|
+
* ------------------------------------------------------------------ */
|
|
162
|
+
|
|
163
|
+
function postIdOf( el ) {
|
|
164
|
+
const host = el.closest( '[data-wp-post]' );
|
|
165
|
+
return host ? host.getAttribute( 'data-wp-post' ) : '';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sectionLabel( el, index ) {
|
|
169
|
+
const explicit = el.getAttribute( 'data-okno-section' ) || el.getAttribute( 'aria-label' );
|
|
170
|
+
if ( explicit ) {
|
|
171
|
+
return explicit;
|
|
172
|
+
}
|
|
173
|
+
const tag = el.tagName.toLowerCase();
|
|
174
|
+
if ( 'header' === tag ) {
|
|
175
|
+
return 'En-tête';
|
|
176
|
+
}
|
|
177
|
+
if ( 'footer' === tag ) {
|
|
178
|
+
return 'Pied de page';
|
|
179
|
+
}
|
|
180
|
+
const heading = el.querySelector( 'h1, h2, h3' );
|
|
181
|
+
if ( heading && heading.textContent.trim() ) {
|
|
182
|
+
const text = heading.textContent.trim().replace( /\s+/g, ' ' );
|
|
183
|
+
return text.length > 48 ? text.slice( 0, 45 ) + '…' : text;
|
|
184
|
+
}
|
|
185
|
+
return 'Section ' + ( index + 1 );
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sectionLayout( el ) {
|
|
189
|
+
const raw = el.getAttribute( 'data-okno-layout' );
|
|
190
|
+
if ( ! raw ) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const parts = raw.split( '.' );
|
|
194
|
+
const index = Number( parts[ parts.length - 1 ] );
|
|
195
|
+
if ( parts.length < 2 || isNaN( index ) ) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return { post: postIdOf( el ) || null, path: parts.slice( 0, -1 ).join( '.' ), index };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collect() {
|
|
202
|
+
const nextFields = new Map();
|
|
203
|
+
const nextSections = [];
|
|
204
|
+
|
|
205
|
+
document.querySelectorAll( '[data-okno-sid]' ).forEach( ( el ) => el.removeAttribute( 'data-okno-sid' ) );
|
|
206
|
+
|
|
207
|
+
// 1. Sections annotées explicitement : elles priment, même à l'intérieur
|
|
208
|
+
// d'une balise <section> générique (sauf imbriquées dans une autre annotée).
|
|
209
|
+
const EXPLICIT = '[data-okno-section], [data-okno-layout]';
|
|
210
|
+
const explicit = [ ...document.querySelectorAll( EXPLICIT ) ].filter(
|
|
211
|
+
( el ) => ! ( el.parentElement && el.parentElement.closest( EXPLICIT ) )
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// 2. Détection automatique (section, header, footer) là où rien n'est annoté :
|
|
215
|
+
// ni à l'intérieur d'une section retenue, ni autour d'une section annotée.
|
|
216
|
+
const auto = [];
|
|
217
|
+
document.querySelectorAll( 'section, header, footer' ).forEach( ( el ) => {
|
|
218
|
+
if ( el.matches( EXPLICIT ) || el.closest( EXPLICIT ) || el.querySelector( EXPLICIT ) ) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if ( auto.some( ( kept ) => kept.contains( el ) ) ) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
auto.push( el );
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
// Ordre du document, identifiants positionnels.
|
|
228
|
+
explicit
|
|
229
|
+
.concat( auto )
|
|
230
|
+
.sort( ( a, b ) => ( a.compareDocumentPosition( b ) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 ) )
|
|
231
|
+
.forEach( ( el ) => {
|
|
232
|
+
const id = 's' + nextSections.length;
|
|
233
|
+
el.setAttribute( 'data-okno-sid', id );
|
|
234
|
+
nextSections.push( {
|
|
235
|
+
id,
|
|
236
|
+
label: sectionLabel( el, nextSections.length ),
|
|
237
|
+
layout: sectionLayout( el ),
|
|
238
|
+
el,
|
|
239
|
+
fields: [],
|
|
240
|
+
} );
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
document.querySelectorAll( '[data-wp-field]' ).forEach( ( el ) => {
|
|
244
|
+
const postId = postIdOf( el );
|
|
245
|
+
const path = el.getAttribute( 'data-wp-field' );
|
|
246
|
+
if ( ! postId || ! path ) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const key = postId + ':' + path;
|
|
250
|
+
let entry = nextFields.get( key );
|
|
251
|
+
if ( ! entry ) {
|
|
252
|
+
entry = {
|
|
253
|
+
postId,
|
|
254
|
+
path,
|
|
255
|
+
refresh: 'save' === el.getAttribute( 'data-okno-refresh' ),
|
|
256
|
+
els: [],
|
|
257
|
+
};
|
|
258
|
+
nextFields.set( key, entry );
|
|
259
|
+
|
|
260
|
+
const host = el.closest( '[data-okno-sid]' );
|
|
261
|
+
const section = host && nextSections.find( ( s ) => s.id === host.getAttribute( 'data-okno-sid' ) );
|
|
262
|
+
if ( section ) {
|
|
263
|
+
section.fields.push( { post: postId, path, refresh: entry.refresh } );
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
entry.els.push( el );
|
|
267
|
+
} );
|
|
268
|
+
|
|
269
|
+
fields = nextFields;
|
|
270
|
+
sections = nextSections;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function computeSignature() {
|
|
274
|
+
return JSON.stringify( [
|
|
275
|
+
window.location.pathname,
|
|
276
|
+
[ ...fields.keys() ].sort(),
|
|
277
|
+
sections.map( ( s ) => [ s.label, s.layout && s.layout.path, s.layout && s.layout.index ] ),
|
|
278
|
+
] );
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ------------------------------------------------------------------ *
|
|
282
|
+
* Application des valeurs
|
|
283
|
+
* ------------------------------------------------------------------ */
|
|
284
|
+
|
|
285
|
+
function sanitizeHtml( html ) {
|
|
286
|
+
const doc = document.implementation.createHTMLDocument( '' );
|
|
287
|
+
doc.body.innerHTML = String( null == html ? '' : html );
|
|
288
|
+
doc.body.querySelectorAll( BANNED_TAGS.join( ',' ) ).forEach( ( el ) => el.remove() );
|
|
289
|
+
doc.body.querySelectorAll( '*' ).forEach( ( el ) => {
|
|
290
|
+
[ ...el.attributes ].forEach( ( attr ) => {
|
|
291
|
+
const name = attr.name.toLowerCase();
|
|
292
|
+
const value = attr.value.replace( /\s+/g, '' ).toLowerCase();
|
|
293
|
+
if ( 0 === name.indexOf( 'on' ) ) {
|
|
294
|
+
el.removeAttribute( attr.name );
|
|
295
|
+
} else if ( ( 'href' === name || 'src' === name || 'xlink:href' === name ) && 0 === value.indexOf( 'javascript:' ) ) {
|
|
296
|
+
el.removeAttribute( attr.name );
|
|
297
|
+
}
|
|
298
|
+
} );
|
|
299
|
+
} );
|
|
300
|
+
return doc.body.innerHTML;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Écrit un texte sans détruire les nœuds que React référence : si l'élément
|
|
305
|
+
* contient un seul nœud texte, on change sa valeur en place.
|
|
306
|
+
*/
|
|
307
|
+
function setText( el, value ) {
|
|
308
|
+
const text = null == value ? '' : String( value );
|
|
309
|
+
const only = el.childNodes.length === 1 ? el.firstChild : null;
|
|
310
|
+
if ( only && only.nodeType === Node.TEXT_NODE ) {
|
|
311
|
+
if ( only.nodeValue !== text ) {
|
|
312
|
+
only.nodeValue = text;
|
|
313
|
+
}
|
|
314
|
+
} else if ( el.textContent !== text ) {
|
|
315
|
+
el.textContent = text;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function applyToElement( el, value, fieldType ) {
|
|
320
|
+
if ( 'image' === fieldType ) {
|
|
321
|
+
const img = 'IMG' === el.tagName ? el : el.querySelector( 'img' );
|
|
322
|
+
if ( img ) {
|
|
323
|
+
if ( img.getAttribute( 'src' ) !== ( value || '' ) ) {
|
|
324
|
+
img.setAttribute( 'src', value || '' );
|
|
325
|
+
img.removeAttribute( 'srcset' );
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
el.style.backgroundImage = value ? 'url("' + value + '")' : '';
|
|
329
|
+
}
|
|
330
|
+
} else if ( 'color_picker' === fieldType ) {
|
|
331
|
+
el.style.setProperty( el.getAttribute( 'data-okno-style' ) || 'color', value || '' );
|
|
332
|
+
} else if ( 'link' === fieldType ) {
|
|
333
|
+
const link = 'A' === el.tagName ? el : el.querySelector( 'a' );
|
|
334
|
+
const data = value && 'object' === typeof value ? value : { url: value };
|
|
335
|
+
if ( link ) {
|
|
336
|
+
link.setAttribute( 'href', data.url || '' );
|
|
337
|
+
if ( data.target ) {
|
|
338
|
+
link.setAttribute( 'target', data.target );
|
|
339
|
+
} else {
|
|
340
|
+
link.removeAttribute( 'target' );
|
|
341
|
+
}
|
|
342
|
+
if ( data.title ) {
|
|
343
|
+
setText( link, data.title );
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else if ( 'wysiwyg' === fieldType || 'html' === el.getAttribute( 'data-okno-apply' ) ) {
|
|
347
|
+
const html = sanitizeHtml( value );
|
|
348
|
+
if ( el.innerHTML !== html ) {
|
|
349
|
+
el.innerHTML = html;
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
setText( el, value );
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function applyOverrides() {
|
|
357
|
+
applying = true;
|
|
358
|
+
try {
|
|
359
|
+
overrides.forEach( ( item, key ) => {
|
|
360
|
+
const entry = fields.get( key );
|
|
361
|
+
if ( entry && ! entry.refresh ) {
|
|
362
|
+
entry.els.forEach( ( el ) => applyToElement( el, item.value, item.fieldType ) );
|
|
363
|
+
}
|
|
364
|
+
} );
|
|
365
|
+
} finally {
|
|
366
|
+
// Les mutations que l'on vient de provoquer arrivent en microtâche.
|
|
367
|
+
Promise.resolve().then( () => {
|
|
368
|
+
applying = false;
|
|
369
|
+
} );
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* ------------------------------------------------------------------ *
|
|
374
|
+
* Cycle de vie : handshake, mutations, navigation client
|
|
375
|
+
* ------------------------------------------------------------------ */
|
|
376
|
+
|
|
377
|
+
function announce() {
|
|
378
|
+
const posts = {};
|
|
379
|
+
fields.forEach( ( entry ) => {
|
|
380
|
+
if ( ! posts[ entry.postId ] ) {
|
|
381
|
+
posts[ entry.postId ] = [];
|
|
382
|
+
}
|
|
383
|
+
posts[ entry.postId ].push( { path: entry.path, refresh: entry.refresh, ratio: slotRatio( entry ) } );
|
|
384
|
+
} );
|
|
385
|
+
signature = computeSignature();
|
|
386
|
+
send( {
|
|
387
|
+
type: 'ready',
|
|
388
|
+
version: VERSION,
|
|
389
|
+
url: window.location.href,
|
|
390
|
+
posts,
|
|
391
|
+
sections: sections.map( ( s ) => ( { id: s.id, label: s.label, layout: s.layout, fields: s.fields } ) ),
|
|
392
|
+
} );
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function slotRatio( entry ) {
|
|
396
|
+
const el = entry.els[ 0 ];
|
|
397
|
+
if ( ! el ) {
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
const target = 'IMG' === el.tagName ? el : el.querySelector( 'img' ) || el;
|
|
401
|
+
const box = target.getBoundingClientRect();
|
|
402
|
+
if ( box.width < 8 || box.height < 8 ) {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
return Math.round( ( box.width / box.height ) * 1000 ) / 1000;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function settle() {
|
|
409
|
+
clearTimeout( settleTimer );
|
|
410
|
+
settleTimer = setTimeout( () => {
|
|
411
|
+
if ( window.location.href !== currentUrl ) {
|
|
412
|
+
// Changement de page côté client : les valeurs d'aperçu de la page
|
|
413
|
+
// précédente ne concernent plus rien.
|
|
414
|
+
currentUrl = window.location.href;
|
|
415
|
+
overrides.clear();
|
|
416
|
+
}
|
|
417
|
+
collect();
|
|
418
|
+
applyOverrides();
|
|
419
|
+
if ( computeSignature() !== signature ) {
|
|
420
|
+
announce();
|
|
421
|
+
}
|
|
422
|
+
}, SETTLE_MS );
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const observer = new MutationObserver( () => {
|
|
426
|
+
if ( ! applying ) {
|
|
427
|
+
settle();
|
|
428
|
+
}
|
|
429
|
+
} );
|
|
430
|
+
|
|
431
|
+
[ 'pushState', 'replaceState' ].forEach( ( method ) => {
|
|
432
|
+
const original = window.history[ method ];
|
|
433
|
+
window.history[ method ] = function () {
|
|
434
|
+
const result = original.apply( this, arguments );
|
|
435
|
+
settle();
|
|
436
|
+
return result;
|
|
437
|
+
};
|
|
438
|
+
} );
|
|
439
|
+
window.addEventListener( 'popstate', settle );
|
|
440
|
+
|
|
441
|
+
/* ------------------------------------------------------------------ *
|
|
442
|
+
* Interactions, par délégation (survivent aux re-renders)
|
|
443
|
+
* ------------------------------------------------------------------ */
|
|
444
|
+
|
|
445
|
+
function fieldTarget( node ) {
|
|
446
|
+
const el = node && node.closest ? node.closest( '[data-wp-field]' ) : null;
|
|
447
|
+
return el && postIdOf( el ) ? el : null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
document.addEventListener( 'mouseover', ( event ) => {
|
|
451
|
+
const field = fieldTarget( event.target );
|
|
452
|
+
const managed = ! field && event.target.closest ? event.target.closest( '[data-okno-managed]' ) : null;
|
|
453
|
+
document.querySelectorAll( '.' + HOVER_CLASS + ', .' + MANAGED_CLASS ).forEach( ( el ) => {
|
|
454
|
+
if ( el !== field && el !== managed ) {
|
|
455
|
+
el.classList.remove( HOVER_CLASS, MANAGED_CLASS );
|
|
456
|
+
}
|
|
457
|
+
} );
|
|
458
|
+
if ( field ) {
|
|
459
|
+
field.classList.add( HOVER_CLASS );
|
|
460
|
+
} else if ( managed ) {
|
|
461
|
+
managed.classList.add( MANAGED_CLASS );
|
|
462
|
+
}
|
|
463
|
+
} );
|
|
464
|
+
|
|
465
|
+
document.addEventListener(
|
|
466
|
+
'click',
|
|
467
|
+
( event ) => {
|
|
468
|
+
const target = event.target;
|
|
469
|
+
if ( ! target || ! target.closest ) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 1. Un champ éditable, même à l'intérieur d'un lien : c'est lui qu'on vise.
|
|
474
|
+
const field = fieldTarget( target );
|
|
475
|
+
if ( field ) {
|
|
476
|
+
event.preventDefault();
|
|
477
|
+
event.stopPropagation();
|
|
478
|
+
send( { type: 'fieldFocus', postId: postIdOf( field ), path: field.getAttribute( 'data-wp-field' ) } );
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 2. Un lien : l'éditeur décide (bascule de page, lien externe…). Capté en
|
|
483
|
+
// phase de capture, donc avant le routeur du framework (Next, React Router).
|
|
484
|
+
const link = target.closest( 'a' );
|
|
485
|
+
if ( link ) {
|
|
486
|
+
event.preventDefault();
|
|
487
|
+
event.stopPropagation();
|
|
488
|
+
const href = link.getAttribute( 'href' ) || '';
|
|
489
|
+
if ( ! href || 0 === href.indexOf( '#' ) ) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
let url;
|
|
493
|
+
try {
|
|
494
|
+
url = new URL( href, window.location.href );
|
|
495
|
+
} catch ( e ) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
send( { type: 'navigate', href: url.href, path: url.pathname, external: url.origin !== window.location.origin } );
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 3. Un contenu géré ailleurs.
|
|
503
|
+
const managed = target.closest( '[data-okno-managed]' );
|
|
504
|
+
if ( managed ) {
|
|
505
|
+
event.preventDefault();
|
|
506
|
+
event.stopPropagation();
|
|
507
|
+
const provider = managed.getAttribute( 'data-okno-managed' ) || '';
|
|
508
|
+
send( {
|
|
509
|
+
type: 'managedFocus',
|
|
510
|
+
provider,
|
|
511
|
+
label: managed.getAttribute( 'data-okno-managed-label' ) || 'Géré dans ' + ( PROVIDERS[ provider ] || provider || 'WordPress' ),
|
|
512
|
+
editUrl: managed.getAttribute( 'data-okno-edit-url' ) || '',
|
|
513
|
+
} );
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 4. Une section.
|
|
518
|
+
const section = target.closest( '[data-okno-sid]' );
|
|
519
|
+
if ( section ) {
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
send( { type: 'sectionFocus', id: section.getAttribute( 'data-okno-sid' ) } );
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
true
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Les formulaires du site ne doivent rien envoyer depuis l'éditeur.
|
|
528
|
+
document.addEventListener( 'submit', ( event ) => event.preventDefault(), true );
|
|
529
|
+
|
|
530
|
+
function flash( el ) {
|
|
531
|
+
el.scrollIntoView( { behavior: 'smooth', block: 'center' } );
|
|
532
|
+
el.classList.add( HOVER_CLASS );
|
|
533
|
+
setTimeout( () => el.classList.remove( HOVER_CLASS ), 1500 );
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
window.addEventListener( 'message', ( event ) => {
|
|
537
|
+
if ( event.origin !== wpOrigin || ! event.data || event.data.ns !== NS ) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const data = event.data;
|
|
541
|
+
|
|
542
|
+
if ( 'init' === data.type ) {
|
|
543
|
+
overrides.clear();
|
|
544
|
+
Object.keys( data.values || {} ).forEach( ( postId ) => {
|
|
545
|
+
const values = data.values[ postId ];
|
|
546
|
+
Object.keys( values ).forEach( ( path ) => {
|
|
547
|
+
overrides.set( postId + ':' + path, values[ path ] );
|
|
548
|
+
} );
|
|
549
|
+
} );
|
|
550
|
+
applyOverrides();
|
|
551
|
+
uncloak();
|
|
552
|
+
} else if ( 'fieldUpdate' === data.type ) {
|
|
553
|
+
overrides.set( data.postId + ':' + data.path, { value: data.value, fieldType: data.fieldType } );
|
|
554
|
+
applyOverrides();
|
|
555
|
+
} else if ( 'focusField' === data.type ) {
|
|
556
|
+
const entry = fields.get( data.postId + ':' + data.path );
|
|
557
|
+
if ( entry && entry.els[ 0 ] ) {
|
|
558
|
+
flash( entry.els[ 0 ] );
|
|
559
|
+
}
|
|
560
|
+
} else if ( 'focusSection' === data.type ) {
|
|
561
|
+
const section = sections.find( ( s ) => s.id === data.id );
|
|
562
|
+
if ( section ) {
|
|
563
|
+
flash( section.el );
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} );
|
|
567
|
+
|
|
568
|
+
function start() {
|
|
569
|
+
collect();
|
|
570
|
+
announce();
|
|
571
|
+
observer.observe( document.body, { childList: true, subtree: true, characterData: true } );
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if ( 'loading' === document.readyState ) {
|
|
575
|
+
document.addEventListener( 'DOMContentLoaded', start );
|
|
576
|
+
} else {
|
|
577
|
+
start();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
window.initOknoBridge = initOknoBridge;
|
|
582
|
+
|
|
583
|
+
// Auto-init depuis l'attribut du tag <script>.
|
|
584
|
+
var script = document.currentScript;
|
|
585
|
+
var origin = script && script.getAttribute( 'data-wp-origin' );
|
|
586
|
+
if ( origin ) {
|
|
587
|
+
initOknoBridge( {
|
|
588
|
+
wpOrigin: origin,
|
|
589
|
+
cloakTimeout: Number( script.getAttribute( 'data-okno-cloak-timeout' ) ) || undefined,
|
|
590
|
+
} );
|
|
591
|
+
}
|
|
592
|
+
} )();
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixelersagency/okno-bridge",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Pont postMessage de l'éditeur visuel Okno : rend n'importe quel site (React, Next.js, Remix, Vite, Nuxt, SvelteKit, Astro, HTML) éditable depuis WordPress. Zéro dépendance, inactif hors de l'éditeur.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"wordpress",
|
|
7
|
+
"headless",
|
|
8
|
+
"acf",
|
|
9
|
+
"visual-editing",
|
|
10
|
+
"react",
|
|
11
|
+
"nextjs",
|
|
12
|
+
"astro",
|
|
13
|
+
"remix",
|
|
14
|
+
"vite",
|
|
15
|
+
"nuxt",
|
|
16
|
+
"sveltekit"
|
|
17
|
+
],
|
|
18
|
+
"license": "GPL-2.0-or-later",
|
|
19
|
+
"author": "Pixelers (https://pixelers.fr)",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/okno-bridge.js",
|
|
22
|
+
"module": "./src/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./src/index.js",
|
|
26
|
+
"default": "./dist/okno-bridge.js"
|
|
27
|
+
},
|
|
28
|
+
"./react": {
|
|
29
|
+
"import": "./src/react.js"
|
|
30
|
+
},
|
|
31
|
+
"./standalone": "./dist/okno-bridge.js"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node build.js"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/pixelersagency/okno.git",
|
|
45
|
+
"directory": "bridge"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/pixelersagency/okno#readme",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": ">=17"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"react": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|