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 |
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 |
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 |
395 |
45 |
46 | Title with H2 tag
47 |
48 |
49 |
Content
50 |
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 `