PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / resources / js / gutenberg-shortcodes.js

gutenberg-shortcodes.js in VikBooking Hotel Booking Engine & PMS trunk, at admin/resources/js/gutenberg-shortcodes.js

642 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function(wp) {
2
3 /**
4 * Registers a new block provided a unique name and an object defining its behavior.
5 * @link https://github.com/WordPress/gutenberg/tree/master/blocks#api
6 */
7 const registerBlockType = wp.blocks.registerBlockType;
8
9 /**
10 * Returns a new element of given type. Element is an abstraction layer atop React.
11 * @link https://github.com/WordPress/gutenberg/tree/master/packages/element#element
12 */
13 const el = wp.element.createElement;
14
15 /**
16 * Retrieves the translation of text.
17 * @link https://github.com/WordPress/gutenberg/tree/master/i18n#api
18 */
19 const __ = wp.i18n.__;
20
21 /**
22 * This variable is used to keep the very first shortcode
23 * after the loading of the page.
24 */
25 let currentShortcode = null;
26
27 /**
28 * This registry holds the information specified during the
29 * creation of a new shortcode.
30 */
31 let newShortcodeRegistry = null;
32
33 /**
34 * Every block starts by registering a new block type definition.
35 * @link https://wordpress.org/gutenberg/handbook/block-api/
36 */
37 registerBlockType('vikbooking/gutenberg-shortcodes', {
38 /**
39 * This is the block display title, which can be translated with `i18n` functions.
40 * The block inserter will show this name.
41 */
42 title: __('VikBooking Shortcode', 'vikbooking'),
43
44 /**
45 * This is the block description, which is displayed within the right sidebar.
46 */
47 description: __('Add a shortcode configured through VikBooking.', 'vikbooking'),
48
49 /**
50 * The icon can be a DASHICON or a SVG entity.
51 * NOTE: we need to use a different icon because Gutenberg seems
52 * to have problems in displaying the coffee icon.
53 */
54 icon: 'building',
55
56 /**
57 * Blocks are grouped into categories to help users browse and discover them.
58 * The categories provided by core are `common`, `embed`, `formatting`, `layout` and `widgets`.
59 */
60 category: 'widgets',
61
62 /**
63 * Sometimes a block could have aliases that help users discover it while searching.
64 * You can do so by providing an array of terms (which can be translated).
65 * It is only allowed to add as much as three terms per block.
66 */
67 keywords: [
68 __('shortcodes'), __('list'), __('page'),
69 ],
70
71 /**
72 * Optional block extended support features.
73 */
74 supports: {
75 // do not edit as HTML
76 html: false,
77
78 // use the block just once per post
79 multiple: false,
80
81 // don't allow the block to be converted into a reusable block
82 reusable: false,
83 },
84
85 /**
86 * Attributes provide the structured data needs of a block.
87 * They can exist in different forms when they are serialized,
88 * but they are declared together under a common interface.
89 */
90 attributes: {
91 shortcode: {
92 type: 'string',
93 source: 'html',
94 selector: 'div',
95 },
96 toggler: {
97 type: 'boolean',
98 default: false,
99 }
100 },
101
102 /**
103 * The API version of the block.
104 *
105 * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/
106 */
107 apiVersion: 3,
108
109 /**
110 * The edit function describes the structure of your block in the context of the editor.
111 * This represents what the editor will render when the block is used.
112 * @link https://wordpress.org/gutenberg/handbook/block-edit-save/#edit
113 *
114 * @param Object props Properties passed from the editor.
115 *
116 * @return Element Element to render.
117 */
118 edit: (props) => {
119
120 // iterate vikbooking shortcodes to build select options
121 let options = [];
122
123 let shortcodes_boxes = [];
124
125 if (currentShortcode === null) {
126 // if not set, define current value
127 currentShortcode = props.attributes.shortcode;
128 }
129
130 // insert empty option
131 options.push({
132 label: __('- pick a shortcode -', 'vikbooking'),
133 value: '',
134 });
135
136 // insert an option to support the creation of new shortcodes
137 options.push({
138 label: __('- create a new shortcode -', 'vikbooking'),
139 value: 'new',
140 });
141
142 // evaluate if toggler checkbox is checked
143 let togglerChecked = props.attributes.toggler;
144
145 for (let group in VIKBOOKING_SHORTCODES_BLOCK.shortcodes) {
146 let groups = VIKBOOKING_SHORTCODES_BLOCK.shortcodes[group];
147
148 for (let i = 0; i < groups.length; i++) {
149 let data = groups[i];
150
151 let post_id = parseInt(data.post_id);
152
153 // push option only in case:
154 // - toggler is enabled (see all)
155 // - the shortcode is not assigned to any post
156 // - the page of the shortcode is equal to this one
157 // - the shortcode is equal to the current one
158 if (togglerChecked || !post_id || post_id === wp.data.select('core/editor').getCurrentPostId() || data.shortcode == currentShortcode) {
159
160 options.push({
161 label: data.name,
162 value: data.shortcode,
163 });
164
165 // build ASSIGNEE field
166 let assigneeField = null;
167
168 if (post_id && data.shortcode != currentShortcode) {
169 assigneeField = el(
170 'a',
171 {
172 href: 'javascript:void(0);',
173 onClick: () => {
174 alert(__('This shortcode is already used by a different post. If you select this shortcode, it will be detached from the existing post and assigned to this new post.', 'vikbooking'));
175 },
176 },
177 el(
178 'span',
179 {
180 className: 'assigned',
181 },
182 __('Post #', 'vikbooking') + post_id
183 )
184 );
185 } else {
186 // safe shortcode
187 assigneeField = el('span', {}, (post_id ? __('Post #', 'vikbooking') + post_id : '--'));
188 }
189
190 // check if the box should be displayed
191 let toggled = props.attributes.shortcode == data.shortcode;
192
193 // setup information div
194 shortcodes_boxes.push(el(
195 'div',
196 {
197 className: 'vikbooking-shortcode-info-box' + (toggled ? ' toggled' : ''),
198 },
199 // create child elements
200 el(
201 'div',
202 {
203 className: 'vbo-sh-info-control'
204 },
205 [
206 el('label', {}, __('Type:', 'vikbooking')),
207 el('span', {}, group),
208 ]
209 ),
210 el(
211 'div',
212 {
213 className: 'vbo-sh-info-control'
214 },
215 [
216 el('label', {}, __('Name:', 'vikbooking')),
217 el('span', {}, data.name),
218 ]
219 ),
220 el(
221 'div',
222 {
223 className: 'vbo-sh-info-control'
224 },
225 [
226 el('label', {}, __('Created on:', 'vikbooking')),
227 el('span', {}, data.createdon),
228 ]
229 ),
230 el(
231 'div',
232 {
233 className: 'vbo-sh-info-control'
234 },
235 [
236 el('label', {}, __('Assignee:', 'vikbooking')),
237 assigneeField
238 ]
239 ),
240 el(
241 'div',
242 {
243 className: 'vbo-sh-info-control'
244 },
245 el(
246 wp.components.TextareaControl,
247 {
248 label: __('Shortcode:', 'vikbooking'),
249 value: data.shortcode,
250 readonly: true,
251 __nextHasNoMarginBottom: true,
252 __next40pxDefaultSize: true,
253 }
254 )
255 ),
256 ));
257 }
258 }
259 }
260
261 // build SVG for shortcode dashicon
262 const svg = el(
263 'svg',
264 {
265 className: 'dashicon dashicons-shortcode',
266 role: 'img',
267 focusable: false,
268 xmlns: 'http://www.w3.org/2000/svg',
269 width: '20',
270 height: '20',
271 viewBox: '0 0 20 20',
272 },
273 el(
274 'path',
275 {
276 d: 'M6 14H4V6h2V4H2v12h4M7.1 17h2.1l3.7-14h-2.1M14 4v2h2v8h-2v2h4V4',
277 }
278 )
279 );
280
281 // use the new version of InspectorControls
282 // provided by WordPress 5.4, if supported
283 let InspectorControls;
284
285 if (wp.blockEditor && wp.blockEditor.InspectorControls) {
286 // use new version
287 InspectorControls = wp.blockEditor.InspectorControls;
288 } else {
289 // fallback to the older one
290 InspectorControls = wp.editor.InspectorControls;
291 }
292
293 let navBar = [];
294
295 // add shortcode accordion
296 navBar.push(
297 el(
298 wp.components.PanelBody,
299 {
300 title: __('Shortcode'),
301 initialOpen: true,
302 },
303 // add shortcodes information
304 shortcodes_boxes,
305 el(
306 wp.components.ToggleControl,
307 {
308 label: __('See all', 'vikbooking'),
309 checked: togglerChecked,
310 help: togglerChecked ? __('Display all the existing shortcodes.', 'vikbooking') : __('Toggle to display also the shortcodes that are already assigned to a post.', 'vikbooking'),
311 onChange: (toggler) => {
312 props.setAttributes({ toggler: toggler });
313 },
314 __nextHasNoMarginBottom: true,
315 __next40pxDefaultSize: true,
316 }
317 )
318 )
319 );
320
321 // in case the shortcode dropdown has the "new" option selected,
322 // display a new panel to start the creation of a new shortcode
323 if (props.attributes.shortcode === 'new') {
324
325 if (!newShortcodeRegistry) {
326 // initialize only once the registry used to hold the data
327 // of the new shortcode that we want to create
328 newShortcodeRegistry = new VBOBlockPropertiesDecorator({lang: '*'}, props);
329 } else {
330 // always reconnect the registry to the updated attributes holder
331 // as the saved reference might differ in case the block gets deleted
332 newShortcodeRegistry.connect(props);
333 }
334
335 let newItemPanelFields = [];
336
337 // add field to select the site page
338 newItemPanelFields.push(
339 el(
340 wp.components.SelectControl,
341 {
342 label: __('Page Type', 'vikbooking'),
343 help: __('Choose the type of the page that should be displayed in the front-end.', 'vikbooking'),
344 value: newShortcodeRegistry.attributes.type,
345 options: [
346 {
347 label: __('Select an option', 'vikbooking'),
348 value: '',
349 }
350 ].concat(VIKBOOKING_SHORTCODES_BLOCK.views.map((page) => {
351 return {
352 label: page.name,
353 value: page.type,
354 };
355 })),
356 onChange: (value) => {
357 // preserve only the language whenever the type changes
358 newShortcodeRegistry.replaceAttributes({type: value, lang: newShortcodeRegistry.attributes.lang});
359 newShortcodeRegistry.setAttributes({});
360 },
361 __nextHasNoMarginBottom: true,
362 __next40pxDefaultSize: true,
363 }
364 )
365 );
366
367 // add field to select the language
368 newItemPanelFields.push(
369 el(
370 wp.components.SelectControl,
371 {
372 label: __('Language', 'vikbooking'),
373 help: __('Choose whether the shortcode should be available for a specific language only.', 'vikbooking'),
374 value: newShortcodeRegistry.attributes.lang,
375 options: [
376 {
377 label: __('All', 'vikbooking'),
378 value: '*',
379 }
380 ].concat(VIKBOOKING_SHORTCODES_BLOCK.languages.map((lang) => {
381 return {
382 label: lang.name,
383 value: lang.tag,
384 };
385 })),
386 onChange: (value) => {
387 newShortcodeRegistry.setAttributes({lang: value});
388 },
389 __nextHasNoMarginBottom: true,
390 __next40pxDefaultSize: true,
391 }
392 )
393 );
394
395 // get the details of the selected view
396 let selectedView = VIKBOOKING_SHORTCODES_BLOCK.views.filter(view => view.type === newShortcodeRegistry.attributes.type).shift();
397
398 if (selectedView) {
399 selectedView.fields.forEach((field) => {
400 // check if we have a list with predefined options
401 if (Array.isArray(field.options)) {
402 // make sure the default value is supported
403 if (field.options.filter(opt => opt.value == field.value).length === 0) {
404 // take the first option as default value, or an empty array in case of multiple list
405 field.value = field.multiple ? [] : field.options[0].value;
406 }
407 }
408
409 // set up a default value for the current field
410 newShortcodeRegistry.defineAttribute(field.name, field.value);
411
412 // create element according to the provided fields
413 let fieldControl = window.vboCreateControlElement(field, newShortcodeRegistry);
414
415 // register the element within the list of fields
416 newItemPanelFields.push(fieldControl);
417 });
418 }
419
420 // add the panel to create a new shortcode at runtime
421 navBar.push(
422 el(
423 wp.components.PanelBody,
424 {
425 title: __('New Item', 'vikbooking'),
426 initialOpen: true,
427 },
428 newItemPanelFields
429 )
430 );
431
432 // add button to save the shortcode
433 newItemPanelFields.push(
434 el(
435 wp.components.Button,
436 {
437 text: __('Create Shortcode', 'vikbooking'),
438 isPrimary: true,
439 onClick: (event) => {
440 // make sure we don't have a pending request
441 // if (event.target.disabled) {
442 if (event.target.getAttribute('aria-disabled') === 'true') {
443 return false;
444 }
445
446 // prevent duplicate requests
447 event.target.setAttribute('aria-disabled', true);
448 event.target.classList.add('is-busy');
449
450 // access the post data container
451 let postData = wp.data.select('core/editor');
452
453 // access the view parameters
454 let params = Object.assign({}, newShortcodeRegistry.attributes);
455 delete params.type;
456 delete params.lang;
457
458 // get the currently selected parent for this post
459 let parentId = postData.getEditedPostAttribute('parent');
460
461 // prepare request data
462 const request = {
463 name: postData.getEditedPostAttribute('title'),
464 type: newShortcodeRegistry.attributes.type,
465 lang: newShortcodeRegistry.attributes.lang,
466 parent_id: 0,
467 jform: params,
468 };
469
470 if (parentId) {
471 request.parent_id = (
472 // we need to find the shortcode assigned to a post ID equal to the parent selected for this page/post
473 Object.values(VIKBOOKING_SHORTCODES_BLOCK.shortcodes).flat().filter(shortcode => shortcode.post_id == parentId).shift()
474 // use a placeholder in case of no matching shortcodes
475 || {id: 0}
476 ).id;
477 }
478
479 // rely on a promise for a better ease of use
480 new Promise((resolve, reject) => {
481 // validate the title first
482 if (!request.name) {
483 reject(__('You need to specify a title for this post first.', 'vikbooking'));
484 return;
485 }
486
487 // make request to save the shortcode
488 jQuery.ajax({
489 url: VIKBOOKING_SHORTCODES_BLOCK.ajaxurl,
490 type: 'post',
491 data: request,
492 }).done((shortcode) => {
493 resolve(shortcode);
494 }).fail((error) => {
495 reject(error.responseText || error.statusText);
496 });
497 }).then((shortcode) => {
498 // create a new custom slot is not yet registered
499 if (VIKBOOKING_SHORTCODES_BLOCK.shortcodes.custom === undefined) {
500 VIKBOOKING_SHORTCODES_BLOCK.shortcodes.custom = [];
501 }
502
503 // append the newly created shortcode to the custom slot
504 VIKBOOKING_SHORTCODES_BLOCK.shortcodes.custom.push(shortcode);
505
506 // update the selected shortcode
507 props.setAttributes({shortcode: shortcode.shortcode});
508 }).catch((error) => {
509 // something went wrong, alert the user
510 alert(error || __('An error has occurred', 'vikbooking'));
511
512 // re-enable save button
513 event.target.setAttribute('aria-disabled', false);
514 event.target.classList.remove('is-busy');
515 });
516 },
517 }
518 )
519 );
520 }
521
522 // setup inspector (right-side area)
523 let controls = el(
524 // create InspectorControls element
525 InspectorControls,
526 // define inspector properties
527 {
528 key: 'controls',
529 },
530 navBar
531 );
532
533 let renderer;
534
535 if (props.attributes.shortcode === 'new') {
536 renderer = el(
537 'div',
538 {
539 style: {
540 border: '2px solid #ddd',
541 padding: '10px',
542 background: '#eee',
543 },
544 },
545 __('You can create a new shortcode from the block settings under the right sidebar.', 'vikbooking')
546 );
547 } else {
548 renderer = el(
549 wp.serverSideRender,
550 {
551 block: 'vikbooking/gutenberg-shortcodes',
552 attributes: props.attributes,
553 }
554 );
555 }
556
557 return [
558 controls,
559 el(
560 // create <div> wrapper
561 'div',
562 // define wrapper properties
563 {
564 className: 'vbo-shortcode-admin-wrapper',
565 },
566 // <div> contains select
567 [
568 el(
569 // create <select> for shortcode
570 wp.components.SelectControl,
571 // define select properties
572 {
573 label: [svg, __('Shortcode')],
574 value: props.attributes.shortcode,
575 onChange: (shortcode) => {
576 props.setAttributes({shortcode: shortcode});
577 },
578 options: options,
579 className: 'wp-block-shortcode',
580 __nextHasNoMarginBottom: true,
581 __next40pxDefaultSize: true,
582 }
583 ),
584 renderer,
585 ]
586 )
587 ];
588 },
589
590 /**
591 * The save function defines the way in which the different attributes should be combined
592 * into the final markup, which is then serialized by Gutenberg into `post_content`.
593 * @link https://wordpress.org/gutenberg/handbook/block-edit-save/#save
594 *
595 * @return Element Element to render.
596 */
597 save: (props) => {
598 let shortcode = props.attributes.shortcode;
599
600 return el(
601 'div',
602 {
603 className: props.className,
604 },
605 shortcode === 'new' ? '' : shortcode
606 );
607 }
608 });
609
610 /**
611 * Use a placeholder for the props parameter provided by the edit callback.
612 */
613 const VBOBlockPropertiesDecorator = class VBOBlockPropertiesDecorator {
614 constructor(props, adaptee) {
615 this.attributes = props || {};
616
617 this.connect(adaptee);
618 }
619
620 connect(adaptee) {
621 // keep a reference to the original inspector properties holder
622 this.adaptee = adaptee;
623 }
624
625 setAttributes(props) {
626 Object.assign(this.attributes, props);
627
628 // update a temporary field within the original attributes to force the inspector refresh
629 this.adaptee.setAttributes({refreshTrigger: new Date().toISOString()});
630 }
631
632 defineAttribute(key, value) {
633 if (!this.attributes.hasOwnProperty(key) && value !== undefined && value !== null) {
634 this.attributes[key] = value;
635 }
636 }
637
638 replaceAttributes(props) {
639 this.attributes = props;
640 }
641 }
642 })(window.wp);