HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux spn-python 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 x86_64
User: arjun (1000)
PHP: 8.1.2-1ubuntu2.20
Disabled: NONE
Upload Files
File: /var/www/html/bwcdev/wp-content/plugins/quiz-master-next/blocks/src/question/edit.js
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { escapeAttribute } from "@wordpress/escape-html";
import apiFetch from '@wordpress/api-fetch';
import {
	InspectorControls,
	RichText,
	InnerBlocks,
	useBlockProps,
	store as blockEditorStore,
	BlockControls,
} from '@wordpress/block-editor';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch, useSelect, select } from '@wordpress/data';
import { createBlock } from '@wordpress/blocks';
import {
	PanelBody,
	ToggleControl,
	SelectControl,
	ToolbarGroup, 
	ToolbarButton,
	TextControl,
	CheckboxControl,
	FormTokenField,
	Button,
	ExternalLink,
	Modal
} from '@wordpress/components';
import FeaturedImage from '../component/FeaturedImage';
import SelectAddCategory from '../component/SelectAddCategory';
import { warningIcon, plusIcon } from "../component/icon";
import { qsmIsEmpty, qsmStripTags, qsmFormData, qsmValueOrDefault, qsmDecodeHtml, qsmUniqueArray, qsmMatchingValueKeyArray } from '../helper';


//check for duplicate questionID attr
const isQuestionIDReserved = ( questionIDCheck, clientIdCheck ) => {
    const blocksClientIds = select( 'core/block-editor' ).getClientIdsWithDescendants();
    return qsmIsEmpty( blocksClientIds ) ? false : blocksClientIds.some( ( blockClientId ) => {
        const { questionID  } = select( 'core/block-editor' ).getBlockAttributes( blockClientId );
		//different Client Id but same questionID attribute means duplicate
        return clientIdCheck !== blockClientId && questionID === questionIDCheck;
    } );
};

/**
 * https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md#allowedformats-array
 *  
 */
export default function Edit( props ) {
	//check for QSM initialize data
	if ( 'undefined' === typeof qsmBlockData ) {
		return null;
	}
	
	const { className, attributes, setAttributes, isSelected, clientId, context } = props;

	/** https://github.com/WordPress/gutenberg/issues/22282  */
	const isParentOfSelectedBlock = useSelect( ( select ) => isSelected || select( 'core/block-editor' ).hasSelectedInnerBlock( clientId, true ) );

	const quizID = context['quiz-master-next/quizID'];
	const {
		quiz_name,
		post_id,
		rest_nonce
	} = context['quiz-master-next/quizAttr'];
	const pageID = context['quiz-master-next/pageID'];
	const { createNotice } = useDispatch( noticesStore );

	//Get finstion to find index of blocks
	const { 
		getBlockRootClientId, 
		getBlockIndex 
	} = useSelect( blockEditorStore );

	//Get funstion to insert block
	const {
		insertBlock
	} = useDispatch( blockEditorStore );

	const {
		isChanged = false,//use in editor only to detect if any change occur in this block
		questionID,
		type,
		description,
		title,
		correctAnswerInfo,
		commentBox,
		category,
		multicategories=[],
		hint,
		featureImageID,
		featureImageSrc,
		answers,
		answerEditor,
		matchAnswer,
		required,
		settings={},
	} = attributes;
	
	//Variable to decide if correct answer info input field should be available 
	const [ enableCorrectAnsInfo, setEnableCorrectAnsInfo ] = useState( ! qsmIsEmpty( correctAnswerInfo ) );
	//Advance Question modal
	const [ isOpenAdvanceQModal, setIsOpenAdvanceQModal ] = useState( false );

	const proActivated = ( '1' == qsmBlockData.is_pro_activated );
	const isAdvanceQuestionType = ( qtype ) => 14 < parseInt( qtype );

	//Available file types
	const fileTypes = qsmBlockData.file_upload_type.options;

	//Get selected file types
	const selectedFileTypes = () => {
		let file_types = settings?.file_upload_type || qsmBlockData.file_upload_type.default;
		return qsmIsEmpty( file_types ) ? [] : file_types.split(',');
	}

	//Is file type checked
	const isCheckedFileType = ( fileType ) => selectedFileTypes().includes( fileType );

	//Set file type
	const setFileTypes = ( fileType ) => {
		let file_types = selectedFileTypes();
		if ( file_types.includes( fileType ) ) {
			file_types = file_types.filter( file_type => file_type != fileType );
		} else {
			file_types.push( fileType );
		}
		file_types = file_types.join(',');
		setAttributes({
			settings:{
				...settings,
				file_upload_type: file_types
			}
		})
	}
	
	/**Generate question id if not set or in case duplicate questionID ***/
	useEffect( () => {
		let shouldSetID = true;
		if ( shouldSetID ) {
		
			if ( qsmIsEmpty( questionID ) || '0' == questionID || ( ! qsmIsEmpty( questionID ) && isQuestionIDReserved( questionID, clientId ) ) ) {
				
				//create a question
				let newQuestion = qsmFormData( {
					"id": null,
					"rest_nonce": rest_nonce,
					"quizID": quizID,
					"quiz_name": quiz_name,
					"postID": post_id,
					"answerEditor": qsmValueOrDefault( answerEditor, 'text' ),
					"type": qsmValueOrDefault( type , '0' ),
					"name": qsmDecodeHtml( qsmValueOrDefault( description ) ),
					"question_title": qsmValueOrDefault( title ),
					"answerInfo": qsmDecodeHtml( qsmValueOrDefault( correctAnswerInfo ) ),
					"comments": qsmValueOrDefault( commentBox, '1' ),
					"hint": qsmValueOrDefault( hint ),
					"category": qsmValueOrDefault( category ),
					"multicategories": [],
					"required": qsmValueOrDefault( required, 0 ),
					"answers": answers,
					"page": 0,
					"featureImageID": featureImageID,
					"featureImageSrc": featureImageSrc,
					"matchAnswer": null,
				} );

				//AJAX call
				apiFetch( {
					path: '/quiz-survey-master/v1/questions',
					method: 'POST',
					body: newQuestion
				} ).then( ( response ) => {
					
					if ( 'success' == response.status ) {
						let question_id = response.id;
						setAttributes( { questionID: question_id } );
					}
				}).catch(
					( error ) => {
						console.log( 'error',error );
						createNotice( 'error', error.message, {
							isDismissible: true,
							type: 'snackbar',
						} );
					}
				);
			}
		}
		
		//cleanup
		return () => {
			shouldSetID = false;
		};
		
	}, [] );

	//detect change in question
	useEffect( () => {
		let shouldSetChanged = true;
		if ( shouldSetChanged && isSelected  && false === isChanged ) {
			
			setAttributes( { isChanged: true } );
		}

		//cleanup
		return () => {
			shouldSetChanged = false;
		};
	}, [
		questionID,
		type,
		description,
		title,
		correctAnswerInfo,
		commentBox,
		category,
		multicategories,
		hint,
		featureImageID,
		featureImageSrc,
		answers,
		answerEditor,
		matchAnswer,
		required,
		settings,
	] )

	//add classes
	const blockProps = useBlockProps( {
		className: isParentOfSelectedBlock ? ' in-editing-mode is-highlighted ':'' ,
	} );

	const QUESTION_TEMPLATE = [
		[
			'qsm/quiz-answer-option',
			{
				optionID:'0'
			}
		],
		[
			'qsm/quiz-answer-option',
			{
				optionID:'1'
			}
		]

	];

	//Get category ancestor
	const getCategoryAncestors = ( termId, categories ) => {
		let parents = [];
		if ( ! qsmIsEmpty( categories[ termId ] ) && '0' != categories[ termId ]['parent'] ) {
			termId = categories[ termId ]['parent'];
			parents.push( termId );
			if ( ! qsmIsEmpty( categories[ termId ] ) && '0' != categories[ termId ]['parent'] ) {
				let ancestor = getCategoryAncestors( termId, categories );
				parents = [ ...parents, ...ancestor ];
			}
		} 
		
		return qsmUniqueArray( parents );
	 }

	//check if a category is selected
	const isCategorySelected = ( termId ) =>  multicategories.includes( termId );

	//set or unset category
	const setUnsetCatgory = ( termId, categories ) => {
		let multiCat = ( qsmIsEmpty( multicategories ) || 0 === multicategories.length ) ? ( qsmIsEmpty( category ) ? [] : [ category ] ) : multicategories;
		
		//Case: category unselected
		if ( multiCat.includes( termId ) ) {
			//remove category if already set
			multiCat = multiCat.filter( catID =>  catID != termId );
			let children = [];
			//check for if any child is selcted 
			multiCat.forEach( childCatID => {
				//get ancestors of category
				let ancestorIds = getCategoryAncestors( childCatID, categories );
				//given unselected category is an ancestor of selected category
				if ( ancestorIds.includes( termId ) ) {
					//remove category if already set
					multiCat = multiCat.filter( catID =>  catID != childCatID );
				}
			});
		} else {
			//add category if not set
			multiCat.push( termId );
			//get ancestors of category
			let ancestorIds = getCategoryAncestors( termId, categories );
			//select all ancestor
			multiCat = [ ...multiCat, ...ancestorIds ];
		}

		multiCat = qsmUniqueArray( multiCat );

		setAttributes({ 
			category: '',
			multicategories: [ ...multiCat ]
		});
	}

	//Notes relation to question type
	const notes = ['12','7','3','5','14'].includes( type ) ? __( 'Note: Add only correct answer options with their respective points score.', 'quiz-master-next' ) : '';

	//set Question type
	const setQuestionType = ( qtype ) => {
		if ( ! qsmIsEmpty( MicroModal ) && ! proActivated && ['15', '16', '17'].includes( qtype ) ) {
			//Show modal for advance question type
			let modalEl = document.getElementById('modal-advanced-question-type');
			if ( ! qsmIsEmpty( modalEl ) ) {
				MicroModal.show('modal-advanced-question-type');
			}
		} else if ( proActivated && isAdvanceQuestionType( qtype ) ) {
			setIsOpenAdvanceQModal( true );
		} else {
			setAttributes( { type: qtype } );
		}
	}

	//insert new Question
	const insertNewQuestion = () => {
		if ( qsmIsEmpty( props?.name )) {
			console.log("block name not found");
			return true;
		}
		const blockToInsert = createBlock( props.name );
	
		const selectBlockOnInsert = true;
		insertBlock(
			blockToInsert,
			getBlockIndex( clientId ) + 1,
			getBlockRootClientId( clientId ),
			selectBlockOnInsert
		);
	}

	//insert new Question
	const insertNewPage = () => {
		const blockToInsert = createBlock( 'qsm/quiz-page' );
		const currentPageClientID = getBlockRootClientId( clientId );
		const newPageIndex = getBlockIndex( currentPageClientID ) + 1;
		const qsmBlockClientID = getBlockRootClientId( currentPageClientID );
		const selectBlockOnInsert = true;
		insertBlock(
			blockToInsert,
			newPageIndex,
			qsmBlockClientID,
			selectBlockOnInsert
		);
	}

	return (
	<>
		<BlockControls>
			<ToolbarGroup>
				<ToolbarButton
					icon='plus-alt2'
					label={ __( 'Add New Question', 'quiz-master-next' ) }
					onClick={ () => insertNewQuestion() }
				/>
				<ToolbarButton
					icon='welcome-add-page'
					label={ __( 'Add New Page', 'quiz-master-next' ) }
					onClick={ () => insertNewPage() }
				/>
			</ToolbarGroup>
		</BlockControls>
	{ isOpenAdvanceQModal && (
		<Modal 
		contentLabel={ __( 'Use QSM Editor for Advanced Question', 'quiz-master-next' ) }
		className='qsm-advance-q-modal'
		isDismissible={ false }
		size='small'
		__experimentalHideHeader={ true }
		>
			<div className='qsm-modal-body' >
				<h3 className='qsm-title'>
					{ warningIcon() }
					<br />
					{ __( 'Use QSM editor for Advanced Question', 'quiz-master-next' ) }
				</h3>
				<p className='qsm-description'>
					{ __( "Currently, the block editor doesn't support advanced question type. We are working on it. Alternatively, you can add advanced questions from your QSM's quiz editor.", "quiz-master-next" ) }
				</p>
				<div className='qsm-modal-btn-wrapper'>
					<Button variant="secondary" onClick={ () => setIsOpenAdvanceQModal( false ) }>
						{ __( 'Cancel', 'quiz-master-next' ) }
					</Button>
					<Button variant="primary" onClick={ () => {} }>
						<ExternalLink 
							href={ qsmBlockData.quiz_settings_url+'&quiz_id='+quizID }
						>
							{ __( 'Add Question from quiz editor', 'quiz-master-next' ) }
						</ExternalLink>
					</Button>
				</div>
			</div>
			
		</Modal>
	) }
	 { isAdvanceQuestionType( type ) ? (
		<>
			<InspectorControls>
				<PanelBody title={ __( 'Question settings', 'quiz-master-next' ) } initialOpen={ true }>
					<h2 className="block-editor-block-card__title">{ __( 'ID', 'quiz-master-next' )+': '+questionID }</h2>
					<h3>{ __( 'Advanced Question Type', 'quiz-master-next' ) }</h3>
				</PanelBody>
			</InspectorControls>	
			<div  { ...blockProps } >
			<h4 className={ 'qsm-question-title qsm-error-text' } >{ __( 'Advanced Question Type : ', 'quiz-master-next' ) + title }</h4>
			<p> 
			{ __( 'Edit question in QSM ', 'quiz-master-next' ) }
			<ExternalLink 
				href={ qsmBlockData.quiz_settings_url+'&quiz_id='+quizID }
			>
					{ __( 'editor', 'quiz-master-next' ) }
			</ExternalLink>
			</p>
			</div>
		</>
		):(
		<>
		<InspectorControls>
			<PanelBody title={ __( 'Question settings', 'quiz-master-next' ) } initialOpen={ true }>
			<h2 className="block-editor-block-card__title">{ __( 'ID', 'quiz-master-next' )+': '+questionID }</h2>
			{ /** Question Type **/ }
			<SelectControl
				label={ qsmBlockData.question_type.label }
				value={ type || qsmBlockData.question_type.default }
				onChange={ ( type ) =>
					setQuestionType( type )
				}
				help={ qsmIsEmpty( qsmBlockData.question_type_description[ type ] ) ? '' : qsmBlockData.question_type_description[ type ]+' '+notes }
				__nextHasNoMarginBottom
			>
				{
				! qsmIsEmpty( qsmBlockData.question_type.options ) && qsmBlockData.question_type.options.map( qtypes => 
					(
					<optgroup label={ qtypes.category } key={ "qtypes"+qtypes.category }  >
						{
							qtypes.types.map( qtype => 
								(
								<option value={ qtype.slug } key={ "qtype"+qtype.slug }  >{ qtype.name }</option>
								)
							)
						}
					</optgroup>
					)
					)
				}
			</SelectControl>
			{/**Answer Type */}
			{
				['0','4','1','10','13'].includes( type ) && 
				<SelectControl
					label={ qsmBlockData.answerEditor.label }
					value={ answerEditor || qsmBlockData.answerEditor.default }
					options={ qsmBlockData.answerEditor.options }
					onChange={ ( answerEditor ) =>
						setAttributes( { answerEditor } )
					}
					__nextHasNoMarginBottom
				/>
			}
			<ToggleControl
				label={ __( 'Required', 'quiz-master-next' ) }
				checked={ ! qsmIsEmpty( required ) && '1' == required  }
				onChange={ () => setAttributes( { required : ( ( ! qsmIsEmpty( required ) && '1' == required ) ? 0 : 1 ) } ) }
			/>
			<ToggleControl
				label={ __( 'Show Correct Answer Info', 'quiz-master-next' ) }
				checked={ enableCorrectAnsInfo  }
				onChange={ () => setEnableCorrectAnsInfo( ! enableCorrectAnsInfo ) }
			/>
			</PanelBody>
			{/**File Upload */}
			{ '11' == type && (
				<PanelBody title={ __( 'File Settings', 'quiz-master-next' ) } initialOpen={ false }>
				{/**Upload Limit */}
				<TextControl
					type='number'
					label={ qsmBlockData.file_upload_limit.heading  }
					value={ settings?.file_upload_limit ?? qsmBlockData.file_upload_limit.default }
					onChange={ ( limit ) => setAttributes( { settings:{
						...settings,
						file_upload_limit: limit
					} } ) }
				/>
				{/**Allowed File Type */}
				<label className="qsm-inspector-label">
					{ qsmBlockData.file_upload_type.heading }
				</label>
				{
					Object.keys( qsmBlockData.file_upload_type.options ).map( filetype => (
						<CheckboxControl
							key={ 'filetype-'+filetype }
							label={  fileTypes[filetype] }
							checked={ isCheckedFileType( filetype ) }
							onChange={ () => setFileTypes( filetype ) }
						/>
					) )
				}
				</PanelBody>
			)}
			{/**Categories */}
			<SelectAddCategory 
				isCategorySelected={ isCategorySelected }
				setUnsetCatgory={ setUnsetCatgory }
			/>
			{/**Hint */}
			<PanelBody title={ __( 'Hint', 'quiz-master-next' ) } initialOpen={ false }  >
			<TextControl
				label=''
				value={ hint }
				onChange={ ( hint ) => setAttributes( { hint: escapeAttribute( hint ) } ) }
			/>
			</PanelBody>
			{/**Comment Box */}
			<PanelBody title={ qsmBlockData.commentBox.heading } initialOpen={ false } >
			<SelectControl
				label={ qsmBlockData.commentBox.label }
				value={ commentBox || qsmBlockData.commentBox.default }
				options={ qsmBlockData.commentBox.options }
				onChange={ ( commentBox ) =>
					setAttributes( { commentBox } )
				}
				__nextHasNoMarginBottom
			/>
			</PanelBody>
			{/**Feature Image */}
			<PanelBody title={ __( 'Featured image', 'quiz-master-next' ) } initialOpen={ true }>
				<FeaturedImage 
				featureImageID={ featureImageID }
				onUpdateImage={ ( mediaDetails ) => {
					setAttributes({ 
						featureImageID: mediaDetails.id,
						featureImageSrc: mediaDetails.url
					});
				}  }
				onRemoveImage={ ( id ) => {
					setAttributes({ 
						featureImageID: undefined,
						featureImageSrc: undefined,
					});
				}  }
				/>
			</PanelBody>
		</InspectorControls>
		<div  { ...blockProps } >
			<RichText
				tagName='h4'
				title={ __( 'Question title', 'quiz-master-next' ) }
				aria-label={ __( 'Question title', 'quiz-master-next' ) }
				placeholder={  __( 'Type your question here', 'quiz-master-next' ) }
				value={ escapeAttribute( title ) }
				onChange={ ( title ) => setAttributes( { title: escapeAttribute( title ) } ) }
				allowedFormats={ [ ] }
				withoutInteractiveFormatting
				className={ 'qsm-question-title' }
			/>
			{
				isParentOfSelectedBlock && 
				<>
				<RichText
					tagName='p'
					title={ __( 'Question description', 'quiz-master-next' ) }
					aria-label={ __( 'Question description', 'quiz-master-next' ) }
					placeholder={  __( 'Description goes here... (optional)', 'quiz-master-next' ) }
					value={ qsmDecodeHtml( description ) }
					onChange={ ( description ) => setAttributes({ description }) }
					className={ 'qsm-question-description' }
					__unstableEmbedURLOnPaste
					__unstableAllowPrefixTransformations
				/>
				{
					! ['8','11','6','9'].includes( type ) &&
					<InnerBlocks
						allowedBlocks={ ['qsm/quiz-answer-option'] }
						template={ QUESTION_TEMPLATE }
					/>
				}
				{
					enableCorrectAnsInfo && (
						<RichText
							tagName='p'
							title={ __( 'Correct Answer Info', 'quiz-master-next' ) }
							aria-label={ __( 'Correct Answer Info', 'quiz-master-next' ) }
							placeholder={  __( 'Correct answer info goes here', 'quiz-master-next' ) }
							value={ qsmDecodeHtml( correctAnswerInfo ) }
							onChange={ ( correctAnswerInfo ) => setAttributes({ correctAnswerInfo }) }
							className={ 'qsm-question-correct-answer-info' }
							__unstableEmbedURLOnPaste
							__unstableAllowPrefixTransformations
						/>
					)
				}
				{
					isParentOfSelectedBlock && ( 
					<div className='block-editor-block-list__insertion-point-inserter qsm-add-new-ques-wrapper'>
					<Button 
					icon={ plusIcon }
					label={ __( 'Add New Question', 'quiz-master-next' ) }
					tooltipPosition="bottom"
					onClick={ () => insertNewQuestion() }
					variant="secondary"
					className='add-new-question-btn block-editor-inserter__toggle'
					>
					</Button>
					</div>
					)
				}
				
				</>
			}
		</div>
		</>
		)
	}
	</>
	);
}