diff --git a/blocks/definition/README.md b/blocks/definition/README.md new file mode 100644 index 00000000..b1a956cf --- /dev/null +++ b/blocks/definition/README.md @@ -0,0 +1,20 @@ +# Definition block + +A block to display a term and definition or abbreviation with semantic markup ( `
`, ``, `` etc). + +Features: + +- Online definition lookup +- Semantic markup of definitions and abbreviations + +### TODO + +- [ ] Multiple dictionary sources, and the ability to add one's own API key. +- [ ] Support a list of definitions, or an accordion style for glossary pages that have lots of definitions in them +- [ ] Add definition image/thumbnail option +- [ ] Add audio tag for pronunciation example +- [ ] A permalink / easy to copy anchor link to the definition from the front-end + +![alt text][screenshot] + +[screenshot]: screenshot.png "Definition block screenshot" diff --git a/blocks/definition/editor.scss b/blocks/definition/editor.scss new file mode 100644 index 00000000..43758d0e --- /dev/null +++ b/blocks/definition/editor.scss @@ -0,0 +1,96 @@ +/** + * Editor styles + */ + +.wp-block-a8c-definition { + padding: 10px 10px; + .wp-block-a8c-definition__term { + font-size: 2.5rem; + border-bottom: 2px solid #eeeeee; + display: block; + margin-bottom: 10px; + } + .wp-block-a8c-definition__definition { + padding-left: 0; + margin-left: 0; + } + .wp-block-a8c-definition__term-metadata { + padding-left: 10px; + font-size: 14px; + opacity: 0.8; + font-style: italic; + font-weight: normal; + } + .wp-block-a8c-definition__term-text { + font-style: normal; + } + .wp-block-a8c-definition__term-definition { + padding-left: 10px; + } + .wp-block-a8c-definition__term-metadata-item { + display: inline-block; + margin-right: 8px; + } + .wp-block-a8c-definition__term-metadata-item:after { + content: ']'; + } + .wp-block-a8c-definition__term-metadata-item:before { + content: '['; + } + // Minimal style + &.is-style-minimal, + &.is-minimal { + padding: 0; + .wp-block-a8c-definition__term { + font-size: 2rem; + border-bottom: 0; + display: block; + margin-bottom: 10px; + } + .wp-block-a8c-definition__term-metadata { + padding-left: 0; + font-size: .9rem; + font-weight: normal; + display: block; + } + .wp-block-a8c-definition__term-definition { + padding-left: 0; + } + .wp-block-a8c-definition__term-metadata-item:after, + .wp-block-a8c-definition__term-metadata-item:before { + content: ''; + } + } +} + +.wp-block-a8c-definition__panel-row--definition-settings { + display: flex; + flex-direction: column; + align-items: start; + margin-bottom: 24px; + + .components-base-control { + margin-bottom: 4px; + } + + .wp-block-a8c-definition__definition-settings-help-text { + font-size: 12px; + font-style: normal; + color: rgb( 117, 117, 117 ); + } +} + +.wp-block-a8c-definition__search-control-container { + display: flex; + flex-direction: column; + align-items: start; + + .wp-block-a8c-definition__search-button { + text-decoration: none; + } + + .components-button { + white-space: normal; + } +} + diff --git a/blocks/definition/index.php b/blocks/definition/index.php new file mode 100644 index 00000000..faf5bca0 --- /dev/null +++ b/blocks/definition/index.php @@ -0,0 +1,20 @@ + 'block-experiments', + 'style' => 'block-experiments', + 'editor_style' => 'block-experiments-editor', + ] + ); + + wp_set_script_translations( 'block-experiments', 'definition' ); +} ); diff --git a/blocks/definition/screenshot.png b/blocks/definition/screenshot.png new file mode 100644 index 00000000..b5a12e18 Binary files /dev/null and b/blocks/definition/screenshot.png differ diff --git a/blocks/definition/src/components/search-results.js b/blocks/definition/src/components/search-results.js new file mode 100644 index 00000000..d86d3907 --- /dev/null +++ b/blocks/definition/src/components/search-results.js @@ -0,0 +1,53 @@ +/** + * External dependencies. + */ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; + +/** + * Search results for dictionary search. + * + * @param {Object} Props. + * @param {string} Props.selectedId + * @param {Array} Props.searchResults + * @param {Function} Props.onSelectDefinition + * @param {string} Props.title + * @return {WPElement} Element to render. + */ +export default function SearchResults( { + selectedId = '', + searchResults = [], + onSelectDefinition, + title, +} ) { + return ( + <> +

+ { title } +

+
    + { searchResults.length && + searchResults.map( ( option ) => ( +
  1. + +
  2. + ) ) } +
+ + ); +} diff --git a/blocks/definition/src/components/term-metadata.js b/blocks/definition/src/components/term-metadata.js new file mode 100644 index 00000000..38214e94 --- /dev/null +++ b/blocks/definition/src/components/term-metadata.js @@ -0,0 +1,56 @@ +/** + * External dependencies. + */ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { findInCollection } from '../utils'; +import { PARTS_OF_SPEECH } from '../constants'; + +/** + * Term metadata to render in both the editor and the save methods. + * + * @param {Object} Props. + * @param {string} Props.partOfSpeech + * @param {string} Props.phoneticTranscription + * @return {WPElement} Element to render. + */ +export function TermMetaData( { partOfSpeech, phoneticTranscription } ) { + const shouldShowTermMetaData = !! partOfSpeech || !! phoneticTranscription; + + if ( ! shouldShowTermMetaData ) { + return null; + } + // Find a translated part of speech value from our list or use the raw value. + const partOfSpeechTitle = + findInCollection( PARTS_OF_SPEECH, 'value', partOfSpeech )?.title || + partOfSpeech; + + return ( + + { partOfSpeech && ( + + { partOfSpeechTitle } + + ) } + { phoneticTranscription && ( + + { phoneticTranscription } + + ) } + + ); +} + +export default TermMetaData; diff --git a/blocks/definition/src/constants.js b/blocks/definition/src/constants.js new file mode 100644 index 00000000..72d3a222 --- /dev/null +++ b/blocks/definition/src/constants.js @@ -0,0 +1,55 @@ +/** + * External dependencies. + */ +import { __ } from '@wordpress/i18n'; + +export const ABBREVIATION = 'abbreviation'; + +export const DEFAULT_LOCALE = 'en'; + +export const PARTS_OF_SPEECH = [ + { + label: __( 'Choose a word class (optional)', 'definition' ), + value: '', + }, + { + label: __( 'Noun, e.g., Apple', 'definition' ), + title: __( 'Noun', 'definition' ), + value: 'noun', + }, + { + label: __( 'Verb, e.g., Eat', 'definition' ), + title: __( 'Verb', 'definition' ), + value: 'verb', + }, + { + label: __( 'Article, e.g., The', 'definition' ), + title: __( 'Article', 'definition' ), + value: 'article', + }, + { + label: __( 'Pronoun, e.g., Their', 'definition' ), + title: __( 'Pronoun', 'definition' ), + value: 'pronoun', + }, + { + label: __( 'Preposition, e.g., With', 'definition' ), + title: __( 'Preposition', 'definition' ), + value: 'preposition', + }, + { + label: __( 'Adverb, e.g., Quickly', 'definition' ), + title: __( 'Adverb', 'definition' ), + value: 'adverb', + }, + { + label: __( 'Conjunction, e.g., And', 'definition' ), + title: __( 'Conjunction', 'definition' ), + value: 'conjunction', + }, + { + label: __( 'Abbreviation, e.g., PLC', 'definition' ), + title: __( 'Abbreviation', 'definition' ), + value: ABBREVIATION, + }, +]; diff --git a/blocks/definition/src/controls.js b/blocks/definition/src/controls.js new file mode 100644 index 00000000..bb0f8719 --- /dev/null +++ b/blocks/definition/src/controls.js @@ -0,0 +1,308 @@ +/** + * External dependencies + */ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + Button, + Panel, + PanelBody, + SelectControl, + ToggleControl, + PanelRow, + ExternalLink, +} from '@wordpress/components'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import SearchResults from './components/search-results'; +import useFetchDefinition from './use-fetch-definition'; +import { isEmpty } from 'lodash'; +import { DictionaryApi } from './lib/dictionary-apis'; +import { getLocaleFromLocaleData } from './utils'; + +/** + * Returns help text for the abbreviation toggle control + * + * @param {boolean} checked Whether the control is checked or not. + * @return {string} The help message. + */ +function getToggleAbbreviationHelp( checked ) { + return checked + ? __( + 'Your definition is an abbreviation, e.g., LOL, and is wrapped in an tag', + 'definition' + ) + : __( + 'Your definition is a whole word or term and not an abbreviation.', + 'definition' + ); +} + +/** + * Returns help text for the should show meta toggle control. + * + * @param {boolean} checked Whether the control is checked or not. + * @return {string} The help message. + */ +function getToggleShouldShowTermMetaHelp( checked ) { + return checked + ? __( 'Show parts of speech and other term information.', 'definition' ) + : __( 'Hide parts of speech and other term information', 'definition' ); +} + +/** + * Stores and returns a dictionary API object. + * + * @param {string} key A key identifier for a Dictionary API. + * @return {Function} A dictionary class with static member methods. + */ +function getDictionaryApiByKey( key = 'dictionaryApi' ) { + return { + dictionaryApi: DictionaryApi, + }[ key ]; +} + +/** + * Returns editor controls. + * + * @param root0 + * @param root0.term + * @param root0.isAbbreviation + * @param root0.onToggleAbbreviation + * @param root0.partOfSpeech + * @param root0.onChangePartOfSpeech + * @param root0.partsOfSpeechOptions + * @param root0.onSelectDefinition + * @param root0.shouldShowTermMeta + * @param root0.onToggleShouldShowTermMeta + * @return {WPElement} Element to render. + */ +export default function DefinitionControls( { + term, + isAbbreviation, + onToggleAbbreviation, + partOfSpeech, + onChangePartOfSpeech, + partsOfSpeechOptions, + onSelectDefinition, + shouldShowTermMeta, + onToggleShouldShowTermMeta, +} ) { + const [ shouldShowLanguagePicker, setShouldShowLanguagePicker ] = useState( + false + ); + const [ searchLocale, setSearchLocale ] = useState( + getLocaleFromLocaleData() + ); + const [ shouldShowSearchResults, setShouldShowSearchResults ] = useState( + false + ); + const [ definitionOptions, setDefinitionOptions ] = useState( [] ); + // We cache the last search term so we compare with incoming `term` prop. + const [ searchTerm, setSearchTerm ] = useState( '' ); + const [ selectedSearchTermId, setSelectedSearchTermId ] = useState( null ); + + const dictionaryApi = getDictionaryApiByKey(); + const { + isFetching, + definitionData, + fetchDefinition, + errorMessage, + } = useFetchDefinition( { + locale: searchLocale, + api: dictionaryApi, + } ); + const searchForDefinition = () => { + if ( ! term || isFetching ) { + return; + } + + const strippedTerm = stripHTML( term ); + + // Don't perform fetch if the current term already matches the fetched term. + if ( strippedTerm === definitionData?.definition ) { + setShouldShowSearchResults( true ); + return; + } + + setSearchTerm( strippedTerm ); + fetchDefinition( strippedTerm ); + }; + + const setDefinitionData = ( indexKey ) => { + setSelectedSearchTermId( indexKey ); + const { + definition, + partOfSpeech, + phoneticTranscription, + isAbbreviation, + } = dictionaryApi.getSelectedDefinition( + definitionData, + indexKey.split( '-' ) + ); + onSelectDefinition( { + definition, + partOfSpeech, + phoneticTranscription, + isAbbreviation, + } ); + }; + + const showLocalePicker = () => setShouldShowLanguagePicker( true ); + const hideLocalePicker = () => setShouldShowLanguagePicker( false ); + + // Close the search results if the definition term changes. + useEffect( () => { + if ( term !== searchTerm || errorMessage ) { + setShouldShowSearchResults( false ); + } + }, [ term, searchTerm, errorMessage ] ); + + // Set new UI definition data when definitionData from fetch updates. + useEffect( () => { + if ( ! isEmpty( definitionData ) ) { + const newDefinitionOptions = DictionaryApi.getOptionsList( + definitionData + ); + + setDefinitionOptions( newDefinitionOptions ); + + if ( newDefinitionOptions.length > 0 ) { + setShouldShowSearchResults( true ); + } + } + }, [ definitionData ] ); + + return ( + + + + + + Not sure? See{ ' ' } + + Parts of Speech + + + + + + + + + + + + + { ! term && ( + + { ' ' } + { __( + 'Enter a definition term in the Editor block to search.', + 'definition' + ) } + + ) } + { term && ( + + { term && ! shouldShowSearchResults && ( + + ) } + { errorMessage && ( + + { errorMessage } + + ) } + { shouldShowSearchResults && ( + + ) } + + ) } +{/* + { __( 'Current search locale:' ) } + + { shouldShowLanguagePicker && ( +
+ { dictionaryApi + .getSupportedLocales() + .map( ( locale ) => ( + + ) ) } +
+ ) } +
*/} +
+
+ ); +} diff --git a/blocks/definition/src/edit.js b/blocks/definition/src/edit.js new file mode 100644 index 00000000..56ec8e93 --- /dev/null +++ b/blocks/definition/src/edit.js @@ -0,0 +1,142 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls, + RichText, + useBlockProps, +} from '@wordpress/block-editor'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DefinitionControls from './controls'; +import TermMetaData from './components/term-metadata'; +import { PARTS_OF_SPEECH, ABBREVIATION } from './constants'; + +/** + * Definition Edit method. + * + * @param props + * @param props.attributes + * @param props.setAttributes + * @return {WPElement} Element to render. + */ +export default function DefinitionEdit( { attributes, setAttributes } ) { + const { + definition, + term, + isAbbreviation, + partOfSpeech, + phoneticTranscription, + shouldShowTermMeta, + } = attributes; + const blockProps = useBlockProps( { + className: 'wp-block-a8c-definition', + } ); + const onToggleAbbreviation = ( newValue ) => { + const newPartOfSpeech = + newValue && partOfSpeech !== ABBREVIATION + ? ABBREVIATION + : partOfSpeech; + setAttributes( { + isAbbreviation: newValue, + partOfSpeech: newPartOfSpeech, + } ); + }; + const onToggleShouldShowTermMeta = () => + setAttributes( { shouldShowTermMeta: ! shouldShowTermMeta } ); + const onChangePartOfSpeech = ( newValue ) => { + const newIsAbbreviation = + newValue === ABBREVIATION && ! isAbbreviation ? true : false; + setAttributes( { + partOfSpeech: newValue, + isAbbreviation: newIsAbbreviation, + } ); + }; + const setDefinitionData = ( { + definition, + partOfSpeech, + phoneticTranscription, + isAbbreviation, + } ) => { + setAttributes( { + definition, + partOfSpeech, + phoneticTranscription, + isAbbreviation, + } ); + }; + const definitionTagName = isAbbreviation ? 'abbr' : 'dfn'; + + // Reset term data if term is deleted. + useEffect( () => { + if ( ! term ) { + setAttributes( { + partOfSpeech: '', + definition: '', + isAbbreviation: false, + phoneticTranscription: '', + } ); + } + }, [ term ] ); + + return ( + <> + + + +
+
+ + setAttributes( { term: newTerm } ) + } + value={ term } + multiline={ false } + /> + { shouldShowTermMeta && ( + + ) } +
+ + setAttributes( { definition: newDefinition } ) + } + value={ definition } + /> +
+ + ); +} diff --git a/blocks/definition/src/index.js b/blocks/definition/src/index.js new file mode 100644 index 00000000..b73c6c92 --- /dev/null +++ b/blocks/definition/src/index.js @@ -0,0 +1,80 @@ +/** + * External dependencies. + */ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import save from './save'; + +/** + * Register block type definition. + */ +export const registerBlock = () => { + registerBlockType( 'a8c/definition', { + apiVersion: 2, + title: __( 'Definition', 'definition' ), + description: __( 'A term and definition block.', 'definition' ), + category: 'text', + keywords: [ __( 'define', 'definition' ), __( 'term', 'definition' ) ], + icon: 'book', + attributes: { + term: { + type: 'string', + source: 'html', + selector: 'dfn', + default: '', + }, + definition: { + type: 'string', + source: 'html', + selector: 'dd', + default: '', + }, + shouldShowTermMeta: { + type: 'boolean', + default: false, + }, + isAbbreviation: { + type: 'boolean', + default: true, + }, + partOfSpeech: { + type: 'string', + default: '', + }, + phoneticTranscription: { + type: 'string', + default: '', + }, + }, + /* styles: [ + { + name: 'default', + label: __( 'Default', 'definition' ), + isDefault: true, + }, + { + name: 'minimal', + label: __( 'Minimal', 'definition' ), + }, + ],*/ + example: { + attributes: { + term: __( 'Hot dog', 'definition' ), + definition: __( + 'A hot dog (also spelled hotdog) is a food consisting of a grilled or steamed sausage served in the slit of a partially sliced bun.', + 'definition' + ), + }, + }, + edit, + save, + } ); +}; diff --git a/blocks/definition/src/lib/dictionary-apis/class-dictionary-api.js b/blocks/definition/src/lib/dictionary-apis/class-dictionary-api.js new file mode 100644 index 00000000..33f15941 --- /dev/null +++ b/blocks/definition/src/lib/dictionary-apis/class-dictionary-api.js @@ -0,0 +1,141 @@ +/** + * A collection of static methods to fetch/interpret the results of the dictionaryapi API endpoint. + * See: https://dictionaryapi.dev/ + * Each dictionary source class should have the same interface. + */ + +export default class DictionaryApi { + /** + * Returns a list of supported locale slugs. These are the slugs that the API accepts. + * Any transformations between WordPress and 3rd-party slugs should be done in `getFetchUrl`. + * + * @return {Array} A list of supported locale slugs. + */ + static getSupportedLocales() { + return [ + 'ar', + 'de', + 'en', + 'en_GB', + 'es', + 'fr', + 'hi', + 'it', + 'ja', + 'ko', + 'ru', + 'pt-BR', + 'tr', + ]; + } + + /** + * Returns an iterable collection of objects so we can display a select list etc. + * + * @param {Object} definitionData The raw JSON results from a successful call to the API. + * @return {Array} A collection of options. + */ + static getOptionsList( definitionData ) { + const newDefinitionOptions = []; + for ( const definitionsIndex in definitionData ) { + if ( + definitionData.hasOwnProperty( definitionsIndex ) && + Array.isArray( definitionData[ definitionsIndex ].meanings ) + ) { + definitionData[ definitionsIndex ].meanings.forEach( + ( meaning, meaningsIndex ) => { + meaning.definitions.forEach( + ( subDefinition, subDefinitionIndex ) => { + newDefinitionOptions.push( { + value: `${ definitionsIndex }-${ meaningsIndex }-${ subDefinitionIndex }`, + label: `${ subDefinition.definition } [${ meaning.partOfSpeech }]`, + } ); + } + ); + } + ); + } + } + return newDefinitionOptions; + } + + /** + * For the given indices, fetches the result from the API search results data. + * + * @param {Object} definitionData The raw JSON results from a successful call to the API. + * @param {Array} indexArray An array of indices pointing to a child property of the data. The order must correspond to the depth of the target value. + * @return {Object} The properties of the definition. The properties correspond to the expected block attributes. + */ + static getSelectedDefinition( definitionData, indexArray = [] ) { + const definition = + definitionData[ indexArray[ 0 ] ].meanings[ indexArray[ 1 ] ] + .definitions[ indexArray[ 2 ] ].definition; + const partOfSpeech = + definitionData[ indexArray[ 0 ] ].meanings[ indexArray[ 1 ] ] + .partOfSpeech; + let isAbbreviation = false; + if ( partOfSpeech === 'abbreviation' ) { + isAbbreviation = true; + } + const phoneticTranscription = + definitionData[ indexArray[ 0 ] ].phonetics[ indexArray[ 1 ] ] + ?.text || + definitionData[ indexArray[ 0 ] ].phonetics[ 0 ]?.text; + // TODO: add pronunciation audio. Low prio. + //const newPhoneticAudio = definitionData[ indexArray[0] ].phonetics[ indexArray[1] ]?.audio || definitionData[ indexArray[0] ].phonetics[0]?.audio; + return { + definition, + partOfSpeech, + phoneticTranscription, + isAbbreviation, + }; + } + + /** + * Takes a WordPress language code and returns the API's corresponding code. + * Not a required on the public interface. + * + * @param slug + * @return {string} The locale code. + */ + static _getApiSlug( slug ) { + const supportedLocales = DictionaryApi.getSupportedLocales(); + + // Check if there's a direct match. + if ( supportedLocales.indexOf( slug ) > -1 ) { + return slug; + } + + // Return any custom transforms. + const wpToApiSlugDictionary = { + pt_BR: 'pt-BR', + }; + + if ( wpToApiSlugDictionary[ slug ] ) { + return wpToApiSlugDictionary[ slug ]; + } + + // Finally check if there's match on the root of any language variants. + if ( slug[ 2 ] === '_' ) { + slug = slug.split( '_' )[ 0 ]; + } + + if ( supportedLocales.indexOf( slug ) > -1 ) { + return slug; + } + + return slug; + } + + /** + * Returns a concatenated URL to fetch a definition for a given term and language. + * + * @param {string} term The search term. + * @param {string} lang The locale. + * @return {string} The URL. + */ + static getFetchUrl( term, lang = 'en' ) { + const apiLocale = DictionaryApi._getApiSlug( lang ); + return `https://api.dictionaryapi.dev/api/v2/entries/${ apiLocale }/${ term }`; + } +} diff --git a/blocks/definition/src/lib/dictionary-apis/index.js b/blocks/definition/src/lib/dictionary-apis/index.js new file mode 100644 index 00000000..2a077fe1 --- /dev/null +++ b/blocks/definition/src/lib/dictionary-apis/index.js @@ -0,0 +1,3 @@ +import DictionaryApi from './class-dictionary-api'; + +export { DictionaryApi }; diff --git a/blocks/definition/src/save.js b/blocks/definition/src/save.js new file mode 100644 index 00000000..3e1f26e6 --- /dev/null +++ b/blocks/definition/src/save.js @@ -0,0 +1,53 @@ +/** + * External dependencies. + */ +import { RichText, useBlockProps } from '@wordpress/block-editor'; +import TermMetaData from './components/term-metadata'; + +/** + * Save method. Renders the frontend markup. + * + * @param props + * @param props.attributes + * @return {WPElement} Element to render. + */ +export default function save( { attributes } ) { + const { + definition, + term, + isAbbreviation, + partOfSpeech, + phoneticTranscription, + shouldShowTermMeta, + } = attributes; + const blockProps = useBlockProps.save( { + className: 'wp-block-a8c-definition', + } ); + const definitionTagName = isAbbreviation ? 'abbr' : 'dfn'; + + return ( +
+
+ + { shouldShowTermMeta && ( + + ) } +
+ +
+ ); +} diff --git a/blocks/definition/src/use-fetch-definition.js b/blocks/definition/src/use-fetch-definition.js new file mode 100644 index 00000000..19c5492a --- /dev/null +++ b/blocks/definition/src/use-fetch-definition.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { DEFAULT_LOCALE } from './constants'; + +/** + * A hook to fetch a dictionary definition from an API. + * + * @param {string} initialSearchTermValue The initial value of the search term. + * @param {string} locale The target language of the definition. + * @return {object} A object of methods and properties to access API request data. + */ +const useFetchDefinition = ( { + initialSearchTermValue = '', + locale = DEFAULT_LOCALE, + api, +} ) => { + const [ isFetching, setIsFetching ] = useState( false ); + const [ term, setTerm ] = useState( initialSearchTermValue ); + const [ definitionData, setDefinitionData ] = useState( {} ); + const [ errorMessage, setErrorMessage ] = useState( '' ); + const defaultErrorMessage = __( + "Sorry, we couldn't find a definition.", + 'definition' + ); + + useEffect( () => { + if ( ! term ) { + return; + } + + setErrorMessage( '' ); + + // TODO: support other locales. Medium priority. + // TODO: support other dictionary sources. Low priority. + const fetchUrl = api.getFetchUrl( term, locale ); + + const fetchResults = async () => { + setIsFetching( true ); + + const definitionFetch = await window.fetch( fetchUrl ) + .then( ( response ) => { + if ( response.ok ) { + return response; + } + setErrorMessage( defaultErrorMessage ); + return false; + } ) + .catch( () => { + setErrorMessage( defaultErrorMessage ); + return false; + } ); + + if ( definitionFetch ) { + const definitionResponse = await definitionFetch.json(); + setDefinitionData( { + term, + ...definitionResponse + } ); + } + setIsFetching( false ); + }; + + fetchResults(); + }, [ term ] ); + + return { isFetching, definitionData, errorMessage, fetchDefinition: setTerm }; +}; + +export default useFetchDefinition; diff --git a/blocks/definition/src/utils.js b/blocks/definition/src/utils.js new file mode 100644 index 00000000..743e7bbd --- /dev/null +++ b/blocks/definition/src/utils.js @@ -0,0 +1,34 @@ +/** + * External dependencies. + */ +import { getLocaleData } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { DEFAULT_LOCALE } from './constants'; + +/** + * Returns current locale slug. + * + * @return {string} The locale slug. + */ +export function getLocaleFromLocaleData() { + const localeData = getLocaleData(); + return ( localeData && localeData[ '' ]?.lang ) || DEFAULT_LOCALE; +} + +/** + * Returns a member from a collection of objects where key === value. + * + * @param {Array} collection The collection to search. + * @param {String} key The property name. + * @param {String|Number|Boolean} value The value of the property. + * @return {Object|null} The locale slug. + */ +export function findInCollection( collection, key, value ) { + if ( collection instanceof Object ) { + return collection.find( ( item ) => item[ key ] === value ); + } + return null; +} diff --git a/blocks/definition/style.scss b/blocks/definition/style.scss new file mode 100644 index 00000000..1470e139 --- /dev/null +++ b/blocks/definition/style.scss @@ -0,0 +1,46 @@ +/** + * Editor styles + */ + +.wp-block-a8c-definition__panel-row { + display: block; + margin-bottom: 20px; +} +.wp-block-a8c-definition__search-results-container { + background: white; + padding: 10px; + position: relative; +} +.wp-block-a8c-definition__search-control-container { + .wp-block-a8c-definition__current-locale, + .wp-block-a8c-definition__search-button { + padding: 0px 10px; + text-decoration: none; + height: auto; + } +} + +.wp-block-a8c-definition__current-locale { + display: flex; + align-items: flex-start; + justify-content: flex-end; + .wp-block-a8c-definition__current-locale-button { + margin-left:8px; + } +} + +.wp-block-a8c-definition__search-results-title { + margin: 0 0 4px 0; + color: grey; + text-transform: uppercase; + font-size: 11px; +} +.wp-block-a8c-definition__search-results-item .wp-block-a8c-definition__search-results-item-button { + border: 0; + white-space: normal; + text-align: left; + height: auto; +} +.wp-block-a8c-definition__error-message { + color: crimson; +} diff --git a/editor.scss b/editor.scss index 1a0f9502..3bb46413 100644 --- a/editor.scss +++ b/editor.scss @@ -7,3 +7,4 @@ @import './blocks/starscape/editor.scss'; @import './blocks/waves/editor.scss'; @import './blocks/book/editor.scss'; +@import './blocks/definition/editor.scss'; diff --git a/src/index.js b/src/index.js index 283c3cd3..760772b6 100644 --- a/src/index.js +++ b/src/index.js @@ -30,6 +30,7 @@ import * as sketchBlock from '../blocks/sketch/src'; import * as starscapeBlock from '../blocks/starscape/src'; import * as wavesBlock from '../blocks/waves/src'; import * as bookBlock from '../blocks/book/src'; +import * as definitionBlock from '../blocks/definition/src'; // Instantiate the blocks, adding them to our block category bauhausCentenaryBlock.registerBlock(); @@ -40,3 +41,4 @@ sketchBlock.registerBlock(); starscapeBlock.registerBlock(); wavesBlock.registerBlock(); bookBlock.registerBlock(); +definitionBlock.registerBlock(); diff --git a/style.scss b/style.scss index 1cb0e437..a7b98336 100644 --- a/style.scss +++ b/style.scss @@ -7,3 +7,4 @@ @import './blocks/starscape/style.scss'; @import './blocks/waves/style.scss'; @import './blocks/book/style.scss'; +@import './blocks/definition/style.scss';