├── src ├── index.js ├── block │ ├── editor.scss │ ├── settings.js │ ├── transforms.js │ ├── index.scss │ ├── index.js │ ├── deprecated.js │ ├── html-tag-icon.js │ └── edit.js ├── utils │ └── classnames.js └── store │ └── index.js ├── .gitignore ├── .gitattributes ├── .github └── FUNDING.yml ├── assets ├── icon-128x128.png ├── icon-256x256.png ├── screenshot-1.png └── screenshot-2.png ├── .editorconfig ├── package.json ├── block.json ├── js ├── accordion-blocks.min.js └── accordion-blocks.js ├── accordion-blocks.php └── readme.txt /src/index.js: -------------------------------------------------------------------------------- 1 | import './store'; 2 | import './block'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.map 4 | .nova 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: philbuchanan 4 | -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philbuchanan/Accordion-Blocks/HEAD/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philbuchanan/Accordion-Blocks/HEAD/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philbuchanan/Accordion-Blocks/HEAD/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philbuchanan/Accordion-Blocks/HEAD/assets/screenshot-2.png -------------------------------------------------------------------------------- /src/block/editor.scss: -------------------------------------------------------------------------------- 1 | .editor-styles-wrapper { 2 | .c-accordion__item { 3 | &.is-selected { 4 | border-bottom: 1px solid var(--wp-admin-theme-color) !important; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig File 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | # 4 space tab indentation 11 | indent_style = tab 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accordion-blocks", 3 | "version": "1.4.1", 4 | "description": "Gutenberg blocks for creating responsive accordion drop-downs.", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "wp-scripts start", 8 | "build": "wp-scripts build" 9 | }, 10 | "author": "pbuchanan", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@wordpress/scripts": "19.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/classnames.js: -------------------------------------------------------------------------------- 1 | function classnames() { 2 | let classes = []; 3 | 4 | for (let i = 0; i < arguments.length; i++) { 5 | let arg = arguments[i]; 6 | 7 | if (!arg) { 8 | continue; 9 | } 10 | 11 | let argType = typeof arg; 12 | 13 | if (argType === 'string' || argType === 'number') { 14 | classes.push(arg); 15 | } 16 | else if (Array.isArray(arg) && arg.length) { 17 | let inner = classnames.apply(null, arg); 18 | 19 | if (inner) { 20 | classes.push(inner); 21 | } 22 | } 23 | else if (argType === 'object') { 24 | for (let key in arg) { 25 | if (hasOwnProperty.call(arg, key) && arg[key]) { 26 | classes.push(key); 27 | } 28 | } 29 | } 30 | } 31 | 32 | return classes.join(' '); 33 | } 34 | 35 | export default classnames; 36 | -------------------------------------------------------------------------------- /src/block/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { SVG, Path } from '@wordpress/components'; 6 | 7 | export default { 8 | icon: 9 | 10 | 11 | 12 | , 13 | 14 | example: { 15 | attributes: { 16 | title: __('Accordion item title', 'accordion-blocks'), 17 | titleTag: 'h3', 18 | }, 19 | innerBlocks: [{ 20 | name: 'core/paragraph', 21 | attributes: { 22 | content: __('Sample accordion item content for previewing styles in the editor.', 'accordion-blocks'), 23 | }, 24 | }], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /block.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "2", 3 | "name": "pb/accordion-item", 4 | "title": "Accordion Item", 5 | "category": "text", 6 | "textdomain": "accordion-blocks", 7 | "attributes": { 8 | "title": { 9 | "type": "string", 10 | "source": "html", 11 | "selector": ".c-accordion__title" 12 | }, 13 | "initiallyOpen": { 14 | "type": "boolean", 15 | "default": false 16 | }, 17 | "clickToClose": { 18 | "type": "boolean", 19 | "default": true 20 | }, 21 | "autoClose": { 22 | "type": "boolean", 23 | "default": true 24 | }, 25 | "titleTag": { 26 | "type": "string", 27 | "default": "h2" 28 | }, 29 | "scroll": { 30 | "type": "boolean", 31 | "default": false 32 | }, 33 | "scrollOffset": { 34 | "type": "number", 35 | "default": 0 36 | }, 37 | "uuid": { 38 | "type": "number" 39 | } 40 | }, 41 | "supports": { 42 | "anchor": true 43 | }, 44 | "editorScript": "file:./build/index.js", 45 | "editorStyle": "file:./build/index.css" 46 | } 47 | -------------------------------------------------------------------------------- /src/block/transforms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createBlock } from '@wordpress/blocks'; 5 | 6 | export default { 7 | from: [ 8 | { 9 | type: 'block', 10 | blocks: ['core/heading'], 11 | transform: (attributes) => { 12 | return createBlock('pb/accordion-item', { 13 | title: attributes.content, 14 | titleTag: 'h' + (attributes.level <= 4 ? attributes.level : 2), 15 | }); 16 | }, 17 | }, 18 | { 19 | type: 'block', 20 | isMultiBlock: true, 21 | blocks: ['core/paragraph'], 22 | transform: (attributes) => { 23 | return createBlock('pb/accordion-item', {}, attributes.map( 24 | item => createBlock('core/paragraph', { 25 | content: item.content 26 | }) 27 | )); 28 | }, 29 | }, 30 | ], 31 | to: [ 32 | { 33 | type: 'block', 34 | blocks: ['core/paragraph'], 35 | transform: (attributes, innerBlocks) => { 36 | let newBlocks = innerBlocks.map(block => createBlock(block.name, block.attributes)); 37 | 38 | const level = attributes.titleTag !== 'button' ? 39 | parseInt(attributes.titleTag.replace('h', '')) : 2; 40 | 41 | newBlocks.splice(0, 0, createBlock('core/heading', { 42 | content: attributes.title, 43 | anchor: attributes.anchor, 44 | className: attributes.className, 45 | level: level, 46 | })); 47 | 48 | return newBlocks; 49 | }, 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createReduxStore, register } from '@wordpress/data'; 5 | import apiFetch from '@wordpress/api-fetch'; 6 | 7 | const initialState = { 8 | defaults: { 9 | initiallyOpen: false, 10 | clickToClose: true, 11 | autoClose: true, 12 | scroll: false, 13 | scrollOffset: 0, 14 | }, 15 | }; 16 | 17 | const actions = { 18 | setDefaults(defaults) { 19 | return { 20 | type: 'SET_DEFAULTS', 21 | defaults, 22 | }; 23 | }, 24 | saveDefaultSettings(defaults) { 25 | return { 26 | type: 'SAVE_DEFAULTS', 27 | defaults, 28 | }; 29 | }, 30 | fetchFromAPI(path) { 31 | return { 32 | type: 'FETCH_FROM_API', 33 | path, 34 | }; 35 | }, 36 | }; 37 | 38 | const store = createReduxStore('accordion-blocks', { 39 | reducer(state = initialState, action) { 40 | switch (action.type) { 41 | case 'SET_DEFAULTS': 42 | return Object.assign({}, state, { 43 | defaults: action.defaults 44 | }); 45 | case 'SAVE_DEFAULTS': 46 | apiFetch({ 47 | path: 'accordion-blocks/v1/defaults', 48 | data: action.defaults, 49 | method: 'POST', 50 | }) 51 | .then((response) => {}) 52 | .catch((error) => {}); 53 | 54 | return Object.assign({}, state, { 55 | defaults: action.defaults 56 | }); 57 | default: 58 | return state; 59 | } 60 | }, 61 | 62 | actions, 63 | 64 | selectors: { 65 | getDefaultSettings(state) { 66 | return state.defaults; 67 | }, 68 | }, 69 | 70 | controls: { 71 | FETCH_FROM_API(action) { 72 | return apiFetch({ 73 | path: action.path 74 | }); 75 | }, 76 | }, 77 | 78 | resolvers: { 79 | * getDefaultSettings() { 80 | const path = '/accordion-blocks/v1/defaults'; 81 | const defaults = yield actions.fetchFromAPI(path); 82 | 83 | return actions.setDefaults(defaults); 84 | }, 85 | }, 86 | }); 87 | 88 | register(store); 89 | -------------------------------------------------------------------------------- /src/block/index.scss: -------------------------------------------------------------------------------- 1 | // # Variables 2 | // ----------------------------------------------------------------------------- 3 | 4 | $color-border: #ddd; 5 | $color-open-icon: #777; 6 | 7 | 8 | 9 | // # Accordion Default Styles 10 | // ----------------------------------------------------------------------------- 11 | 12 | // Force all content open if there is no JS. 13 | .c-accordion__item.no-js { 14 | .c-accordion__content { 15 | display: block !important; 16 | } 17 | 18 | .c-accordion__title { 19 | padding-right: none; 20 | cursor: default; 21 | 22 | &:after { 23 | display: none; 24 | } 25 | } 26 | } 27 | 28 | .c-accordion__title--button { 29 | display: inline-block; 30 | color: inherit; 31 | background-color: transparent; 32 | text-align: left; 33 | vertical-align: middle; 34 | font: inherit; 35 | text-decoration: none; 36 | direction: ltr; 37 | border: none; 38 | border-radius: 0; 39 | width: 100%; 40 | height: auto; 41 | padding: 0; 42 | margin: 0; 43 | transition: 0; 44 | box-shadow: none; 45 | overflow: auto; 46 | -webkit-appearance: none; 47 | -moz-appearance: none; 48 | appearance: none; 49 | 50 | &:focus, 51 | &:hover { 52 | color: inherit; 53 | background-color: transparent; 54 | } 55 | } 56 | 57 | .c-accordion__title { 58 | position: relative; 59 | padding-right: 2rem; 60 | cursor: pointer; 61 | 62 | &:after { 63 | position: absolute; 64 | top: 50%; 65 | right: 0; 66 | content: "+"; 67 | color: $color-open-icon; 68 | font-weight: 300; 69 | -webkit-transform: translateY(-50%); 70 | transform: translateY(-50%); 71 | } 72 | } 73 | 74 | .is-open { 75 | > .c-accordion__title { 76 | &:after { 77 | content: "\2212"; 78 | } 79 | } 80 | } 81 | 82 | [data-initially-open="false"] { 83 | .c-accordion__content { 84 | display: none; 85 | } 86 | } 87 | 88 | // Make all the content visible when printing the page. 89 | @media print { 90 | .c-accordion__content { 91 | display: block !important; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/block/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { registerBlockType } from '@wordpress/blocks'; 5 | import { 6 | RichText, 7 | useBlockProps, 8 | useInnerBlocksProps, 9 | } from '@wordpress/block-editor'; 10 | 11 | /** 12 | * Internal dependencies 13 | */ 14 | import './index.scss'; 15 | import './editor.scss'; 16 | import settings from './settings'; 17 | import transforms from './transforms'; 18 | import edit from './edit'; 19 | import deprecated from './deprecated'; 20 | 21 | registerBlockType('pb/accordion-item', { 22 | ...settings, 23 | 24 | transforms, 25 | 26 | edit, 27 | 28 | save: ({ attributes }) => { 29 | const { 30 | className, 31 | title, 32 | initiallyOpen, 33 | clickToClose, 34 | autoClose, 35 | titleTag, 36 | scroll, 37 | scrollOffset, 38 | uuid, 39 | } = attributes; 40 | 41 | let itemClasses = [ 42 | 'c-accordion__item', 43 | 'js-accordion-item', 44 | 'no-js', 45 | ]; 46 | 47 | let titleClasses = [ 48 | 'c-accordion__title', 49 | 'js-accordion-controller', 50 | ]; 51 | 52 | let contentStyles = {}; 53 | 54 | if (titleTag === 'button') { 55 | titleClasses.push('c-accordion__title--button'); 56 | } 57 | 58 | if (initiallyOpen) { 59 | itemClasses.push('is-open'); 60 | } 61 | else { 62 | contentStyles.display = 'none'; 63 | } 64 | 65 | const blockProps = useBlockProps.save({ 66 | className: [...itemClasses, className].join(' '), 67 | 'data-initially-open': initiallyOpen, 68 | 'data-click-to-close': clickToClose, 69 | 'data-auto-close': autoClose, 70 | 'data-scroll': scroll, 71 | 'data-scroll-offset': scrollOffset, 72 | }); 73 | 74 | const innerBlocksProps = useInnerBlocksProps.save({ 75 | id: 'ac-' + uuid, 76 | className: 'c-accordion__content', 77 | }); 78 | 79 | return ( 80 |
81 | 88 |
89 |
90 | ); 91 | }, 92 | 93 | deprecated, 94 | }); 95 | -------------------------------------------------------------------------------- /js/accordion-blocks.min.js: -------------------------------------------------------------------------------- 1 | !function(u){"use strict";u(".js-accordion-item").removeClass("no-js"),u.fn.accordionBlockItem=function(o){var n=u.extend({initiallyOpen:!1,autoClose:!0,clickToClose:!0,scroll:!1,scrollOffset:!1},o),t=250,c=window.location.hash.replace("#",""),e={};function i(o){return u(o).children(".js-accordion-controller").attr("id").replace("at-","")}function s(){r(),e.content.clearQueue().stop().slideDown(t,function(){n.scroll&&setTimeout(function(){u("html, body").animate({scrollTop:e.self.offset().top-n.scrollOffset},t)},t)}),u(document).trigger("openAccordionItem",e)}function r(){e.self.addClass("is-open is-read"),e.controller.attr("aria-expanded",!0),e.content.prop("hidden",!1)}function l(){e.content.slideUp(t,function(){a()})}function a(){e.self.removeClass("is-open"),e.controller.attr("aria-expanded",!1),e.content.attr("hidden",!0)}function d(){n.autoClose&&e.self.hasClass("is-open")&&l()}return e.self=u(this),e.id=u(this).attr("id"),e.controller=u(this).children(".js-accordion-controller"),e.uuid=i(e.self),e.content=u("#ac-"+e.uuid),e.accordionGroupItems=[e.uuid],e.accordionAncestorItems=[],e.controller.attr({tabindex:0,"aria-controls":"ac-"+e.uuid}),n.scrollOffset=Math.floor(parseInt(n.scrollOffset,10))||0,u.each(e.self.siblings(".js-accordion-item"),function(o,n){var t=i(n);e.accordionGroupItems.push(t)}),u.each(e.self.parents(".js-accordion-item"),function(o,n){var t=i(n);e.accordionAncestorItems.push(t)}),n.initiallyOpen?r():e.id===c?(s(),u.each(e.accordionAncestorItems,function(o,n){u(document).trigger("openAncestorAccordionItem",n)})):a(),e.controller.on("click",function(){return e.self.hasClass("is-open")?n.clickToClose&&l():s(),!1}),u(document).on("openAccordionItem",function(o,n){n!==e&&0 { 48 | const { 49 | className, 50 | title, 51 | initiallyOpen, 52 | clickToClose, 53 | autoClose, 54 | titleTag, 55 | scroll, 56 | scrollOffset, 57 | uuid, 58 | } = attributes; 59 | 60 | let itemClasses = [ 61 | 'c-accordion__item', 62 | 'js-accordion-item', 63 | 'no-js', 64 | ]; 65 | 66 | let titleClasses = [ 67 | 'c-accordion__title', 68 | 'js-accordion-controller', 69 | ]; 70 | 71 | let contentStyles = {}; 72 | 73 | if (titleTag === 'button') { 74 | titleClasses.push('c-accordion__title--button'); 75 | } 76 | 77 | if (initiallyOpen) { 78 | itemClasses.push('is-open'); 79 | } 80 | else { 81 | contentStyles.display = 'none'; 82 | } 83 | 84 | return ( 85 |
93 | 102 |
107 | 108 |
109 |
110 | ); 111 | }, 112 | }, 113 | ]; 114 | 115 | export default deprecated; 116 | -------------------------------------------------------------------------------- /src/block/html-tag-icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wordpress dependencies 3 | */ 4 | import { SVG, Path } from '@wordpress/components'; 5 | 6 | const HtmlTagIcon = ({ tag }) => { 7 | const tagToPath = { 8 | h1: 'M9 5h2v10H9v-4H5v4H3V5h2v4h4V5zm6.6 0c-.6.9-1.5 1.7-2.6 2v1h2v7h2V5h-1.4z', 9 | h2: 'M7 5h2v10H7v-4H3v4H1V5h2v4h4V5zm8 8c.5-.4.6-.6 1.1-1.1.4-.4.8-.8 1.2-1.3.3-.4.6-.8.9-1.3.2-.4.3-.8.3-1.3 0-.4-.1-.9-.3-1.3-.2-.4-.4-.7-.8-1-.3-.3-.7-.5-1.2-.6-.5-.2-1-.2-1.5-.2-.4 0-.7 0-1.1.1-.3.1-.7.2-1 .3-.3.1-.6.3-.9.5-.3.2-.6.4-.8.7l1.2 1.2c.3-.3.6-.5 1-.7.4-.2.7-.3 1.2-.3s.9.1 1.3.4c.3.3.5.7.5 1.1 0 .4-.1.8-.4 1.1-.3.5-.6.9-1 1.2-.4.4-1 .9-1.6 1.4-.6.5-1.4 1.1-2.2 1.6V15h8v-2H15z', 10 | h3: 'M12.1 12.2c.4.3.8.5 1.2.7.4.2.9.3 1.4.3.5 0 1-.1 1.4-.3.3-.1.5-.5.5-.8 0-.2 0-.4-.1-.6-.1-.2-.3-.3-.5-.4-.3-.1-.7-.2-1-.3-.5-.1-1-.1-1.5-.1V9.1c.7.1 1.5-.1 2.2-.4.4-.2.6-.5.6-.9 0-.3-.1-.6-.4-.8-.3-.2-.7-.3-1.1-.3-.4 0-.8.1-1.1.3-.4.2-.7.4-1.1.6l-1.2-1.4c.5-.4 1.1-.7 1.6-.9.5-.2 1.2-.3 1.8-.3.5 0 1 .1 1.6.2.4.1.8.3 1.2.5.3.2.6.5.8.8.2.3.3.7.3 1.1 0 .5-.2.9-.5 1.3-.4.4-.9.7-1.5.9v.1c.6.1 1.2.4 1.6.8.4.4.7.9.7 1.5 0 .4-.1.8-.3 1.2-.2.4-.5.7-.9.9-.4.3-.9.4-1.3.5-.5.1-1 .2-1.6.2-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1l1.1-1.4zM7 9H3V5H1v10h2v-4h4v4h2V5H7v4z', 11 | h4: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm10-2h-1v2h-2v-2h-5v-2l4-6h3v6h1v2zm-3-2V7l-2.8 4H16z', 12 | h5: 'M12.1 12.2c.4.3.7.5 1.1.7.4.2.9.3 1.3.3.5 0 1-.1 1.4-.4.4-.3.6-.7.6-1.1 0-.4-.2-.9-.6-1.1-.4-.3-.9-.4-1.4-.4H14c-.1 0-.3 0-.4.1l-.4.1-.5.2-1-.6.3-5h6.4v1.9h-4.3L14 8.8c.2-.1.5-.1.7-.2.2 0 .5-.1.7-.1.5 0 .9.1 1.4.2.4.1.8.3 1.1.6.3.2.6.6.8.9.2.4.3.9.3 1.4 0 .5-.1 1-.3 1.4-.2.4-.5.8-.9 1.1-.4.3-.8.5-1.3.7-.5.2-1 .3-1.5.3-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1-.1-.1 1-1.5 1-1.5zM9 15H7v-4H3v4H1V5h2v4h4V5h2v10z', 13 | h6: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm8.6-7.5c-.2-.2-.5-.4-.8-.5-.6-.2-1.3-.2-1.9 0-.3.1-.6.3-.8.5l-.6.9c-.2.5-.2.9-.2 1.4.4-.3.8-.6 1.2-.8.4-.2.8-.3 1.3-.3.4 0 .8 0 1.2.2.4.1.7.3 1 .6.3.3.5.6.7.9.2.4.3.8.3 1.3s-.1.9-.3 1.4c-.2.4-.5.7-.8 1-.4.3-.8.5-1.2.6-1 .3-2 .3-3 0-.5-.2-1-.5-1.4-.9-.4-.4-.8-.9-1-1.5-.2-.6-.3-1.3-.3-2.1s.1-1.6.4-2.3c.2-.6.6-1.2 1-1.6.4-.4.9-.7 1.4-.9.6-.3 1.1-.4 1.7-.4.7 0 1.4.1 2 .3.5.2 1 .5 1.4.8 0 .1-1.3 1.4-1.3 1.4zm-2.4 5.8c.2 0 .4 0 .6-.1.2 0 .4-.1.5-.2.1-.1.3-.3.4-.5.1-.2.1-.5.1-.7 0-.4-.1-.8-.4-1.1-.3-.2-.7-.3-1.1-.3-.3 0-.7.1-1 .2-.4.2-.7.4-1 .7 0 .3.1.7.3 1 .1.2.3.4.4.6.2.1.3.3.5.3.2.1.5.2.7.1z', 14 | button: 'M17.5,4.5H2.5A1.5,1.5,0,0,0,1,6v8a1.5,1.5,0,0,0,1.5,1.5h15A1.5,1.5,0,0,0,19,14V6A1.5,1.5,0,0,0,17.5,4.5Zm0,8.75a.76.76,0,0,1-.75.75H3.25a.76.76,0,0,1-.75-.75V6.75A.76.76,0,0,1,3.25,6h13.5a.76.76,0,0,1,.75.75ZM5.5,11h9V9h-9Z', 15 | }; 16 | 17 | return ( 18 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default HtmlTagIcon; 30 | -------------------------------------------------------------------------------- /js/accordion-blocks.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 'use strict'; 3 | 4 | // Remove the 'no-js' class since JavaScript is enabled 5 | $('.js-accordion-item').removeClass('no-js'); 6 | 7 | 8 | 9 | /** 10 | * Accordion Blocks plugin function 11 | * 12 | * @param object options Plugin settings to override the defaults 13 | */ 14 | $.fn.accordionBlockItem = function(options) { 15 | var settings = $.extend({ 16 | // Set default settings 17 | initiallyOpen: false, 18 | autoClose: true, 19 | clickToClose: true, 20 | scroll: false, 21 | scrollOffset: false, 22 | }, options); 23 | 24 | var duration = 250; 25 | var hashID = window.location.hash.replace('#', ''); 26 | 27 | var item = {}; 28 | 29 | item.self = $(this); 30 | item.id = $(this).attr('id'); 31 | item.controller = $(this).children('.js-accordion-controller'); 32 | item.uuid = getAccordionItemUUID(item.self); 33 | item.content = $('#ac-' + item.uuid); 34 | item.accordionGroupItems = [item.uuid]; 35 | item.accordionAncestorItems = []; 36 | 37 | 38 | 39 | /** 40 | * Initial setup 41 | * Set the scroll offset, and figure out which items should be open by 42 | * default. 43 | */ 44 | (function initialSetup() { 45 | /** 46 | * Set up some defaults for this controller 47 | * These cannot be set in the blocks `save` function because 48 | * WordPress strips `tabindex` and `aria-controls` attributes from 49 | * saved post content. See `_wp_add_global_attributes` function in 50 | * wp-includes/kses.php for list of allowed attributes. 51 | */ 52 | item.controller.attr({ 53 | 'tabindex': 0, 54 | 'aria-controls': 'ac-' + item.uuid, 55 | }); 56 | 57 | settings.scrollOffset = Math.floor(parseInt(settings.scrollOffset, 10)) || 0; 58 | 59 | /** 60 | * Add any sibling accordion items to the accordionGroupItems array. 61 | */ 62 | $.each(item.self.siblings('.js-accordion-item'), function(index, ele) { 63 | var uuid = getAccordionItemUUID(ele); 64 | 65 | item.accordionGroupItems.push(uuid); 66 | }); 67 | 68 | /** 69 | * Add any parent accordion items to the accordionAncestorItems array. 70 | */ 71 | $.each(item.self.parents('.js-accordion-item'), function(index, ele) { 72 | var uuid = getAccordionItemUUID(ele); 73 | 74 | item.accordionAncestorItems.push(uuid); 75 | }); 76 | 77 | // If this item has `initially-open prop` set to true, open it 78 | if (settings.initiallyOpen) { 79 | /** 80 | * We aren't opening the item here (only setting open attributes) 81 | * because the openItem() function fires the `openAccordionItem` 82 | * event which, if `autoClose` is set, would override the users 83 | * defined initiallyOpen settings. 84 | * 85 | * Only setting open attributes is fine since the item's content 86 | * display (`display: none|block`) is already set by the plugin. 87 | */ 88 | setOpenItemAttributes(); 89 | } 90 | // If the hash matches this item, open it 91 | else if (item.id === hashID) { 92 | /** 93 | * Unlike the `initiallyOpen` case above, if a hash is detected 94 | * that matches one of the accordion items, we probably _want_ 95 | * the other items to close so the user can focus on this item. 96 | */ 97 | openItem(); 98 | 99 | // Open ancestors if necessary 100 | $.each(item.accordionAncestorItems, function(index, uuid) { 101 | $(document).trigger('openAncestorAccordionItem', uuid); 102 | }); 103 | } 104 | // Otherwise, close the item 105 | else { 106 | /** 107 | * Don't use closeItem() function call since it animates the 108 | * closing. Instead, we only need to set the closed attributes. 109 | */ 110 | setCloseItemAttributes(); 111 | } 112 | })(); 113 | 114 | 115 | 116 | /** 117 | * Default click function 118 | * Called when an accordion controller is clicked. 119 | */ 120 | function clickHandler() { 121 | // Only open the item if item isn't already open 122 | if (!item.self.hasClass('is-open')) { 123 | // Open clicked item 124 | openItem(); 125 | } 126 | // If item is open, and click to close is set, close it 127 | else if (settings.clickToClose) { 128 | closeItem(); 129 | } 130 | 131 | return false; 132 | } 133 | 134 | 135 | 136 | /** 137 | * Get the accordion item UUID for a given accordion item DOM element. 138 | */ 139 | function getAccordionItemUUID(ele) { 140 | return $(ele).children('.js-accordion-controller').attr('id').replace('at-', ''); 141 | } 142 | 143 | 144 | 145 | /** 146 | * Opens an accordion item 147 | * Also handles accessibility attribute settings. 148 | */ 149 | function openItem() { 150 | setOpenItemAttributes(); 151 | 152 | // Clear/stop any previous animations before revealing content 153 | item.content.clearQueue().stop().slideDown(duration, function() { 154 | // Scroll page to the title 155 | if (settings.scroll) { 156 | // Pause scrolling until other items have closed 157 | setTimeout(function() { 158 | $('html, body').animate({ 159 | scrollTop: item.self.offset().top - settings.scrollOffset 160 | }, duration); 161 | }, duration); 162 | } 163 | }); 164 | 165 | $(document).trigger('openAccordionItem', item); 166 | } 167 | 168 | 169 | 170 | /** 171 | * Set open item attributes 172 | * Mark accordion item as open and read and set aria attributes. 173 | */ 174 | function setOpenItemAttributes() { 175 | item.self.addClass('is-open is-read'); 176 | item.controller.attr('aria-expanded', true); 177 | item.content.prop('hidden', false); 178 | } 179 | 180 | 181 | 182 | /** 183 | * Closes an accordion item 184 | * Also handles accessibility attribute settings. 185 | */ 186 | function closeItem() { 187 | // Close the item 188 | item.content.slideUp(duration, function() { 189 | setCloseItemAttributes(); 190 | }); 191 | } 192 | 193 | 194 | 195 | /** 196 | * Set closed item attributes 197 | * Mark accordion item as closed and set aria attributes. 198 | */ 199 | function setCloseItemAttributes() { 200 | item.self.removeClass('is-open'); 201 | item.controller.attr('aria-expanded', false); 202 | item.content.attr('hidden', true); 203 | } 204 | 205 | 206 | 207 | /** 208 | * Close all items if auto close is enabled 209 | */ 210 | function maybeCloseItem() { 211 | if (settings.autoClose && item.self.hasClass('is-open')) { 212 | closeItem(); 213 | } 214 | } 215 | 216 | 217 | 218 | /** 219 | * Add event listeners 220 | */ 221 | item.controller.on('click', clickHandler); 222 | 223 | 224 | 225 | /** 226 | * Listen for other accordion items opening 227 | * 228 | * The `openAccordionItem` event is fired whenever an accordion item is 229 | * opened after initial plugin setup. 230 | */ 231 | $(document).on('openAccordionItem', function(event, ele) { 232 | /** 233 | * Only trigger potential close these conditions are met: 234 | * 235 | * 1. This isn't the item the user just clicked to open. 236 | * 2. This accordion is in the same group of accordions as the one 237 | * that was just clicked to open. 238 | * 3. This accordion is not an ancestor of the item that was just 239 | * clicked to open. 240 | * 241 | * This serves two purposes: 242 | * 243 | * 1. It allows nesting of accordions to work. 244 | * 2. It allows users to group accordions to control independently 245 | * of other groups of accordions. 246 | * 3. It allows child accordions to be opened via hash change 247 | * without automatically closing the parent accordion, therefore 248 | * hiding the accordion the user just indicated they wanted open. 249 | */ 250 | if ( 251 | ele !== item && 252 | ele.accordionGroupItems.indexOf(item.uuid) > 0 && 253 | ele.accordionAncestorItems.indexOf(item.uuid) === -1 254 | ) { 255 | maybeCloseItem(); 256 | } 257 | }); 258 | 259 | 260 | 261 | /** 262 | * Listen for ancestor opening requests 263 | * 264 | * The `openAncestorAccordionItem` event is fired whenever a nested 265 | * accordion item is opened, but the ancestors may also need to be 266 | * opened. 267 | */ 268 | $(document).on('openAncestorAccordionItem', function(event, uuid) { 269 | if (uuid === item.uuid) { 270 | openItem(); 271 | } 272 | }); 273 | 274 | 275 | 276 | item.controller.on('keydown', function(event) { 277 | var code = event.which; 278 | 279 | if (item.controller.prop('tagName') !== 'BUTTON') { 280 | // 13 = Return, 32 = Space 281 | if ((code === 13) || (code === 32)) { 282 | // Simulate click on the controller 283 | $(this).click(); 284 | } 285 | } 286 | 287 | // 27 = Esc 288 | if (code === 27) { 289 | maybeCloseItem(); 290 | } 291 | }); 292 | 293 | // Listen for hash changes (in page jump links for accordions) 294 | $(window).on('hashchange', function() { 295 | hashID = window.location.hash.replace('#', ''); 296 | 297 | // Only open this item if the has matches the ID 298 | if (hashID === item.id) { 299 | var ele = $('#' + hashID); 300 | 301 | // If there is a hash and the hash is on an accordion item 302 | if (ele.length && ele.hasClass('js-accordion-item')) { 303 | // Open clicked item 304 | openItem(); 305 | 306 | // Open ancestors if necessary 307 | $.each(item.accordionAncestorItems, function(index, uuid) { 308 | $(document).trigger('openAncestorAccordionItem', uuid); 309 | }); 310 | } 311 | } 312 | }); 313 | 314 | return this; 315 | }; 316 | 317 | 318 | 319 | // Loop through accordion settings objects 320 | // Wait for the entire page to load before loading the accordion 321 | $(window).on('load', function() { 322 | $('.js-accordion-item').each(function() { 323 | $(this).accordionBlockItem({ 324 | // Set default settings 325 | initiallyOpen: $(this).data('initially-open'), 326 | autoClose: $(this).data('auto-close'), 327 | clickToClose: $(this).data('click-to-close'), 328 | scroll: $(this).data('scroll'), 329 | scrollOffset: $(this).data('scroll-offset'), 330 | }); 331 | }); 332 | }); 333 | }(jQuery)); 334 | -------------------------------------------------------------------------------- /src/block/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | import { Fragment, useEffect, useState } from '@wordpress/element'; 6 | import { useInstanceId } from '@wordpress/compose'; 7 | import { useSelect, dispatch } from '@wordpress/data'; 8 | import { 9 | BlockControls, 10 | InspectorControls, 11 | RichText, 12 | useBlockProps, 13 | useInnerBlocksProps, 14 | } from '@wordpress/block-editor'; 15 | import { 16 | PanelBody, 17 | Toolbar, 18 | ToolbarGroup, 19 | ToolbarButton, 20 | Dropdown, 21 | ToggleControl, 22 | RangeControl, 23 | Button, 24 | } from '@wordpress/components'; 25 | import { useEntityProp } from '@wordpress/core-data'; 26 | 27 | /** 28 | * Internal dependencies 29 | */ 30 | import HtmlTagIcon from './html-tag-icon'; 31 | import classnames from '../utils/classnames'; 32 | 33 | const AccordionItemEdit = ({ 34 | className, 35 | attributes, 36 | setAttributes, 37 | clientId, 38 | isSelected, 39 | }) => { 40 | const { 41 | title, 42 | initiallyOpen, 43 | clickToClose, 44 | autoClose, 45 | titleTag, 46 | scroll, 47 | scrollOffset, 48 | uuid, 49 | } = attributes; 50 | 51 | // An accordion item is considered new if it doesn't have a UUID yet 52 | const [isNew, setIsNew] = useState(!uuid); 53 | 54 | const isParentOfSelectedBlock = useSelect((select) => { 55 | return select('core/block-editor').hasSelectedInnerBlock(clientId, true); 56 | }); 57 | 58 | const isAccordionItemSelected = useSelect((select) => { 59 | const block = select('core/block-editor').getSelectedBlock(); 60 | 61 | return block ? block.name === 'pb/accordion-item' : false; 62 | }); 63 | 64 | /** 65 | * UUID is generated as a combination of the post ID and this block's 66 | * instance ID. 67 | * 68 | * This ensures the UUID is unique not just in this post, but across all 69 | * posts. This is necessary since accordions from multiple posts may be 70 | * displayed on the same page (e.g. an archive page that shows the full post 71 | * content). See issue #31. 72 | * 73 | * We use instanceId so a new UUID is generated even if the accordion item 74 | * is duplicated. See issue #47. 75 | * 76 | * TODO: The one downside to this approach is that sometimes accordion 77 | * items' UUIDs change when the editor is reloaded. For example, if the user 78 | * removes a block and saves the page, when the editor loads again, it will 79 | * assign new instanceIds to each block. 80 | */ 81 | const instanceId = useInstanceId(AccordionItemEdit); 82 | const entityId = useSelect((select) => { 83 | return select('core/editor') !== null 84 | ? select('core/editor').getCurrentPostId() : 0; 85 | }); 86 | 87 | useEffect(() => { 88 | const id = Number(`${ entityId }${ instanceId }`); 89 | 90 | if (id !== uuid) { 91 | setAttributes({uuid: id}); 92 | } 93 | }, [instanceId]); 94 | 95 | const isNestedAccordion = useSelect((select) => { 96 | const parentBlocks = select('core/block-editor').getBlockParentsByBlockName(clientId, 'pb/accordion-item'); 97 | 98 | return !!parentBlocks.length; 99 | }); 100 | 101 | const userCanSetDefaults = useSelect((select) => { 102 | /** 103 | * Only Administrators and Editors may set new default settings. Only 104 | * editors and above can create pages, so we use that as a proxy for 105 | * editor role and above. 106 | */ 107 | return select('core').canUser('create', 'pages'); 108 | }); 109 | 110 | const defaults = useSelect((select) => { 111 | return select('accordion-blocks').getDefaultSettings(); 112 | }); 113 | 114 | const settingsAreDefaults = 115 | !(defaults === undefined || defaults === null) && 116 | initiallyOpen === defaults.initiallyOpen && 117 | clickToClose === defaults.clickToClose && 118 | autoClose === defaults.autoClose && 119 | scroll === defaults.scroll && 120 | scrollOffset === defaults.scrollOffset; 121 | 122 | useEffect(() => { 123 | /** 124 | * We only set this accordion item's attributes to defaults if it is new 125 | * and its attributes aren't already the defaults. 126 | */ 127 | if (isNew && !settingsAreDefaults) { 128 | setAttributes({ 129 | initiallyOpen: defaults.initiallyOpen, 130 | clickToClose: defaults.clickToClose, 131 | autoClose: defaults.autoClose, 132 | scroll: defaults.scroll, 133 | scrollOffset: defaults.scrollOffset, 134 | }); 135 | } 136 | }, [defaults]); 137 | 138 | const blockProps = useBlockProps({ 139 | className: classnames( 140 | 'c-accordion__item', 141 | 'js-accordion-item', 142 | ) 143 | }); 144 | 145 | const innerBlocksProps = useInnerBlocksProps({ 146 | className: 'c-accordion__content', 147 | }); 148 | 149 | return ( 150 | 151 | 152 | } 154 | label={ __('Change accordion title tag', 'pb') } 155 | controls={ [ 156 | { 157 | tag: 'h1', 158 | label: __('Heading 1', 'accordion-blocks'), 159 | }, 160 | { 161 | tag: 'h2', 162 | label: __('Heading 2', 'accordion-blocks'), 163 | }, 164 | { 165 | tag: 'h3', 166 | label: __('Heading 3', 'accordion-blocks'), 167 | }, 168 | { 169 | tag: 'h4', 170 | label: __('Heading 4', 'accordion-blocks'), 171 | }, 172 | { 173 | tag: 'h5', 174 | label: __('Heading 5', 'accordion-blocks'), 175 | }, 176 | { 177 | tag: 'h6', 178 | label: __('Heading 6', 'accordion-blocks'), 179 | }, 180 | { 181 | tag: 'button', 182 | label: __('Button', 'accordion-blocks'), 183 | }, 184 | ].map((control) => { 185 | return { 186 | name: control.tag, 187 | icon: , 188 | title: control.label, 189 | isActive: titleTag === control.tag, 190 | onClick: () => setAttributes({'titleTag': control.tag}), 191 | } 192 | }) } 193 | isCollapsed={ true } 194 | /> 195 | 196 | 197 | { isNestedAccordion && ( 198 |
205 | { __('This accordion item is nested inside another accordion item. While this will work, it may not be what you intended.', 'accordion-blocks') } 206 |
207 | ) } 208 | 209 | setAttributes({initiallyOpen: value}) } 217 | /> 218 | setAttributes({clickToClose: value}) } 226 | /> 227 | setAttributes({autoClose: value}) } 235 | /> 236 | setAttributes({ 244 | scroll: value, 245 | scrollOffset: 0, 246 | }) } 247 | /> 248 | { !!scroll && ( 249 | setAttributes({scrollOffset: parseInt(value, 10) ? parseInt(value, 10) : 0}) } 253 | min={ 0 } 254 | max={ 1000 } 255 | help={ __('A pixel offset for the final scroll position.', 'accordion-blocks') } 256 | /> 257 | ) } 258 | { !settingsAreDefaults && ( 259 | 260 |
261 | { userCanSetDefaults && ( 262 | 263 | 277 |

281 | { __('Default settings only apply when creating new accordion items.', 'accordion-blocks') } 282 |

283 |
284 | ) } 285 |

286 | 301 |

302 |
303 | ) } 304 |
305 |
306 |
307 | setAttributes({title: value}) } 319 | /> 320 |
321 |
322 | 323 | ); 324 | }; 325 | 326 | export default AccordionItemEdit; 327 | -------------------------------------------------------------------------------- /accordion-blocks.php: -------------------------------------------------------------------------------- 1 | plugin_version = $this->get_plugin_version(); 37 | 38 | // Register block 39 | add_action('init', array($this, 'register_block')); 40 | 41 | // Enqueue frontend assets 42 | add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); 43 | 44 | // Tell WordPress which JavaScript files contain translations 45 | add_action('init', array($this, 'set_script_translations')); 46 | 47 | if (is_admin()) { 48 | // Add link to documentation on plugin page 49 | add_filter("plugin_action_links_$basename", array($this, 'add_documentation_link')); 50 | } 51 | 52 | // Register defaults site setting 53 | add_action('rest_api_init', array($this, 'register_settings')); 54 | add_action('admin_init', array($this, 'register_settings')); 55 | 56 | // Add API endpoint to get and set settings 57 | add_action('rest_api_init', array($this, 'register_rest_routes')); 58 | 59 | // Add settings page 60 | add_action('admin_menu', array($this, 'add_settings_menu')); 61 | add_action('admin_init', array($this, 'settings_api_init')); 62 | 63 | add_action('render_block_pb/accordion-item', array($this, 'enqueue_assets_on_render')); 64 | } 65 | 66 | 67 | 68 | /** 69 | * Current plugin version number 70 | */ 71 | private function get_plugin_version() { 72 | $plugin_data = get_file_data(__FILE__, array('Version' => 'Version'), false); 73 | 74 | return (defined('WP_DEBUG') && WP_DEBUG) ? time() : $plugin_data['Version']; 75 | } 76 | 77 | 78 | 79 | /** 80 | * Register the block's assets for the editor 81 | */ 82 | public function register_block() { 83 | register_block_type(__DIR__); 84 | } 85 | 86 | 87 | 88 | /** 89 | * Enqueue the block's assets for the frontend 90 | */ 91 | public function enqueue_frontend_assets() { 92 | $min = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min'; 93 | 94 | wp_register_script( 95 | 'pb-accordion-blocks-frontend-script', 96 | plugins_url("js/accordion-blocks$min.js", __FILE__), 97 | array('jquery'), 98 | $this->plugin_version, 99 | true 100 | ); 101 | 102 | wp_register_style( 103 | 'pb-accordion-blocks-style', 104 | plugins_url('build/index.css', __FILE__), 105 | array(), 106 | $this->plugin_version 107 | ); 108 | 109 | $load_scripts_globally = $this->should_load_scripts_globally(); 110 | 111 | if ($load_scripts_globally || has_block('pb/accordion-item', get_the_ID())) { 112 | wp_enqueue_script('pb-accordion-blocks-frontend-script'); 113 | wp_enqueue_style('pb-accordion-blocks-style'); 114 | } 115 | } 116 | 117 | 118 | /** 119 | * Always enqueue the registered assets when the block is rendered 120 | * fixes has_block not working in reusable blocks 121 | */ 122 | public function enqueue_assets_on_render($output) { 123 | wp_enqueue_script('pb-accordion-blocks-frontend-script'); 124 | wp_enqueue_style('pb-accordion-blocks-style'); 125 | return $output; 126 | } 127 | 128 | 129 | /** 130 | * Tell WordPress which JavaScript files contain translations 131 | */ 132 | function set_script_translations() { 133 | wp_set_script_translations('pb-accordion-blocks-editor-script', 'accordion-blocks'); 134 | } 135 | 136 | 137 | 138 | /** 139 | * Register accordion defaults site setting 140 | */ 141 | public function register_settings() { 142 | register_setting( 143 | 'general', 144 | 'accordion_blocks_defaults', 145 | array( 146 | 'type' => 'object', 147 | 'show_in_rest' => array( 148 | 'schema' => array( 149 | 'type' => 'object', 150 | 'properties' => array( 151 | 'initiallyOpen' => array( 152 | 'type' => 'boolean', 153 | ), 154 | 'clickToClose' => array( 155 | 'type' => 'boolean', 156 | ), 157 | 'autoClose' => array( 158 | 'type' => 'boolean', 159 | ), 160 | 'scroll' => array( 161 | 'type' => 'boolean', 162 | ), 163 | 'scrollOffset' => array( 164 | 'type' => 'integer', 165 | ), 166 | ), 167 | ), 168 | ), 169 | 'default' => array( 170 | 'initiallyOpen' => false, 171 | 'clickToClose' => true, 172 | 'autoClose' => true, 173 | 'scroll' => false, 174 | 'scrollOffset' => 0, 175 | ), 176 | ) 177 | ); 178 | 179 | register_setting( 180 | 'accordion_blocks_settings', 181 | 'accordion_blocks_load_scripts_globally', 182 | array( 183 | 'type' => 'boolean', 184 | 'default' => 'on', 185 | ) 186 | ); 187 | } 188 | 189 | 190 | 191 | /** 192 | * Register rest endpoint to get and set plugin defaults 193 | */ 194 | public function register_rest_routes() { 195 | register_rest_route('accordion-blocks/v1', '/defaults', array( 196 | 'methods' => WP_REST_Server::READABLE, 197 | 'callback' => array($this, 'api_get_defaults'), 198 | 'permission_callback' => function() { 199 | return current_user_can('edit_posts'); 200 | } 201 | )); 202 | 203 | register_rest_route('accordion-blocks/v1', '/defaults', array( 204 | 'methods' => WP_REST_Server::EDITABLE, 205 | 'callback' => array($this, 'api_set_defaults'), 206 | 'permission_callback' => function() { 207 | return current_user_can('publish_pages'); 208 | } 209 | )); 210 | } 211 | 212 | 213 | 214 | /** 215 | * Get accordion block default settings 216 | * 217 | * @return object Default accordion block settings object 218 | */ 219 | public function api_get_defaults(WP_REST_Request $request) { 220 | $response = new WP_REST_Response(get_option('accordion_blocks_defaults')); 221 | $response->set_status(200); 222 | 223 | return $response; 224 | } 225 | 226 | 227 | 228 | /** 229 | * Set accordion block default settings 230 | * 231 | * @param data object The date passed from the API 232 | * @return object Default accordion block settings object 233 | */ 234 | public function api_set_defaults($request) { 235 | $old_defaults = get_option('accordion_blocks_defaults'); 236 | 237 | $new_defaults = json_decode($request->get_body()); 238 | 239 | $new_defaults = (object) array( 240 | 'initiallyOpen' => isset($new_defaults->initiallyOpen) ? $new_defaults->initiallyOpen : $old_defaults->initiallyOpen, 241 | 'clickToClose' => isset($new_defaults->clickToClose) ? $new_defaults->clickToClose : $old_defaults->clickToClose, 242 | 'autoClose' => isset($new_defaults->autoClose) ? $new_defaults->autoClose : $old_defaults->autoClose, 243 | 'scroll' => isset($new_defaults->scroll) ? $new_defaults->scroll : $old_defaults->scroll, 244 | 'scrollOffset' => isset($new_defaults->scrollOffset) ? $new_defaults->scrollOffset : $old_defaults->scrollOffset, 245 | ); 246 | 247 | $updated = update_option('accordion_blocks_defaults', $new_defaults); 248 | 249 | $response = new WP_REST_Response($new_defaults); 250 | $response->set_status($updated ? 201 : 500); 251 | 252 | return $response; 253 | } 254 | 255 | 256 | 257 | /** 258 | * Add documentation link on plugin page 259 | */ 260 | public function add_documentation_link($links) { 261 | array_push($links, sprintf('%s', 262 | 'http://wordpress.org/plugins/accordion-blocks/', 263 | _x('Documentation', 'link to documentation on wordpress.org site', 'accordion-blocks') 264 | )); 265 | 266 | array_push($links, sprintf('%s', 267 | 'https://philbuchanan.com/donate/', 268 | __('Donate', 'accordion-blocks') 269 | )); 270 | 271 | return $links; 272 | } 273 | 274 | 275 | 276 | /** 277 | * Get the load_scripts_globally option and return true or false. 278 | */ 279 | private function should_load_scripts_globally() { 280 | /** 281 | * This removes the old option (the option name had a typo), but ensures 282 | * the new option gets updated with the same setting. 283 | */ 284 | if (get_option('accordion_blocks_load_scripts_globablly') == 'on') { 285 | update_option('accordion_blocks_load_scripts_globally', 'on'); 286 | } 287 | 288 | delete_option('accordion_blocks_load_scripts_globablly'); 289 | 290 | $load_scripts_globally = get_option('accordion_blocks_load_scripts_globally', 'on'); 291 | 292 | return !!$load_scripts_globally; 293 | } 294 | 295 | 296 | 297 | /** 298 | * Add the admin menu settings page 299 | */ 300 | public function add_settings_menu() { 301 | add_options_page( 302 | __('Accordion Blocks Settings', 'accordion-blocks'), 303 | __('Accordion Blocks', 'accordion-blocks'), 304 | 'manage_options', 305 | 'accordion_blocks_settings', 306 | array($this, 'render_settings_page') 307 | ); 308 | } 309 | 310 | 311 | 312 | /** 313 | * Render the settings page 314 | */ 315 | public function render_settings_page() { 316 | if (!current_user_can('manage_options')) { 317 | wp_die(__('You do not have sufficient permissions to access this page.', 'accordion-blocks')); 318 | } ?> 319 | 320 |
321 |

322 |
323 | 328 |
329 |
330 | 'accordion_blocks_load_scripts_globally', 353 | ) 354 | ); 355 | } 356 | 357 | 358 | 359 | /** 360 | * Callback function for Accordion Blocks global settings section 361 | * Add section intro copy here (if necessary) 362 | */ 363 | public function accordion_blocks_global_settings_section_callback() {} 364 | 365 | 366 | 367 | /** 368 | * Callback function for load scripts globally setting 369 | */ 370 | public function load_scripts_globally_setting_callback() { 371 | $load_scripts_globally = $this->should_load_scripts_globally(); ?> 372 |
373 | 374 | 375 | 376 | 386 |
387 |

388 | 389 |

390 |

391 | 392 |

393 |
394 |
395 | 45 | 48 | 51 |
52 | 53 | = Custom CSS = 54 | 55 | You can use the following CSS classes to customize the look of the accordion. 56 | 57 | .c-accordion__item {} /* The accordion item container */ 58 | .c-accordion__item.is-open {} /* is-open is added to open accordion items */ 59 | .c-accordion__item.is-read {} /* is-read is added to accordion items that have been opened at least once */ 60 | .c-accordion__title {} /* An accordion item title */ 61 | .c-accordion__title--button {} /* An accordion item title that is using a `