PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / media / js / gutenberg-shortcodes.js
vikappointments / media / js Last commit date
admin.js 4 days ago bootstrap.min.js 4 days ago gutenberg-shortcodes.js 4 days ago gutenberg-tools.js 4 days ago gutenberg-widgets.js 4 days ago system.js 4 days ago tinymce-shortcodes.js 4 days ago
gutenberg-shortcodes.js
642 lines
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('vikappointments/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: __('VikAppointments Shortcode', 'vikappointments'),
43
44 /**
45 * This is the block description, which is displayed within the right sidebar.
46 */
47 description: __('Add a shortcode configured through VikAppointments.', 'vikappointments'),
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: 'calendar-alt',
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 vikappointments 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 -', 'vikappointments'),
133 value: '',
134 });
135
136 // insert an option to support the creation of new shortcodes
137 options.push({
138 label: __('- create a new shortcode -', 'vikappointments'),
139 value: 'new',
140 });
141
142 // evaluate if toggler checkbox is checked
143 let togglerChecked = props.attributes.toggler;
144
145 for (let group in VIKAPPOINTMENTS_SHORTCODES_BLOCK.shortcodes) {
146 let groups = VIKAPPOINTMENTS_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.', 'vikappointments'));
175 },
176 },
177 el(
178 'span',
179 {
180 className: 'assigned',
181 },
182 __('Post #', 'vikappointments') + post_id
183 )
184 );
185 } else {
186 // safe shortcode
187 assigneeField = el('span', {}, (post_id ? __('Post #', 'vikappointments') + 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: 'vikappointments-shortcode-info-box' + (toggled ? ' toggled' : ''),
198 },
199 // create child elements
200 el(
201 'div',
202 {
203 className: 'vap-sh-info-control'
204 },
205 [
206 el('label', {}, __('Type:', 'vikappointments')),
207 el('span', {}, group),
208 ]
209 ),
210 el(
211 'div',
212 {
213 className: 'vap-sh-info-control'
214 },
215 [
216 el('label', {}, __('Name:', 'vikappointments')),
217 el('span', {}, data.name),
218 ]
219 ),
220 el(
221 'div',
222 {
223 className: 'vap-sh-info-control'
224 },
225 [
226 el('label', {}, __('Created on:', 'vikappointments')),
227 el('span', {}, data.createdon),
228 ]
229 ),
230 el(
231 'div',
232 {
233 className: 'vap-sh-info-control'
234 },
235 [
236 el('label', {}, __('Assignee:', 'vikappointments')),
237 assigneeField
238 ]
239 ),
240 el(
241 'div',
242 {
243 className: 'vap-sh-info-control'
244 },
245 el(
246 wp.components.TextareaControl,
247 {
248 label: __('Shortcode:', 'vikappointments'),
249 value: data.shortcode,
250 readonly: true,
251 __next40pxDefaultSize: true,
252 __nextHasNoMarginBottom: 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', 'vikappointments'),
309 checked: togglerChecked,
310 help: togglerChecked ? __('Display all the existing shortcodes.', 'vikappointments') : __('Toggle to display also the shortcodes that are already assigned to a post.', 'vikappointments'),
311 onChange: (toggler) => {
312 props.setAttributes({ toggler: toggler });
313 },
314 __next40pxDefaultSize: true,
315 __nextHasNoMarginBottom: 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 VAPBlockPropertiesDecorator({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', 'vikappointments'),
343 help: __('Choose the type of the page that should be displayed in the front-end.', 'vikappointments'),
344 value: newShortcodeRegistry.attributes.type,
345 options: [
346 {
347 label: __('Select an option', 'vikappointments'),
348 value: '',
349 }
350 ].concat(VIKAPPOINTMENTS_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 __next40pxDefaultSize: true,
362 __nextHasNoMarginBottom: 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', 'vikappointments'),
373 help: __('Choose whether the shortcode should be available for a specific language only.', 'vikappointments'),
374 value: newShortcodeRegistry.attributes.lang,
375 options: [
376 {
377 label: __('All', 'vikappointments'),
378 value: '*',
379 }
380 ].concat(VIKAPPOINTMENTS_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 __next40pxDefaultSize: true,
390 __nextHasNoMarginBottom: true,
391 }
392 )
393 );
394
395 // get the details of the selected view
396 let selectedView = VIKAPPOINTMENTS_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.vapCreateControlElement(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', 'vikappointments'),
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', 'vikappointments'),
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(VIKAPPOINTMENTS_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.', 'vikappointments'));
484 return;
485 }
486
487 // make request to save the shortcode
488 jQuery.ajax({
489 url: VIKAPPOINTMENTS_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 (VIKAPPOINTMENTS_SHORTCODES_BLOCK.shortcodes.custom === undefined) {
500 VIKAPPOINTMENTS_SHORTCODES_BLOCK.shortcodes.custom = [];
501 }
502
503 // append the newly created shortcode to the custom slot
504 VIKAPPOINTMENTS_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', 'vikappointments'));
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.', 'vikappointments')
546 );
547 } else {
548 renderer = el(
549 wp.serverSideRender,
550 {
551 block: 'vikappointments/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: 'vap-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 __next40pxDefaultSize: true,
581 __nextHasNoMarginBottom: 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 VAPBlockPropertiesDecorator = class VAPBlockPropertiesDecorator {
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);