PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-form-builder / field-packs / static-text / _src / static-text-links.js

static-text-links.js in Booking Calendar 11.9, at includes/page-form-builder/field-packs/static-text/_src/static-text-links.js

704 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPBC BFB Static Text link-token editor and renderer helpers.
3 *
4 * @since 11.8.5
5 */
6 (function ( w, d ) {
7 'use strict';
8
9 var core = w.WPBC_BFB_Core || {};
10 var boot = w.WPBC_BFB_Static_Text_Links_Boot || {};
11
12 /**
13 * Return an empty array without relying on an Array constructor alias.
14 *
15 * @return {Array} Empty array.
16 */
17 function array() {
18 return [];
19 }
20
21 /**
22 * Return a localized editor string with a deterministic fallback.
23 *
24 * @param {string} key Localization key.
25 * @param {string} fallback English fallback.
26 *
27 * @return {string} Localized or fallback string.
28 */
29 function get_text( key, fallback ) {
30 return String( boot[ key ] || fallback || '' );
31 }
32
33 /**
34 * Escape text for HTML content.
35 *
36 * @param {*} raw_value Candidate value.
37 *
38 * @return {string} HTML-safe text.
39 */
40 function escape_html( raw_value ) {
41 var sanitize = core.WPBC_BFB_Sanitize || {};
42
43 if ( 'function' === typeof sanitize.escape_html ) {
44 return sanitize.escape_html( String( raw_value || '' ) );
45 }
46
47 return String( raw_value || '' )
48 .replace( /&/g, '&' )
49 .replace( /</g, '&lt;' )
50 .replace( />/g, '&gt;' )
51 .replace( /"/g, '&quot;' )
52 .replace( /'/g, '&#039;' );
53 }
54
55 /**
56 * Escape text for an HTML attribute.
57 *
58 * @param {*} raw_value Candidate value.
59 *
60 * @return {string} Attribute-safe text.
61 */
62 function escape_attr( raw_value ) {
63 return escape_html( raw_value );
64 }
65
66 /**
67 * Clone JSON-safe field data.
68 *
69 * @param {*} source_value Candidate value.
70 *
71 * @return {*} Cloned value, or the original value when cloning fails.
72 */
73 function clone_value( source_value ) {
74 try {
75 return JSON.parse( JSON.stringify( source_value ) );
76 } catch ( err ) {
77 return source_value;
78 }
79 }
80
81 /**
82 * Convert a candidate token key to the supported brace-token grammar.
83 *
84 * @param {*} raw_value Candidate token key.
85 *
86 * @return {string} Normalized token key.
87 */
88 function normalize_token_key( raw_value ) {
89 var safe_value = String( raw_value || '' ).toLowerCase().slice( 0, 64 );
90
91 safe_value = safe_value.replace( /[\s\-]+/g, '_' );
92 safe_value = safe_value.replace( /[^a-z0-9_]/g, '' );
93 safe_value = safe_value.replace( /_+/g, '_' );
94
95 return safe_value.replace( /^_+|_+$/g, '' );
96 }
97
98 /**
99 * Normalize a link type to an editor-supported value.
100 *
101 * @param {*} raw_type Candidate link type.
102 *
103 * @return {string} `url` or `anchor`.
104 */
105 function normalize_link_type( raw_type ) {
106 return 'anchor' === String( raw_type || '' ) ? 'anchor' : 'url';
107 }
108
109 /**
110 * Normalize a link target to an allow-listed value.
111 *
112 * @param {*} raw_target Candidate target.
113 *
114 * @return {string} `_self` or `_blank`.
115 */
116 function normalize_target( raw_target ) {
117 return '_self' === String( raw_target || '' ) ? '_self' : '_blank';
118 }
119
120 /**
121 * Normalize a space-delimited CSS class list.
122 *
123 * @param {*} raw_classes Candidate class list.
124 *
125 * @return {string} Sanitized class list.
126 */
127 function normalize_css_classes( raw_classes ) {
128 var sanitize = core.WPBC_BFB_Sanitize || {};
129 var classes = String( raw_classes || '' ).slice( 0, 200 );
130
131 if ( 'function' === typeof sanitize.sanitize_css_classlist ) {
132 return sanitize.sanitize_css_classlist( classes );
133 }
134
135 return classes
136 .split( /\s+/ )
137 .map(
138 function ( class_name ) {
139 return class_name.replace( /[^A-Za-z0-9_-]/g, '' );
140 }
141 )
142 .filter( Boolean )
143 .join( ' ' );
144 }
145
146 /**
147 * Normalize one URL or anchor destination for safe browser rendering.
148 *
149 * WordPress performs the authoritative save-time URL filtering. This client
150 * check prevents executable schemes from entering Builder preview markup.
151 *
152 * @param {*} raw_destination Candidate destination.
153 * @param {string} link_type Normalized link type.
154 *
155 * @return {string} Safe destination, or an empty string when rejected.
156 */
157 function normalize_destination( raw_destination, link_type ) {
158 var destination = String( raw_destination || '' ).trim().slice( 0, 2048 );
159 var compact_url;
160 var scheme_match;
161 var allowed_schemes = {
162 http : true,
163 https : true,
164 mailto : true,
165 tel : true,
166 ftp : true,
167 ftps : true
168 };
169
170 if ( 'anchor' === link_type ) {
171 return destination
172 .replace( /^#+/, '' )
173 .replace( /[^A-Za-z0-9_:\-.]/g, '' );
174 }
175
176 compact_url = destination.replace( /[\x00-\x20\x7f]+/g, '' );
177 scheme_match = compact_url.match( /^([a-z][a-z0-9+.-]*):/i );
178
179 if ( scheme_match && ! allowed_schemes[ String( scheme_match[ 1 ] || '' ).toLowerCase() ] ) {
180 return '';
181 }
182
183 return destination;
184 }
185
186 /**
187 * Normalize one executable-free link definition.
188 *
189 * @param {*} raw_link Candidate definition.
190 * @param {number} index Zero-based fallback index.
191 *
192 * @return {Object} Normalized definition.
193 */
194 function normalize_link( raw_link, index ) {
195 var link_obj = raw_link && 'object' === typeof raw_link ? raw_link : {};
196 var fallback_key = 'link_' + String( index + 1 );
197 var token_key = normalize_token_key( link_obj.key || '' ) || fallback_key;
198 var link_type = normalize_link_type( link_obj.link_type );
199 var visible_text = String( link_obj.text || '' ).trim().slice( 0, 300 );
200
201 return {
202 key : token_key,
203 text : visible_text || token_key.replace( /_/g, ' ' ),
204 link_type : link_type,
205 destination : normalize_destination( link_obj.destination, link_type ),
206 target : normalize_target( link_obj.target ),
207 cssclass : normalize_css_classes( link_obj.cssclass )
208 };
209 }
210
211 /**
212 * Normalize an ordered link collection and make token keys unique.
213 *
214 * @param {*} raw_links Candidate array or JSON string.
215 *
216 * @return {Array} Normalized ordered definitions.
217 */
218 function normalize_links( raw_links ) {
219 var parsed_links = raw_links;
220 var links = array();
221 var used_keys = {};
222 var index;
223
224 if ( 'string' === typeof parsed_links ) {
225 try {
226 parsed_links = JSON.parse( parsed_links );
227 } catch ( err ) {
228 parsed_links = array();
229 }
230 }
231
232 if ( ! Array.isArray( parsed_links ) ) {
233 return links;
234 }
235
236 for ( index = 0; index < parsed_links.length && index < 20; index++ ) {
237 var normalized_link = normalize_link( parsed_links[ index ], index );
238 var base_key = normalized_link.key;
239 var key_suffix = 2;
240
241 while ( used_keys[ normalized_link.key ] ) {
242 normalized_link.key = base_key.slice( 0, 60 ) + '_' + String( key_suffix );
243 key_suffix++;
244 }
245
246 used_keys[ normalized_link.key ] = true;
247 links.push( normalized_link );
248 }
249
250 return links;
251 }
252
253 /**
254 * Extract unique brace tokens from Static Text content.
255 *
256 * @param {*} raw_text Candidate content.
257 *
258 * @return {Array} Unique token keys in source order.
259 */
260 function extract_tokens( raw_text ) {
261 var token_regex = /\{([a-zA-Z0-9_]+)\}/g;
262 var token_match;
263 var tokens = array();
264 var used_keys = {};
265
266 while ( null !== ( token_match = token_regex.exec( String( raw_text || '' ) ) ) ) {
267 var token_key = normalize_token_key( token_match[ 1 ] );
268 if ( token_key && ! used_keys[ token_key ] ) {
269 used_keys[ token_key ] = true;
270 tokens.push( token_key );
271 }
272 }
273
274 return tokens;
275 }
276
277 /**
278 * Build a key-indexed link map.
279 *
280 * @param {Array} links Normalized definitions.
281 *
282 * @return {Object} Definition map.
283 */
284 function build_link_map( links ) {
285 var link_map = {};
286 var index;
287
288 for ( index = 0; index < links.length; index++ ) {
289 link_map[ links[ index ].key ] = links[ index ];
290 }
291
292 return link_map;
293 }
294
295 /**
296 * Build one escaped link for Builder preview or exported form markup.
297 *
298 * @param {Object} link_obj Normalized link definition.
299 * @param {boolean} preview_only Whether clicks must be disabled in Builder.
300 *
301 * @return {string} Escaped anchor HTML.
302 */
303 function build_link_html( link_obj, preview_only ) {
304 var visible_text = escape_html( link_obj.text || link_obj.key || '' );
305 var classes = 'wpbc_bfb__static_text_link';
306 var destination = normalize_destination( link_obj.destination, link_obj.link_type );
307 var html;
308
309 if ( link_obj.cssclass ) {
310 classes += ' ' + normalize_css_classes( link_obj.cssclass );
311 }
312
313 if ( 'anchor' === link_obj.link_type ) {
314 destination = destination ? '#' + destination : '#';
315 } else if ( ! destination ) {
316 destination = '#';
317 }
318
319 if ( preview_only ) {
320 html = '<a class="' + escape_attr( classes ) + '" aria-disabled="true" tabindex="-1"';
321 } else {
322 html = '<a href="' + escape_attr( destination ) + '" class="' + escape_attr( classes ) + '"';
323 }
324
325 if ( ! preview_only && 'url' === link_obj.link_type ) {
326 html += ' target="' + escape_attr( normalize_target( link_obj.target ) ) + '"';
327 if ( '_blank' === normalize_target( link_obj.target ) ) {
328 html += ' rel="noopener noreferrer"';
329 }
330 }
331
332 return html + '>' + visible_text + '</a>';
333 }
334
335 /**
336 * Render Static Text content with link-token replacements.
337 *
338 * @param {*} raw_text Static Text content.
339 * @param {*} raw_links Link definitions.
340 * @param {Object} options Rendering options.
341 * @param {boolean} options.allow_html Preserve legacy basic-HTML behavior.
342 * @param {boolean} options.nl2br Convert new lines when HTML is not allowed.
343 * @param {boolean} options.preview_only Disable link clicks in Builder.
344 *
345 * @return {string} Rendered HTML with escaped links and text.
346 */
347 function build_content_html( raw_text, raw_links, options ) {
348 var settings = options || {};
349 var source_text = String( raw_text || '' );
350 var normalized = normalize_links( raw_links );
351 var link_map = build_link_map( normalized );
352 var token_regex = /\{([a-zA-Z0-9_]+)\}/g;
353 var token_match;
354 var last_index = 0;
355 var rendered_html = '';
356
357 /**
358 * Render one non-token source segment with the field's legacy text rules.
359 *
360 * @param {string} text_segment Source segment.
361 *
362 * @return {string} Rendered segment.
363 */
364 function render_text_segment( text_segment ) {
365 var rendered_segment = settings.allow_html ? text_segment : escape_html( text_segment );
366
367 if ( ! settings.allow_html && settings.nl2br ) {
368 rendered_segment = rendered_segment.replace( /\n/g, '<br>' );
369 }
370
371 return rendered_segment;
372 }
373
374 while ( null !== ( token_match = token_regex.exec( source_text ) ) ) {
375 var token_key = normalize_token_key( token_match[ 1 ] );
376
377 rendered_html += render_text_segment( source_text.substring( last_index, token_match.index ) );
378 if ( token_key && link_map[ token_key ] ) {
379 rendered_html += build_link_html( link_map[ token_key ], !! settings.preview_only );
380 } else {
381 rendered_html += render_text_segment( token_match[ 0 ] );
382 }
383 last_index = token_match.index + token_match[ 0 ].length;
384 }
385
386 rendered_html += render_text_segment( source_text.substring( last_index ) );
387
388 return rendered_html;
389 }
390
391 /**
392 * Create a DOM element using textContent for visible text.
393 *
394 * @param {string} tag_name Element name.
395 * @param {Object} attrs Attribute map.
396 * @param {string} text Optional text.
397 *
398 * @return {HTMLElement} Created element.
399 */
400 function create_element( tag_name, attrs, text ) {
401 var element = d.createElement( tag_name );
402 var attr_name;
403
404 for ( attr_name in ( attrs || {} ) ) {
405 if ( Object.prototype.hasOwnProperty.call( attrs, attr_name ) ) {
406 element.setAttribute( attr_name, attrs[ attr_name ] );
407 }
408 }
409
410 if ( undefined !== text ) {
411 element.textContent = text;
412 }
413
414 return element;
415 }
416
417 /**
418 * Dispatch an Inspector input event after the hidden JSON writer changes.
419 *
420 * @param {HTMLElement} writer Hidden Inspector writer.
421 *
422 * @return {void}
423 */
424 function dispatch_writer_input( writer ) {
425 var input_event;
426
427 try {
428 input_event = new w.Event( 'input', { bubbles: true } );
429 } catch ( err ) {
430 input_event = d.createEvent( 'Event' );
431 input_event.initEvent( 'input', true, false );
432 }
433
434 writer.dispatchEvent( input_event );
435 }
436
437 /**
438 * Insert a token at the current cursor position in the Static Text input.
439 *
440 * @param {HTMLTextAreaElement} text_input Static Text control.
441 * @param {string} token_key Normalized token key.
442 *
443 * @return {void}
444 */
445 function insert_token_at_cursor( text_input, token_key ) {
446 var token_text = '{' + token_key + '}';
447 var start = Number.isInteger( text_input.selectionStart ) ? text_input.selectionStart : text_input.value.length;
448 var end = Number.isInteger( text_input.selectionEnd ) ? text_input.selectionEnd : start;
449
450 text_input.value = text_input.value.substring( 0, start ) + token_text + text_input.value.substring( end );
451 text_input.focus();
452 text_input.setSelectionRange( start + token_text.length, start + token_text.length );
453 dispatch_writer_input( text_input );
454 }
455
456 /**
457 * Render the Static Text link-definition Factory slot.
458 *
459 * @param {HTMLElement} slot_host Inspector slot host.
460 * @param {Object} context Inspector context with selected field data.
461 *
462 * @return {void}
463 */
464 function render_editor_slot( slot_host, context ) {
465 var field_data = context && context.data ? context.data : {};
466 var links = normalize_links( field_data.links );
467 var editor = create_element( 'div', { 'class': 'wpbc_bfb__static_text_links_editor' } );
468 var token_list = create_element( 'div', { 'class': 'wpbc_bfb__static_text_link_tokens' } );
469 var help = create_element( 'p', { 'class': 'wpbc_bfb__help' }, get_text( 'links_help', 'Use tokens inside braces, for example: {terms} or {privacy_policy}.' ) );
470 var status = create_element( 'p', { 'class': 'wpbc_bfb__help wpbc_bfb__static_text_link_status', 'aria-live': 'polite' } );
471 var rows_list = create_element( 'div', { 'class': 'wpbc_bfb__static_text_links_list' } );
472 var add_button = create_element( 'button', { 'type': 'button', 'class': 'button button-secondary' }, get_text( 'add_link', 'Add link' ) );
473 var toolbar = create_element( 'div', { 'class': 'wpbc_bfb__static_text_links_toolbar' } );
474 var writer = create_element( 'textarea', { 'class': 'inspector__input', 'data-inspector-key': 'links', 'hidden': 'hidden' } );
475 var panel = slot_host.closest( '#wpbc_bfb__inspector' ) || slot_host.closest( '.wpbc_bfb__inspector' );
476 var text_input = panel ? panel.querySelector( 'textarea[data-inspector-key="text"]' ) : null;
477
478 writer.value = JSON.stringify( links );
479 toolbar.appendChild( add_button );
480 editor.appendChild( token_list );
481 editor.appendChild( help );
482 editor.appendChild( status );
483 editor.appendChild( rows_list );
484 editor.appendChild( toolbar );
485 editor.appendChild( writer );
486 slot_host.appendChild( editor );
487
488 /**
489 * Persist normalized editor state through the standard Inspector writer.
490 *
491 * @return {void}
492 */
493 function commit_links() {
494 links = normalize_links( links );
495 writer.value = JSON.stringify( links );
496 dispatch_writer_input( writer );
497 render_tokens_and_status();
498 }
499
500 /**
501 * Return missing token definitions for the current Static Text content.
502 *
503 * @return {Array} Missing token keys.
504 */
505 function get_missing_tokens() {
506 var source_tokens = extract_tokens( text_input ? text_input.value : '' );
507 var link_map = build_link_map( normalize_links( links ) );
508
509 return source_tokens.filter(
510 function ( token_key ) {
511 return ! link_map[ token_key ];
512 }
513 );
514 }
515
516 /**
517 * Render token insertion buttons and current validation guidance.
518 *
519 * @return {void}
520 */
521 function render_tokens_and_status() {
522 var normalized_links = normalize_links( links );
523 var missing_tokens = get_missing_tokens();
524
525 token_list.textContent = '';
526 normalized_links.forEach(
527 function ( link_obj ) {
528 var token_button = create_element(
529 'button',
530 { 'type': 'button', 'class': 'button button-secondary button-small' },
531 '{' + link_obj.key + '}'
532 );
533
534 token_button.addEventListener(
535 'click',
536 function () {
537 if ( text_input ) {
538 insert_token_at_cursor( text_input, link_obj.key );
539 }
540 }
541 );
542 token_list.appendChild( token_button );
543 }
544 );
545
546 if ( missing_tokens.length ) {
547 status.textContent = get_text( 'missing_definitions', 'Add a link definition for: %s' ).replace( '%s', missing_tokens.map( function ( key ) { return '{' + key + '}'; } ).join( ', ' ) );
548 } else if ( ! normalized_links.length ) {
549 status.textContent = get_text( 'no_links', 'Add a link definition, then insert its token into the text.' );
550 } else {
551 status.textContent = '';
552 }
553 }
554
555 /**
556 * Build one labeled link-definition control.
557 *
558 * @param {string} label_text Control label.
559 * @param {HTMLElement} control Form control.
560 *
561 * @return {HTMLElement} Control wrapper.
562 */
563 function build_labeled_control( label_text, control ) {
564 var wrapper = create_element( 'label', { 'class': 'wpbc_bfb__static_text_link_control' } );
565 wrapper.appendChild( create_element( 'span', { 'class': 'wpbc_bfb__static_text_link_label' }, label_text ) );
566 wrapper.appendChild( control );
567 return wrapper;
568 }
569
570 /**
571 * Render every editable definition row from current state.
572 *
573 * @return {void}
574 */
575 function render_rows() {
576 links = normalize_links( links );
577 rows_list.textContent = '';
578
579 links.forEach(
580 function ( link_obj, row_index ) {
581 var row = create_element( 'div', { 'class': 'wpbc_bfb__static_text_link_row' } );
582 var key_input = create_element( 'input', { 'type': 'text', 'maxlength': '64', 'value': link_obj.key } );
583 var text_control = create_element( 'input', { 'type': 'text', 'maxlength': '300', 'value': link_obj.text } );
584 var type_select = create_element( 'select' );
585 var destination_input = create_element( 'input', { 'type': 'text', 'maxlength': '2048', 'value': link_obj.destination } );
586 var target_select = create_element( 'select' );
587 var css_input = create_element( 'input', { 'type': 'text', 'maxlength': '200', 'value': link_obj.cssclass } );
588 var actions = create_element( 'div', { 'class': 'wpbc_bfb__static_text_link_actions' } );
589 var duplicate_button = create_element( 'button', { 'type': 'button', 'class': 'button button-secondary button-small' }, get_text( 'duplicate', 'Duplicate' ) );
590 var remove_button = create_element( 'button', { 'type': 'button', 'class': 'button button-link-delete button-small' }, get_text( 'remove', 'Remove' ) );
591
592 type_select.appendChild( create_element( 'option', { 'value': 'url' }, get_text( 'url', 'URL' ) ) );
593 type_select.appendChild( create_element( 'option', { 'value': 'anchor' }, get_text( 'anchor', 'Anchor' ) ) );
594 type_select.value = link_obj.link_type;
595
596 target_select.appendChild( create_element( 'option', { 'value': '_self' }, '_self' ) );
597 target_select.appendChild( create_element( 'option', { 'value': '_blank' }, '_blank' ) );
598 target_select.value = link_obj.target;
599 target_select.disabled = 'anchor' === link_obj.link_type;
600
601 key_input.addEventListener( 'input', function () {
602 key_input.value = normalize_token_key( key_input.value );
603 links[ row_index ].key = key_input.value || 'link_' + String( row_index + 1 );
604 commit_links();
605 } );
606 text_control.addEventListener( 'input', function () {
607 links[ row_index ].text = text_control.value;
608 commit_links();
609 } );
610 type_select.addEventListener( 'change', function () {
611 links[ row_index ].link_type = normalize_link_type( type_select.value );
612 target_select.disabled = 'anchor' === links[ row_index ].link_type;
613 commit_links();
614 } );
615 destination_input.addEventListener( 'input', function () {
616 links[ row_index ].destination = destination_input.value;
617 commit_links();
618 } );
619 target_select.addEventListener( 'change', function () {
620 links[ row_index ].target = normalize_target( target_select.value );
621 commit_links();
622 } );
623 css_input.addEventListener( 'input', function () {
624 links[ row_index ].cssclass = css_input.value;
625 commit_links();
626 } );
627 duplicate_button.addEventListener( 'click', function () {
628 links.splice( row_index + 1, 0, clone_value( links[ row_index ] ) );
629 links = normalize_links( links );
630 render_rows();
631 commit_links();
632 } );
633 remove_button.addEventListener( 'click', function () {
634 links.splice( row_index, 1 );
635 render_rows();
636 commit_links();
637 } );
638
639 row.appendChild( build_labeled_control( get_text( 'token_key', 'Token Key' ), key_input ) );
640 row.appendChild( build_labeled_control( get_text( 'visible_text', 'Text' ), text_control ) );
641 row.appendChild( build_labeled_control( get_text( 'link_type', 'Type' ), type_select ) );
642 row.appendChild( build_labeled_control( get_text( 'destination', 'Destination' ), destination_input ) );
643 row.appendChild( build_labeled_control( get_text( 'target', 'Target' ), target_select ) );
644 row.appendChild( build_labeled_control( get_text( 'css_class', 'CSS class' ), css_input ) );
645 actions.appendChild( duplicate_button );
646 actions.appendChild( remove_button );
647 row.appendChild( actions );
648 rows_list.appendChild( row );
649 }
650 );
651 }
652
653 add_button.addEventListener(
654 'click',
655 function () {
656 var missing_tokens = get_missing_tokens();
657 var next_key = missing_tokens.length ? missing_tokens[ 0 ] : 'link_' + String( links.length + 1 );
658
659 links.push(
660 {
661 key : next_key,
662 text : next_key.replace( /_/g, ' ' ),
663 link_type : 'url',
664 destination : '',
665 target : '_blank',
666 cssclass : ''
667 }
668 );
669 render_rows();
670 commit_links();
671 }
672 );
673
674 if ( text_input ) {
675 text_input.addEventListener( 'input', render_tokens_and_status );
676 }
677
678 render_rows();
679 render_tokens_and_status();
680 }
681
682 /**
683 * Register the Static Text link editor with the Inspector Factory.
684 *
685 * @return {void}
686 */
687 function register_editor_slot() {
688 w.wpbc_bfb_inspector_factory_slots = w.wpbc_bfb_inspector_factory_slots || {};
689 w.wpbc_bfb_inspector_factory_slots.static_text_links = render_editor_slot;
690 }
691
692 w.WPBC_BFB_Static_Text_Links = {
693 build_content_html : build_content_html,
694 extract_tokens : extract_tokens,
695 normalize_destination : normalize_destination,
696 normalize_links : normalize_links,
697 normalize_token_key : normalize_token_key,
698 register_editor_slot : register_editor_slot
699 };
700
701 register_editor_slot();
702
703 })( window, document );
704