From d117ca192d9f417319eb84cb418bcb48841e9034 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 17:28:55 +0200 Subject: [PATCH 01/23] read links from link blocks, fall back to the attribute --- build/blogroll/render.php | 5 +- includes/class-opml.php | 2 +- includes/class-sources.php | 46 ++++++++++++++++-- src/blogroll/render.php | 5 +- tests/test-opml.php | 11 +++++ tests/test-render.php | 95 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 153 insertions(+), 11 deletions(-) diff --git a/build/blogroll/render.php b/build/blogroll/render.php index 06786a5..1cb3e71 100644 --- a/build/blogroll/render.php +++ b/build/blogroll/render.php @@ -7,7 +7,8 @@ * * @package Blockroll * - * @var array $attributes Block attributes. + * @var array $attributes Block attributes. + * @var WP_Block $block The block, with its link blocks as inner blocks. */ use Blockroll\Links; @@ -16,7 +17,7 @@ defined( 'ABSPATH' ) || exit; -$blockroll_links = Sources::links( $attributes ); +$blockroll_links = Sources::links( $attributes, $block->inner_blocks ); $blockroll_sortable = $attributes['showSort']; diff --git a/includes/class-opml.php b/includes/class-opml.php index caf5b68..9af9eb3 100644 --- a/includes/class-opml.php +++ b/includes/class-opml.php @@ -181,7 +181,7 @@ public static function extract_groups( $post ) { $walker = function ( $blocks ) use ( &$walker, &$groups ) { foreach ( $blocks as $block ) { if ( 'blockroll/blogroll' === $block['blockName'] ) { - $links = Sources::links( (array) ( $block['attrs'] ?? array() ) ); + $links = Sources::links( (array) ( $block['attrs'] ?? array() ), $block['innerBlocks'] ?? array() ); if ( $links ) { $groups[] = array( 'name' => \sanitize_text_field( (string) ( $block['attrs']['metadata']['name'] ?? '' ) ), diff --git a/includes/class-sources.php b/includes/class-sources.php index d25cbf6..5e40d9a 100644 --- a/includes/class-sources.php +++ b/includes/class-sources.php @@ -100,16 +100,27 @@ public static function help_url( $source ) { return \is_scalar( $url ) ? \esc_url_raw( (string) $url ) : ''; } + /** + * The block that holds one link of a manual blogroll. + */ + const LINK_BLOCK = 'blockroll/link'; + /** * Resolve and normalize the links for a block's selected source. * - * @param array $attributes Block attributes. + * @param array $attributes Block attributes. + * @param array|\WP_Block_List|null $inner_blocks Inner blocks of the block, as parsed + * arrays or as WP_Block objects. * @return array Normalized links. */ - public static function links( $attributes ) { + public static function links( $attributes, $inner_blocks = array() ) { $source = self::source( $attributes ); if ( self::MANUAL === $source ) { - $links = (array) ( $attributes['links'] ?? array() ); + $links = self::link_blocks( $inner_blocks ); + if ( ! $links ) { + // Blocks saved before links became blocks of their own. + $links = (array) ( $attributes['links'] ?? array() ); + } } else { /** * Provide links for a selected blogroll source. @@ -138,6 +149,35 @@ function ( $link ) { ); } + /** + * Collect the attributes of the link blocks among a block's inner blocks. + * + * Other blocks are ignored. Takes both shapes an inner block comes in: + * the parsed array of parse_blocks() and the WP_Block of a render. + * + * @param array|\WP_Block_List|null $inner_blocks Inner blocks. + * @return array Raw link arrays, in block order. + */ + private static function link_blocks( $inner_blocks ) { + $links = array(); + if ( ! \is_iterable( $inner_blocks ) ) { + return $links; + } + foreach ( $inner_blocks as $block ) { + if ( $block instanceof \WP_Block ) { + $name = $block->name; + $attrs = $block->attributes; + } else { + $name = $block['blockName'] ?? ''; + $attrs = $block['attrs'] ?? array(); + } + if ( self::LINK_BLOCK === $name ) { + $links[] = (array) $attrs; + } + } + return $links; + } + /** * Get a block's selected source. * diff --git a/src/blogroll/render.php b/src/blogroll/render.php index 06786a5..1cb3e71 100644 --- a/src/blogroll/render.php +++ b/src/blogroll/render.php @@ -7,7 +7,8 @@ * * @package Blockroll * - * @var array $attributes Block attributes. + * @var array $attributes Block attributes. + * @var WP_Block $block The block, with its link blocks as inner blocks. */ use Blockroll\Links; @@ -16,7 +17,7 @@ defined( 'ABSPATH' ) || exit; -$blockroll_links = Sources::links( $attributes ); +$blockroll_links = Sources::links( $attributes, $block->inner_blocks ); $blockroll_sortable = $attributes['showSort']; diff --git a/tests/test-opml.php b/tests/test-opml.php index 180ddfd..7b7140d 100644 --- a/tests/test-opml.php +++ b/tests/test-opml.php @@ -18,6 +18,17 @@ public function test_extract_links() { $this->assertSame( 'https://a.example/', $links[0]['url'] ); } + public function test_extract_links_from_link_blocks() { + $post = self::factory()->post->create_and_get( + array( 'post_content' => '' ) + ); + $links = Blockroll\Opml::extract_links( $post ); + + $this->assertCount( 2, $links ); + $this->assertSame( 'https://a.example/', $links[0]['url'] ); + $this->assertSame( array( 'friend' ), $links[1]['xfn'] ); + } + public function test_extract_links_uses_registered_source_links() { add_filter( 'blockroll_sources', diff --git a/tests/test-render.php b/tests/test-render.php index fc74910..5125ef9 100644 --- a/tests/test-render.php +++ b/tests/test-render.php @@ -12,18 +12,107 @@ class Test_Render extends WP_UnitTestCase { /** * Render the block with the given attributes. * - * @param array $attrs Block attributes. + * @param array $attrs Block attributes. + * @param array $inner_blocks Parsed inner blocks. * @return string Rendered HTML. */ - private function render_block_html( $attrs ) { + private function render_block_html( $attrs, $inner_blocks = array() ) { $block = array( 'blockName' => 'blockroll/blogroll', 'attrs' => $attrs, + 'innerBlocks' => $inner_blocks, + 'innerHTML' => '', + 'innerContent' => array_fill( 0, count( $inner_blocks ), null ), + ); + return render_block( $block ); + } + + /** + * A parsed link block, the way a child of the blogroll is stored. + * + * @param array $attrs Link attributes. + * @return array Parsed block. + */ + private function link_block( $attrs ) { + return array( + 'blockName' => 'blockroll/link', + 'attrs' => $attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), ); - return render_block( $block ); + } + + public function test_renders_link_blocks() { + $html = $this->render_block_html( + array( 'sortBy' => 'manual' ), + array( + $this->link_block( + array( + 'url' => 'https://b.example/', + 'name' => 'B', + 'description' => 'Second', + 'xfn' => array( 'friend' ), + ) + ), + $this->link_block( array( 'url' => 'https://a.example/' ) ), + ) + ); + + $this->assertSame( 2, substr_count( $html, 'class="h-card"' ) ); + $this->assertStringContainsString( 'rel="friend noopener"', $html ); + $this->assertStringContainsString( '

Second

', $html ); + // Manual order is the order of the blocks. + $this->assertLessThan( strpos( $html, 'a.example' ), strpos( $html, 'b.example' ) ); + } + + public function test_link_blocks_win_over_links_attribute() { + $html = $this->render_block_html( + array( + 'links' => array( + array( + 'url' => 'https://old.example/', + 'name' => 'Old', + ), + ), + ), + array( $this->link_block( array( 'url' => 'https://new.example/' ) ) ) + ); + + $this->assertStringContainsString( 'new.example', $html ); + $this->assertStringNotContainsString( 'old.example', $html ); + } + + public function test_links_attribute_still_renders_without_link_blocks() { + $html = $this->render_block_html( + array( + 'links' => array( + array( + 'url' => 'https://old.example/', + 'name' => 'Old', + ), + ), + ) + ); + + $this->assertStringContainsString( 'old.example', $html ); + } + + public function test_other_inner_blocks_are_ignored() { + $paragraph = array( + 'blockName' => 'core/paragraph', + 'attrs' => array( 'url' => 'https://not-a-link.example/' ), + 'innerBlocks' => array(), + 'innerHTML' => '

Hi

', + 'innerContent' => array( '

Hi

' ), + ); + $html = $this->render_block_html( + array(), + array( $paragraph, $this->link_block( array( 'url' => 'https://a.example/' ) ) ) + ); + + $this->assertSame( 1, substr_count( $html, 'class="h-card"' ) ); + $this->assertStringNotContainsString( 'not-a-link', $html ); } public function test_renders_h_card_with_xfn() { From ba8c8a7e58bde6e18ecbff16aa7eb3878102a3be Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:16:51 +0200 Subject: [PATCH 02/23] links as blocks in the editor --- blockroll.php | 1 + build/blogroll/block.json | 4 +- build/blogroll/index-rtl.css | 2 +- build/blogroll/index.asset.php | 2 +- build/blogroll/index.css | 2 +- build/blogroll/index.js | 12 +- build/link/block.json | 52 ++++ build/link/index-rtl.css | 1 + build/link/index.asset.php | 1 + build/link/index.css | 1 + build/link/index.js | 1 + src/blogroll/__tests__/to-url.test.js | 21 ++ src/blogroll/block.json | 2 +- src/blogroll/components/add-link.js | 109 ++++++++ src/blogroll/edit.js | 129 ++++----- src/blogroll/editor.scss | 20 ++ src/blogroll/index.js | 9 +- src/blogroll/utils.js | 11 + src/icons.js | 9 + src/link/block.json | 23 ++ src/link/edit.js | 364 ++++++++++++++++++++++++++ src/link/editor.scss | 58 ++++ src/link/index.js | 9 + 23 files changed, 753 insertions(+), 90 deletions(-) create mode 100644 build/link/block.json create mode 100644 build/link/index-rtl.css create mode 100644 build/link/index.asset.php create mode 100644 build/link/index.css create mode 100644 build/link/index.js create mode 100644 src/blogroll/__tests__/to-url.test.js create mode 100644 src/blogroll/components/add-link.js create mode 100644 src/icons.js create mode 100644 src/link/block.json create mode 100644 src/link/edit.js create mode 100644 src/link/editor.scss create mode 100644 src/link/index.js diff --git a/blockroll.php b/blockroll.php index d3ed139..c913fae 100644 --- a/blockroll.php +++ b/blockroll.php @@ -47,6 +47,7 @@ function init() { Opml::register(); Xfn::register(); \register_block_type( __DIR__ . '/build/blogroll' ); + \register_block_type( __DIR__ . '/build/link' ); } \add_action( 'init', __NAMESPACE__ . '\init' ); diff --git a/build/blogroll/block.json b/build/blogroll/block.json index db82671..59d42bb 100644 --- a/build/blogroll/block.json +++ b/build/blogroll/block.json @@ -4,7 +4,6 @@ "name": "blockroll/blogroll", "title": "Blogroll", "category": "widgets", - "icon": "admin-links", "description": "Share a list of the blogs and sites you follow.", "textdomain": "blockroll", "keywords": [ @@ -70,6 +69,9 @@ "default": true } }, + "providesContext": { + "blockroll/showAvatars": "showAvatars" + }, "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", diff --git a/build/blogroll/index-rtl.css b/build/blogroll/index-rtl.css index 2ace2ae..e602659 100644 --- a/build/blogroll/index-rtl.css +++ b/build/blogroll/index-rtl.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 37c9d47..8209dc7 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'c3dd04e9224b2aa7b893'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'f360bc8f81ff92b6a2c1'); diff --git a/build/blogroll/index.css b/build/blogroll/index.css index 2ace2ae..e602659 100644 --- a/build/blogroll/index.css +++ b/build/blogroll/index.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/build/blogroll/index.js b/build/blogroll/index.js index f39538d..8777db5 100644 --- a/build/blogroll/index.js +++ b/build/blogroll/index.js @@ -1,6 +1,6 @@ -(()=>{"use strict";var e={408(e,l,o){const t=window.wp.blocks,n=JSON.parse('{"UU":"blockroll/blogroll"}'),r=window.wp.i18n,s=window.wp.element,a=window.wp.data,i=window.wp.blockEditor,c=window.wp.components,d=window.wp.primitives,u=window.ReactJSXRuntime;var h=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"M12 3.9 6.5 9.5l1 1 3.8-3.7V20h1.5V6.8l3.7 3.7 1-1z"})}),_=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"m16.5 13.5-3.7 3.7V4h-1.5v13.2l-3.8-3.7-1 1 5.5 5.6 5.5-5.6z"})}),p=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"m19 7-3-3-8.5 8.5-1 4 4-1L19 7Zm-7 11.5H5V20h7v-1.5Z"})}),m=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"})});const b=window.wp.apiFetch;var x=o.n(b);const k=window.wp.notices,f=window.wp.escapeHtml,g={friendship:["friend","acquaintance","contact"],physical:["met"],professional:["co-worker","colleague"],geographical:["co-resident","neighbor"],family:["child","parent","sibling","spouse","kin"],romantic:["muse","crush","date","sweetheart"],identity:["me"]},v=["friendship","geographical","family"],w=Object.values(g).flat();function j({value:e=[],onChange:l}){return(0,u.jsx)(c.FormTokenField,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,__experimentalExpandOnFocus:!0,label:(0,r.__)("Relationship","blockroll"),value:e,suggestions:w,onChange:e=>l(function(e){return e.filter(e=>w.includes(e)).reduce((e,l)=>function(e,l){if("me"===l)return["me"];let o=e.filter(e=>"me"!==e);for(const e of v)g[e].includes(l)&&(o=o.filter(l=>!g[e].includes(l)));return[...o,l]}(e,l),[])}(e))})}function C(e,l,o){const t=[...e];return t.splice(o,0,...t.splice(l,1)),t}function S(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}const B={url:"",name:"",description:"",feedUrl:"",photo:"",xfn:[],added:""};function y({link:e,onSave:l,onCancel:o}){const[t,n]=(0,s.useState)({...B,...e}),[a,i]=(0,s.useState)(!1),[d,h]=(0,s.useState)(null),_=e=>l=>n(o=>({...o,[e]:l}));return(0,u.jsx)(c.Modal,{title:e?(0,r.__)("Edit link","blockroll"):(0,r.__)("Add link","blockroll"),size:"medium",onRequestClose:o,focusOnMount:"firstContentElement",children:(0,u.jsxs)("div",{className:"blockroll-form",children:[d&&(0,u.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,u.jsxs)("div",{className:"blockroll-form__row",children:[(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Address","blockroll"),placeholder:"https://example.com/",type:"url",value:t.url,onChange:_("url")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"secondary",isBusy:a,disabled:!t.url||a,onClick:()=>{i(!0),h(null),x()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}}).then(e=>n(l=>S(l,e))).catch(e=>h(e.message||(0,r.__)("The site could not be reached.","blockroll"))).finally(()=>i(!1))},children:(0,r.__)("Fetch details","blockroll")})]}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Name","blockroll"),value:t.name,onChange:_("name")}),(0,u.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Description","blockroll"),rows:2,value:t.description,onChange:_("description")}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Feed address","blockroll"),type:"url",value:t.feedUrl,onChange:_("feedUrl")}),(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Image address","blockroll"),type:"url",value:t.photo,onChange:_("photo")}),(0,u.jsx)(j,{value:t.xfn,onChange:_("xfn")}),(0,u.jsxs)("div",{className:"blockroll-form__footer",children:[(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:o,children:(0,r.__)("Cancel","blockroll")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",disabled:!t.url,onClick:()=>l({...t,added:t.added||(new Date).toISOString().slice(0,10)}),children:(0,r.__)("Save","blockroll")})]})]})})}var N=(0,u.jsx)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,u.jsx)(d.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function M({onImport:e,onClose:l}){const[o,t]=(0,s.useState)(""),[n,a]=(0,s.useState)(""),[i,d]=(0,s.useState)(""),[h,_]=(0,s.useState)(!1),[p,m]=(0,s.useState)(!1),[b,k]=(0,s.useState)(null),[f,g]=(0,s.useState)(null),v=e=>{e&&(e.text().then(t),a(e.name))},w={file:(0,u.jsxs)("div",{className:"blockroll-import-upload",children:[(0,u.jsx)(c.DropZone,{onFilesDrop:e=>v(e[0])}),(0,u.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:N,variant:"secondary",onChange:e=>v(e.target.files?.[0]),children:n||(0,r.__)("Choose or drop a file","blockroll")})]}),paste:(0,u.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),a("")}}),url:(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,u.jsx)(c.Modal,{title:(0,r.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,u.jsxs)("div",{className:"blockroll-form",children:[f&&(0,u.jsx)(c.Notice,{status:"error",isDismissible:!1,children:f}),(0,u.jsx)("p",{className:"blockroll-import-intro",children:(0,r.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,u.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,r.__)("Upload","blockroll")},{name:"paste",title:(0,r.__)("Paste","blockroll")},{name:"url",title:(0,r.__)("Address","blockroll")}],children:e=>(0,u.jsx)("div",{className:"blockroll-import-tab",children:w[e.name]})}),(0,u.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Fetch details for imported links","blockroll"),help:(0,r.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:_}),(0,u.jsxs)("div",{className:"blockroll-form__footer",children:[null!==b&&(0,u.jsx)("span",{children:(0,r.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ -(0,r.__)("%d links processed…","blockroll"),b)}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,r.__)("Cancel","blockroll")}),(0,u.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:p,disabled:!o&&!i||p,onClick:async()=>{m(!0),g(null);try{const t=o?{opml:o}:{url:i},{links:n}=await x()({path:"/blockroll/v1/import",method:"POST",data:t}),r=h?await async function(e,l){const o=[];for(const t of e){try{const e=await x()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(S(t,e))}catch{o.push(t)}l(o.length)}return o}(n,k):n;e(r),l()}catch(e){g(e.message||(0,r.__)("The file could not be imported.","blockroll")),m(!1),k(null)}},children:(0,r.__)("Import","blockroll")})]})]})})}const O=window.wp.url;function D(e){return(0,O.cleanForSlug)(e)||"blogroll"}function T(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}(0,t.registerBlockType)(n.UU,{edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:t}){const{anchor:n,links:d,source:b,sortBy:g,perPage:v,showAvatars:w,showSort:j,showOpml:S,metadata:B}=e,N=B?.name||"",O=(0,a.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:H}=(0,a.useDispatch)(i.store),z=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=O.select(i.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,s.useEffect)(()=>{if(!n)return H(),void l({anchor:T(D(N),z())});if(t)return;const e=z();e.includes(n)&&(H(),l({anchor:T(n,e)}))},[n,t]);const A=(0,s.useRef)(N);(0,s.useEffect)(()=>{if(A.current===N)return;const e=!n||function(e,l){const o=D(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(n,A.current);A.current=N,e&&l({anchor:T(D(N),z())})},[N]);const P=(0,a.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:r,getSelectedBlockClientId:s}=e(i.store);if(!n||t)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&r(e)?.anchor===n)},[o,n,t]),{createWarningNotice:U,removeNotice:L}=(0,a.useDispatch)(k.store),F=(0,s.useRef)(null);(0,s.useEffect)(()=>{F.current&&(L(F.current),F.current=null),P&&(F.current=`blockroll-anchor-${n}`,U((0,r.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ -(0,r.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,f.escapeHTML)(n)),{id:F.current,isDismissible:!0,__unstableHTML:!0}))},[P,n]),(0,s.useEffect)(()=>()=>{F.current&&L(F.current)},[]);const[I,E]=(0,s.useState)(null),[R,V]=(0,s.useState)(!1),[G,Z]=(0,s.useState)([{label:(0,r.__)("Manual links","blockroll"),value:"manual"}]),[$,W]=(0,s.useState)([]),[q,J]=(0,s.useState)(!1),[X,K]=(0,s.useState)(null),[Q,Y]=(0,s.useState)(!1),ee="manual",le=G.some(e=>b===e.value),oe=!b||Q&&!le?ee:b,te=JSON.stringify(e),ne=G.filter(e=>ee!==e.value),re=G.find(e=>oe===e.value)||G[0];(0,s.useEffect)(()=>{x()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(Z(e),Y(!0))}).catch(()=>{})},[]),(0,s.useEffect)(()=>{let e=!0;return ee===oe?(W([]),K(null),J(!1),()=>{e=!1}):(J(!0),K(null),x()({path:`/blockroll/v1/sources/${oe}/links?attributes=`+encodeURIComponent(te)}).then(l=>{e&&W(Array.isArray(l)?l:[])}).catch(l=>{e&&(W([]),K(l.message||(0,r.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&J(!1)}),()=>{e=!1})},[oe,te]);const se=(0,u.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,u.jsx)(c.Button,{variant:"primary",onClick:()=>E("new"),children:(0,r.__)("Add link","blockroll")}),(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>V(!0),children:(0,r.__)("Import links","blockroll")}),!d.length&&ee===oe&&ne.map(e=>(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,r.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ -(0,r.__)("Use %s","blockroll"),e.label)},e.value))]});let ae=null;ee!==oe||d.length||(ae=(0,u.jsx)(c.Placeholder,{icon:"admin-links",label:(0,r.__)("Blogroll","blockroll"),instructions:(0,r.__)("Share a list of the blogs and sites you follow.","blockroll"),children:se}));const ie=(0,u.jsx)("div",{className:"blockroll-editor-actions",children:(0,u.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:ee}),children:(0,r.__)("Use manual links","blockroll")})}),ce=(e,o=!1)=>(0,u.jsx)("ul",{className:"blockroll-editor-list",children:e.map((e,t)=>(0,u.jsxs)("li",{children:[w&&(e.photo?(0,u.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,u.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,u.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,u.jsx)("strong",{children:e.name||e.url}),(0,u.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]}),!o&&(0,u.jsxs)("span",{className:"blockroll-editor-list__actions",children:[(0,u.jsx)(c.Button,{size:"compact",icon:h,label:(0,r.__)("Move up","blockroll"),disabled:0===t,onClick:()=>l({links:C(d,t,t-1)})}),(0,u.jsx)(c.Button,{size:"compact",icon:_,label:(0,r.__)("Move down","blockroll"),disabled:t===d.length-1,onClick:()=>l({links:C(d,t,t+1)})}),(0,u.jsx)(c.Button,{size:"compact",icon:p,label:(0,r.__)("Edit","blockroll"),onClick:()=>E(t)}),(0,u.jsx)(c.Button,{size:"compact",icon:m,label:(0,r.__)("Remove","blockroll"),isDestructive:!0,onClick:()=>l({links:d.filter((e,l)=>l!==t)})})]})]},e.url))});return(0,u.jsxs)("div",{...(0,i.useBlockProps)(),children:[(0,u.jsx)(i.InspectorControls,{children:(0,u.jsxs)(c.PanelBody,{title:(0,r.__)("Blogroll settings","blockroll"),children:[(0,u.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Name","blockroll"),help:(0,s.createInterpolateElement)((0,r.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ -(0,r.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),n||D(N)),{code:(0,u.jsx)("code",{})}),placeholder:(0,r.__)("Blogroll","blockroll"),value:N,onChange:e=>{const o={...B};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,u.jsx)(c.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Sort by","blockroll"),value:g,options:[{label:(0,r.__)("Name","blockroll"),value:"name"},{label:(0,r.__)("Newest first","blockroll"),value:"added"},{label:(0,r.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,u.jsx)(c.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,r.__)("Links per page","blockroll"),help:(0,r.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:v,onChange:e=>l({perPage:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Show images","blockroll"),checked:w,onChange:e=>l({showAvatars:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Let visitors sort the list","blockroll"),checked:j,onChange:e=>l({showSort:e})}),(0,u.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Show download link","blockroll"),help:(0,r.__)("Links to the list as an OPML file.","blockroll"),checked:S,onChange:e=>l({showOpml:e})})]})}),null!==I&&(0,u.jsx)(y,{link:"new"===I?void 0:d[I],onSave:e=>{const o=[...d];"new"===I?o.push(e):o[I]=e,l({links:o,source:ee}),E(null)},onCancel:()=>E(null)}),R&&(0,u.jsx)(M,{onImport:e=>{const o=new Set(d.map(e=>e.url));l({source:ee,links:[...d,...e.filter(e=>!o.has(e.url))]})},onClose:()=>V(!1)}),ee!==oe?(0,u.jsxs)("div",{className:"blockroll-editor",children:[(0,u.jsx)(c.Placeholder,{icon:"admin-links",label:re.label,instructions:q?(0,r.__)("Loading source preview…","blockroll"):(0,r.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ -(0,r.__)("%d links will be shown when the block is rendered.","blockroll"),$.length),children:ie}),re.help&&(0,u.jsxs)("p",{children:[re.help,re.helpUrl&&(0,u.jsxs)(u.Fragment,{children:[" ",(0,u.jsx)("a",{href:re.helpUrl,children:(0,r.__)("Manage source","blockroll")})]})]}),X&&(0,u.jsx)("p",{children:X}),!X&&!q&&!!$.length&&ce($,!0)]}):ae||(0,u.jsxs)("div",{className:"blockroll-editor",children:[ce(d),se]})]})},save:()=>null})}};const l={};function o(t){const n=l[t];if(void 0!==n)return n.exports;const r=l[t]={exports:{}};return e[t](r,r.exports,o),r.exports}o.m=e,(()=>{const e=[];o.O=(l,t,n,r)=>{if(t){r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];return void(e[s]=[t,n,r])}let a=1/0;for(s=0;s=r)&&Object.keys(o.O).every(e=>o.O[e](t[i]))?t.splice(i--,1):(c=!1,r{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[n,r,s]=t;var a,i,c=0;if(n.some(l=>0!==e[l])){for(a in r)o.o(r,a)&&(o.m[a]=r[a]);if(s)var d=s(o)}for(l&&l(t);co(408));t=o.O(t)})(); \ No newline at end of file +(()=>{"use strict";var e={3(e,l,o){const t=window.wp.blocks,n=window.wp.blockEditor,r=JSON.parse('{"UU":"blockroll/blogroll"}'),s=window.wp.i18n,a=window.wp.element,c=window.wp.data,i=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function b(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}const _=window.ReactJSXRuntime;function m({anchor:e,onAdd:l,onClose:o}){const[t,n]=(0,a.useState)(""),[r,c]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,_.jsx)(i.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,_.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!r&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(c(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(b({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),c(!1)}))})()},children:[d&&(0,_.jsx)(i.Notice,{status:"error",isDismissible:!1,children:d}),(0,_.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",spellCheck:!1,value:t,disabled:r,onChange:e=>{n(e),h(null)}}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:r,disabled:!p||r,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const k=window.wp.primitives;var x=(0,_.jsx)(k.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,_.jsx)(k.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function f({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[n,r]=(0,a.useState)(""),[c,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[m,k]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),r(e.name))},S={file:(0,_.jsxs)("div",{className:"blockroll-import-upload",children:[(0,_.jsx)(i.DropZone,{onFilesDrop:e=>j(e[0])}),(0,_.jsx)(i.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:n||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,_.jsx)(i.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),r("")}}),url:(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:c,onChange:d})};return(0,_.jsx)(i.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,_.jsxs)("div",{className:"blockroll-form",children:[v&&(0,_.jsx)(i.Notice,{status:"error",isDismissible:!1,children:v}),(0,_.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,_.jsx)(i.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,_.jsx)("div",{className:"blockroll-import-tab",children:S[e.name]})}),(0,_.jsx)(i.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,_.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,_.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ +(0,s.__)("%d links processed…","blockroll"),f)}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:m,disabled:!o&&!c||m,onClick:async()=>{k(!0),w(null);try{const t=o?{opml:o}:{url:c},{links:n}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),r=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(b(t,e))}catch{o.push(t)}l(o.length)}return o}(n,g):n;e(r),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),k(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const g=window.wp.url;function v(e){return(0,g.cleanForSlug)(e)||"blogroll"}function w(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const j=(0,_.jsx)(k.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,_.jsx)(k.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(r.UU,{icon:j,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:r}){const{anchor:d,links:b,source:k,sortBy:x,perPage:g,showAvatars:j,showSort:S,showOpml:C,metadata:y}=e,B=y?.name||"",N=(0,c.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:M}=(0,c.useDispatch)(n.store),O=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(n.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return M(),void l({anchor:w(v(B),O())});if(r)return;const e=O();e.includes(d)&&(M(),l({anchor:w(d,e)}))},[d,r]);const A=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(A.current===B)return;const e=!d||function(e,l){const o=v(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,A.current);A.current=B,e&&l({anchor:w(v(B),O())})},[B]);const T=(0,c.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(n.store);if(!d||r)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,r]),{createWarningNotice:D,removeNotice:H}=(0,c.useDispatch)(h.store),P=(0,a.useRef)(null);(0,a.useEffect)(()=>{P.current&&(H(P.current),P.current=null),T&&(P.current=`blockroll-anchor-${d}`,D((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ +(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:P.current,isDismissible:!0,__unstableHTML:!0}))},[T,d]),(0,a.useEffect)(()=>()=>{P.current&&H(P.current)},[]);const[I,L]=(0,a.useState)(!1),[U,z]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[W,G]=(0,a.useState)(!1),J="manual",q=U.some(e=>k===e.value),X=!k||W&&!q?J:k,K=JSON.stringify(e),Q=U.filter(e=>J!==e.value),Y=U.find(e=>X===e.value)||U[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(z(e),G(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return J===X?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${X}/links?attributes=`+encodeURIComponent(K)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[X,K]);const ee=(0,c.useSelect)(e=>e(n.store).getBlockCount(o),[o]),{insertBlock:le}=(0,c.useDispatch)(n.store),[oe,te]=(0,a.useState)(!1),[ne,re]=(0,a.useState)(),se=(0,n.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ae=(0,_.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,_.jsx)(i.Button,{variant:"primary",ref:re,"aria-expanded":oe,onClick:()=>te(!oe),children:(0,s.__)("Add link","blockroll")}),oe&&(0,_.jsx)(m,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),te(!1)},onClose:()=>te(!1)}),(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>L(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&J===X&&Q.map(e=>(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ +(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;J!==X||ee||(ce=(0,_.jsx)(i.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ae}));const ie=(0,_.jsx)("div",{className:"blockroll-editor-actions",children:(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>l({source:J}),children:(0,s.__)("Use manual links","blockroll")})});return(0,_.jsxs)("div",{...(0,n.useBlockProps)(),children:[(0,_.jsx)(n.InspectorControls,{children:(0,_.jsxs)(i.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ +(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),d||v(B)),{code:(0,_.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:B,onChange:e=>{const o={...y};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,_.jsx)(i.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:x,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,_.jsx)(i.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:g,onChange:e=>l({perPage:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:j,onChange:e=>l({showAvatars:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:S,onChange:e=>l({showSort:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:C,onChange:e=>l({showOpml:e})})]})}),I&&(0,_.jsx)(f,{onImport:e=>{const o=new Set(b.map(e=>e.url));l({source:J,links:[...b,...e.filter(e=>!o.has(e.url))]})},onClose:()=>L(!1)}),J!==X?(0,_.jsxs)("div",{className:"blockroll-editor",children:[(0,_.jsx)(i.Placeholder,{icon:"admin-links",label:Y.label,instructions:F?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ +(0,s.__)("%d links will be shown when the block is rendered.","blockroll"),V.length),children:ie}),Y.help&&(0,_.jsxs)("p",{children:[Y.help,Y.helpUrl&&(0,_.jsxs)(_.Fragment,{children:[" ",(0,_.jsx)("a",{href:Y.helpUrl,children:(0,s.__)("Manage source","blockroll")})]})]}),Z&&(0,_.jsx)("p",{children:Z}),!Z&&!F&&!!V.length&&(de=V,(0,_.jsx)("ul",{className:"blockroll-editor-list",children:de.map(e=>(0,_.jsxs)("li",{children:[j&&(e.photo?(0,_.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,_.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,_.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,_.jsx)("strong",{children:e.name||e.url}),(0,_.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]})]},e.url))}))]}):(0,_.jsxs)("div",{className:"blockroll-editor",children:[ce||(0,_.jsx)("div",{...se}),!ce&&ae]})]});var de},save:()=>(0,_.jsx)(n.InnerBlocks.Content,{})})}};const l={};function o(t){const n=l[t];if(void 0!==n)return n.exports;const r=l[t]={exports:{}};return e[t](r,r.exports,o),r.exports}o.m=e,(()=>{const e=[];o.O=(l,t,n,r)=>{if(t){r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];return void(e[s]=[t,n,r])}let a=1/0;for(s=0;s=r)&&Object.keys(o.O).every(e=>o.O[e](t[c]))?t.splice(c--,1):(i=!1,r{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[n,r,s]=t;var a,c,i=0;if(n.some(l=>0!==e[l])){for(a in r)o.o(r,a)&&(o.m[a]=r[a]);if(s)var d=s(o)}for(l&&l(t);io(3));t=o.O(t)})(); \ No newline at end of file diff --git a/build/link/block.json b/build/link/block.json new file mode 100644 index 0000000..c5127b1 --- /dev/null +++ b/build/link/block.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "blockroll/link", + "title": "Link", + "category": "widgets", + "description": "One site in a blogroll.", + "textdomain": "blockroll", + "parent": [ + "blockroll/blogroll" + ], + "supports": { + "html": false, + "inserter": false, + "reusable": false + }, + "usesContext": [ + "blockroll/showAvatars" + ], + "attributes": { + "url": { + "type": "string", + "default": "" + }, + "name": { + "type": "string", + "default": "" + }, + "description": { + "type": "string", + "default": "" + }, + "feedUrl": { + "type": "string", + "default": "" + }, + "photo": { + "type": "string", + "default": "" + }, + "xfn": { + "type": "array", + "default": [] + }, + "added": { + "type": "string", + "default": "" + } + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css" +} \ No newline at end of file diff --git a/build/link/index-rtl.css b/build/link/index-rtl.css new file mode 100644 index 0000000..353e294 --- /dev/null +++ b/build/link/index-rtl.css @@ -0,0 +1 @@ +.wp-block-blockroll-link .blockroll-link__address{align-items:flex-start;display:flex;gap:8px}.wp-block-blockroll-link .blockroll-link__address .components-base-control{flex-grow:1}.wp-block-blockroll-link .u-url{display:block}.wp-block-blockroll-link.h-card .blockroll-link__photo{align-self:start;border-radius:50%;grid-row:1/span 2;height:auto;margin-left:.75em;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__photo .blockroll-no-photo,.wp-block-blockroll-link.h-card .blockroll-link__photo .u-photo{margin-left:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button{align-items:center;color:inherit;display:inline-flex;font:inherit;gap:.25em;height:auto;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button:hover{text-decoration:underline}.blockroll-link__overlay .blockroll-link__overlay-more{display:grid;gap:16px;min-width:300px;padding:16px} diff --git a/build/link/index.asset.php b/build/link/index.asset.php new file mode 100644 index 0000000..d4f3a20 --- /dev/null +++ b/build/link/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '5aa3d5d9411a186187a1'); diff --git a/build/link/index.css b/build/link/index.css new file mode 100644 index 0000000..8239256 --- /dev/null +++ b/build/link/index.css @@ -0,0 +1 @@ +.wp-block-blockroll-link .blockroll-link__address{align-items:flex-start;display:flex;gap:8px}.wp-block-blockroll-link .blockroll-link__address .components-base-control{flex-grow:1}.wp-block-blockroll-link .u-url{display:block}.wp-block-blockroll-link.h-card .blockroll-link__photo{align-self:start;border-radius:50%;grid-row:1/span 2;height:auto;margin-right:.75em;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__photo .blockroll-no-photo,.wp-block-blockroll-link.h-card .blockroll-link__photo .u-photo{margin-right:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button{align-items:center;color:inherit;display:inline-flex;font:inherit;gap:.25em;height:auto;padding:0}.wp-block-blockroll-link.h-card .blockroll-link__meta-button:hover{text-decoration:underline}.blockroll-link__overlay .blockroll-link__overlay-more{display:grid;gap:16px;min-width:300px;padding:16px} diff --git a/build/link/index.js b/build/link/index.js new file mode 100644 index 0000000..686eb7e --- /dev/null +++ b/build/link/index.js @@ -0,0 +1 @@ +(()=>{"use strict";const e=window.wp.blocks,l=JSON.parse('{"UU":"blockroll/link"}'),o=window.wp.i18n,t=window.wp.element,n=window.wp.blockEditor,r=window.wp.components,s=window.wp.primitives,a=window.ReactJSXRuntime;var i=(0,a.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,a.jsx)(s.Path,{d:"M10 17.389H8.444A5.194 5.194 0 1 1 8.444 7H10v1.5H8.444a3.694 3.694 0 0 0 0 7.389H10v1.5ZM14 7h1.556a5.194 5.194 0 0 1 0 10.39H14v-1.5h1.556a3.694 3.694 0 0 0 0-7.39H14V7Zm-4.5 6h5v-1.5h-5V13Z"})}),c=(0,a.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,a.jsx)(s.Path,{d:"M5 10.2h-.8v1.5H5c1.9 0 3.8.8 5.1 2.1 1.4 1.4 2.1 3.2 2.1 5.1v.8h1.5V19c0-2.3-.9-4.5-2.6-6.2-1.6-1.6-3.8-2.6-6.1-2.6zm10.4-1.6C12.6 5.8 8.9 4.2 5 4.2h-.8v1.5H5c3.5 0 6.9 1.4 9.4 3.9s3.9 5.8 3.9 9.4v.8h1.5V19c0-3.9-1.6-7.6-4.4-10.4zM4 20h3v-3H4v3z"})});const d={friendship:["friend","acquaintance","contact"],physical:["met"],professional:["co-worker","colleague"],geographical:["co-resident","neighbor"],family:["child","parent","sibling","spouse","kin"],romantic:["muse","crush","date","sweetheart"],identity:["me"]},u=["friendship","geographical","family"],h=Object.values(d).flat();function m({value:e=[],onChange:l}){return(0,a.jsx)(r.FormTokenField,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,__experimentalExpandOnFocus:!0,label:(0,o.__)("Relationship","blockroll"),value:e,suggestions:h,onChange:e=>l(function(e){return e.filter(e=>h.includes(e)).reduce((e,l)=>function(e,l){if("me"===l)return["me"];let o=e.filter(e=>"me"!==e);for(const e of u)d[e].includes(l)&&(o=o.filter(l=>!d[e].includes(l)));return[...o,l]}(e,l),[])}(e))})}const p=(0,a.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,a.jsx)(s.Path,{d:"M12 11v1.5h8V11h-8zm-6-1c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"})});(0,e.registerBlockType)(l.UU,{icon:p,edit:function({attributes:e,setAttributes:l,context:s,isSelected:d}){const{url:u,name:h,description:p,photo:x,feedUrl:_,xfn:f,added:b}=e,[k,g]=(0,t.useState)(""),[v,w]=(0,t.useState)(null),j=!!v,C=e=>w(e?"toolbar":null),[S,N]=(0,t.useState)(),[y,B]=(0,t.useState)(null),[F,T]=(0,t.useState)(),[H,M]=(0,t.useState)(),A=(0,t.useRef)({}),R=s["blockroll/showAvatars"]??!0,z=(0,n.useBlockProps)({className:u?"h-card":"is-placeholder"}),O=x?(0,a.jsx)("img",{className:"u-photo",src:x,alt:""}):(0,a.jsx)("span",{className:"blockroll-no-photo"}),V=(0,a.jsx)(n.InspectorControls,{children:(0,a.jsx)(r.PanelBody,{title:(0,o.__)("Link settings","blockroll"),children:(0,a.jsx)(r.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,o.__)("Added","blockroll"),help:(0,o.__)("Used when the list is sorted newest first.","blockroll"),type:"date",value:b,onChange:e=>l({added:e})})})});if(!u){const e=()=>{const e=k.trim();e&&l({url:e,added:b||(new Date).toISOString().slice(0,10)})};return(0,a.jsxs)("div",{...z,children:[V,(0,a.jsxs)("form",{className:"blockroll-link__address",onSubmit:l=>{l.preventDefault(),e()},children:[(0,a.jsx)(r.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,o.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"https://example.com/",type:"url",value:k,onChange:g}),(0,a.jsx)(r.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",disabled:!k.trim(),children:(0,o.__)("Add","blockroll")})]})]})}return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.BlockControls,{group:"block",children:(0,a.jsx)(r.ToolbarButton,{icon:i,title:(0,o.__)("Link","blockroll"),isActive:j,onClick:()=>C(!j)})}),V,d&&j&&(0,a.jsx)(r.Popover,{anchor:S,placement:"bottom-start",shift:!0,onClose:()=>C(!1),focusOnMount:"toolbar"===v&&"firstElement",className:"blockroll-link__overlay",children:(0,a.jsx)(n.LinkControl,{value:{url:u,title:h},settings:[],hasTextControl:!0,forceIsEditingLink:!0,showInitialSuggestions:!1,onChange:e=>{l({url:e.url||"",name:e.title??h}),C(!1)},onCancel:()=>C(!1)})}),(0,a.jsxs)("div",{...z,children:[R&&(0,a.jsx)(n.MediaReplaceFlow,{mediaURL:x,allowedTypes:["image"],accept:"image/*",onSelect:e=>l({photo:e.url}),onSelectURL:e=>l({photo:e}),onReset:()=>l({photo:""}),popoverProps:{onFocusOutside:()=>{const e=document.activeElement;e?.closest('.block-editor-media-replace-flow__options, .media-modal, [role="dialog"]')||A.current.isOpen&&A.current.onToggle()}},renderToggle:({children:e,...l})=>(A.current={isOpen:l["aria-expanded"],onToggle:l.onClick},(0,a.jsx)(r.Button,{...l,className:"blockroll-link__photo",label:(0,o.__)("Image","blockroll"),children:O}))}),(0,a.jsx)(n.RichText,{ref:N,onFocus:()=>w("name"),tagName:"a",className:"u-url p-name",href:u,value:h,allowedFormats:[],withoutInteractiveFormatting:!0,placeholder:(0,o.__)("Site name","blockroll"),onChange:e=>l({name:e})}),(0,a.jsx)(n.RichText,{tagName:"p",className:"p-note",value:p,allowedFormats:[],withoutInteractiveFormatting:!0,placeholder:(0,o.__)("Description","blockroll"),onChange:e=>l({description:e})}),(0,a.jsxs)("div",{className:"blockroll-meta",children:[(0,a.jsxs)(r.Button,{className:"blockroll-feed blockroll-link__meta-button",ref:T,"aria-expanded":"feed"===y,onClick:()=>B("feed"===y?null:"feed"),children:[(0,a.jsx)(r.Icon,{icon:c,className:"blockroll-feed-icon"}),_?(0,o.__)("feed","blockroll"):(0,o.__)("Add feed","blockroll")]}),(0,a.jsx)("span",{className:"blockroll-divider","aria-hidden":"true",children:"·"}),(0,a.jsx)(r.Button,{className:"blockroll-link__meta-button",ref:M,"aria-expanded":"xfn"===y,onClick:()=>B("xfn"===y?null:"xfn"),children:f.length>0?(0,a.jsx)("ul",{className:"blockroll-xfn",children:f.map(e=>(0,a.jsx)("li",{children:e},e))}):(0,o.__)("Add relationship","blockroll")})]}),d&&"feed"===y&&(0,a.jsx)(r.Popover,{anchor:F,placement:"bottom-start",shift:!0,onClose:()=>B(null),focusOnMount:"firstElement",className:"blockroll-link__overlay",children:(0,a.jsx)("div",{className:"blockroll-link__overlay-more",children:(0,a.jsx)(r.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,o.__)("Feed address","blockroll"),help:(0,o.__)("The RSS or Atom feed of the site, for readers who subscribe to your list.","blockroll"),type:"url",value:_,onChange:e=>l({feedUrl:e})})})}),d&&"xfn"===y&&(0,a.jsx)(r.Popover,{anchor:H,placement:"bottom-start",shift:!0,onClose:()=>B(null),focusOnMount:!1,className:"blockroll-link__overlay",children:(0,a.jsx)("div",{className:"blockroll-link__overlay-more",ref:e=>e&&e.ownerDocument.defaultView.requestAnimationFrame(()=>e.querySelector("input")?.focus()),children:(0,a.jsx)(m,{value:f,onChange:e=>l({xfn:e})})})})]})]})},save:()=>null})})(); \ No newline at end of file diff --git a/src/blogroll/__tests__/to-url.test.js b/src/blogroll/__tests__/to-url.test.js new file mode 100644 index 0000000..d03940e --- /dev/null +++ b/src/blogroll/__tests__/to-url.test.js @@ -0,0 +1,21 @@ +import { toUrl } from '../utils'; + +describe( 'toUrl', () => { + it( 'keeps a full address', () => { + expect( toUrl( 'https://notiz.blog/' ) ).toBe( 'https://notiz.blog/' ); + expect( toUrl( 'http://notiz.blog' ) ).toBe( 'http://notiz.blog' ); + } ); + + it( 'adds https to a bare domain', () => { + expect( toUrl( 'pfefferle.org' ) ).toBe( 'https://pfefferle.org' ); + expect( toUrl( ' pfefferle.org/blog ' ) ).toBe( + 'https://pfefferle.org/blog' + ); + } ); + + it( 'leaves other schemes alone', () => { + expect( toUrl( 'feed:https://a.example/feed/' ) ).toBe( + 'feed:https://a.example/feed/' + ); + } ); +} ); diff --git a/src/blogroll/block.json b/src/blogroll/block.json index dde3988..a7b376f 100644 --- a/src/blogroll/block.json +++ b/src/blogroll/block.json @@ -4,7 +4,6 @@ "name": "blockroll/blogroll", "title": "Blogroll", "category": "widgets", - "icon": "admin-links", "description": "Share a list of the blogs and sites you follow.", "textdomain": "blockroll", "keywords": [ "blogroll", "links", "opml", "xfn" ], @@ -26,6 +25,7 @@ "showSort": { "type": "boolean", "default": true }, "showOpml": { "type": "boolean", "default": true } }, + "providesContext": { "blockroll/showAvatars": "showAvatars" }, "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "style": "file:./style-index.css", diff --git a/src/blogroll/components/add-link.js b/src/blogroll/components/add-link.js new file mode 100644 index 0000000..152ea41 --- /dev/null +++ b/src/blogroll/components/add-link.js @@ -0,0 +1,109 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; +import { Button, Notice, Popover, TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { mergeDiscovered, toUrl } from '../utils'; + +/** + * The overlay behind "Add link": an address field, anchored at the button. + * + * Adding looks the site up and fills in what it finds, the way the old + * form's "Fetch details" did. When the site can't be reached the error + * is shown and the link can be added with the address alone. + * + * @param {Object} props Component props. + * @param {Element} props.anchor Element the popover is anchored at. + * @param {Function} props.onAdd Called with the attributes of the new link. + * @param {Function} props.onClose Called when the overlay closes. + */ +export default function AddLink( { anchor, onAdd, onClose } ) { + const [ input, setInput ] = useState( '' ); + const [ isBusy, setIsBusy ] = useState( false ); + const [ error, setError ] = useState( null ); + const value = input.trim(); + + const add = () => { + const url = toUrl( value ); + if ( error ) { + onAdd( { url } ); + return; + } + setIsBusy( true ); + apiFetch( { + path: '/blockroll/v1/discover', + method: 'POST', + data: { url }, + } ) + .then( ( found ) => onAdd( mergeDiscovered( { url }, found ) ) ) + .catch( ( fetchError ) => { + setError( + fetchError.message || + __( 'The site could not be reached.', 'blockroll' ) + ); + setIsBusy( false ); + } ); + }; + + return ( + +
{ + event.preventDefault(); + if ( value && ! isBusy ) { + add(); + } + } } + > + { error && ( + + { error } + + ) } +
+ { + setInput( next ); + setError( null ); + } } + /> + +
+
+
+ ); +} diff --git a/src/blogroll/edit.js b/src/blogroll/edit.js index 5ebd142..1d1e4a9 100644 --- a/src/blogroll/edit.js +++ b/src/blogroll/edit.js @@ -13,7 +13,9 @@ import { InspectorControls, store as blockEditorStore, useBlockProps, + useInnerBlocksProps, } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; import { Button, PanelBody, @@ -23,7 +25,6 @@ import { TextControl, ToggleControl, } from '@wordpress/components'; -import { arrowDown, arrowUp, pencil, trash } from '@wordpress/icons'; import apiFetch from '@wordpress/api-fetch'; import { store as noticesStore } from '@wordpress/notices'; import { escapeHTML } from '@wordpress/escape-html'; @@ -31,9 +32,8 @@ import { escapeHTML } from '@wordpress/escape-html'; /** * Internal dependencies */ -import LinkForm from './components/link-form'; +import AddLink from './components/add-link'; import ImportModal from './components/import-modal'; -import { move } from './utils'; import { isGeneratedFrom, slugOf, uniqueAnchor } from './anchors'; /** @@ -206,7 +206,6 @@ export default function Edit( { [] ); - const [ editing, setEditing ] = useState( null ); // Index, 'new', or null. const [ isImporting, setIsImporting ] = useState( false ); const [ sources, setSources ] = useState( [ { label: __( 'Manual links', 'blockroll' ), value: 'manual' }, @@ -288,16 +287,33 @@ export default function Edit( { }; }, [ currentSource, serializedAttributes ] ); - const saveLink = ( link ) => { - const next = [ ...links ]; - if ( 'new' === editing ) { - next.push( link ); - } else { - next[ editing ] = link; - } - setAttributes( { links: next, source: manualSource } ); - setEditing( null ); + // The links of a manual list are blocks of their own. + const linkCount = useSelect( + ( select ) => select( blockEditorStore ).getBlockCount( clientId ), + [ clientId ] + ); + const { insertBlock } = useDispatch( blockEditorStore ); + const [ isAdding, setIsAdding ] = useState( false ); + const [ addAnchor, setAddAnchor ] = useState(); + const addLink = ( link ) => { + insertBlock( + createBlock( 'blockroll/link', { + ...link, + added: new Date().toISOString().slice( 0, 10 ), + } ), + undefined, + clientId + ); + setIsAdding( false ); }; + const innerBlocksProps = useInnerBlocksProps( + { className: 'blockroll-editor-links' }, + { + allowedBlocks: [ 'blockroll/link' ], + templateLock: false, + renderAppender: false, + } + ); const importLinks = ( imported ) => { const known = new Set( links.map( ( link ) => link.url ) ); @@ -312,16 +328,28 @@ export default function Edit( { const actions = (
- + { isAdding && ( + setIsAdding( false ) } + /> + ) } - { ! links.length && + { ! linkCount && manualSource === currentSource && externalSources.map( ( item ) => ( + +
+ ); + } + + return ( + <> + + setIsEditingLink( ! isEditingLink ) } + /> + + { inspector } + { isSelected && isEditingLink && ( + setIsEditingLink( false ) } + focusOnMount={ + 'toolbar' === linkOverlay ? 'firstElement' : false + } + className="blockroll-link__overlay" + > + { + setAttributes( { + url: next.url || '', + name: next.title ?? name, + } ); + setIsEditingLink( false ); + } } + onCancel={ () => setIsEditingLink( false ) } + /> + + ) } +
+ { showAvatar && ( + + setAttributes( { photo: media.url } ) + } + onSelectURL={ ( value ) => + setAttributes( { photo: value } ) + } + onReset={ () => setAttributes( { photo: '' } ) } + popoverProps={ { + onFocusOutside: closePhotoMenuIfFocusOutside, + } } + renderToggle={ ( { children, ...toggleProps } ) => { + photoMenu.current = { + isOpen: toggleProps[ 'aria-expanded' ], + onToggle: toggleProps.onClick, + }; + return ( + + ); + } } + /> + ) } + setLinkOverlay( 'name' ) } + tagName="a" + className="u-url p-name" + href={ url } + value={ name } + allowedFormats={ [] } + withoutInteractiveFormatting + placeholder={ __( 'Site name', 'blockroll' ) } + onChange={ ( value ) => setAttributes( { name: value } ) } + /> + + setAttributes( { description: value } ) + } + /> +
+ + + +
+ { isSelected && 'feed' === metaOverlay && ( + setMetaOverlay( null ) } + focusOnMount="firstElement" + className="blockroll-link__overlay" + > +
+ + setAttributes( { feedUrl: value } ) + } + /> +
+
+ ) } + { isSelected && 'xfn' === metaOverlay && ( + setMetaOverlay( null ) } + focusOnMount={ false } + className="blockroll-link__overlay" + > + { /* The first focusable thing would be a token's remove + button, so the input is focused by hand, once the + popover is placed and can take focus. */ } +
+ node && + node.ownerDocument.defaultView.requestAnimationFrame( + () => node.querySelector( 'input' )?.focus() + ) + } + > + + setAttributes( { xfn: value } ) + } + /> +
+
+ ) } +
+ + ); +} diff --git a/src/link/editor.scss b/src/link/editor.scss new file mode 100644 index 0000000..28611d6 --- /dev/null +++ b/src/link/editor.scss @@ -0,0 +1,58 @@ +.wp-block-blockroll-link { + + .blockroll-link__address { + display: flex; + align-items: flex-start; + gap: 8px; + + .components-base-control { + flex-grow: 1; + } + } + + // The rest of the card is styled by the frontend stylesheet. + .u-url { + display: block; + } + + // The image is a button here; it takes the place the image has on + // the site and the image inside loses its own margin. + &.h-card .blockroll-link__photo { + grid-row: 1 / span 2; + align-self: start; + height: auto; + padding: 0; + margin-right: 0.75em; + border-radius: 50%; + + .u-photo, + .blockroll-no-photo { + margin-right: 0; + } + } + + // The meta row's parts are buttons here, looking like the text on the site. + &.h-card .blockroll-link__meta-button { + display: inline-flex; + align-items: center; + gap: 0.25em; + height: auto; + padding: 0; + font: inherit; + color: inherit; + + &:hover { + text-decoration: underline; + } + } +} + +.blockroll-link__overlay { + + .blockroll-link__overlay-more { + display: grid; + gap: 16px; + padding: 16px; + min-width: 300px; + } +} diff --git a/src/link/index.js b/src/link/index.js new file mode 100644 index 0000000..6f38463 --- /dev/null +++ b/src/link/index.js @@ -0,0 +1,9 @@ +import { registerBlockType } from '@wordpress/blocks'; +import metadata from './block.json'; +import Edit from './edit'; +import { link as icon } from '../icons'; +import './editor.scss'; + +// The blogroll renders the whole list on the server, so a link has no +// output of its own. +registerBlockType( metadata.name, { icon, edit: Edit, save: () => null } ); From 569d65c07928c94c67bf91150c026e55b578dc82 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:32:32 +0200 Subject: [PATCH 03/23] migrate old links and import into link blocks --- build/blogroll/index-rtl.css | 2 +- build/blogroll/index.asset.php | 2 +- build/blogroll/index.css | 2 +- build/blogroll/index.js | 12 ++--- src/blogroll/__tests__/migrate.test.js | 68 ++++++++++++++++++++++++++ src/blogroll/deprecated.js | 38 ++++++++++++++ src/blogroll/edit.js | 36 +++++++++----- src/blogroll/editor.scss | 4 ++ src/blogroll/index.js | 2 + src/blogroll/utils.js | 45 +++++++++++++++++ 10 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 src/blogroll/__tests__/migrate.test.js create mode 100644 src/blogroll/deprecated.js diff --git a/build/blogroll/index-rtl.css b/build/blogroll/index-rtl.css index e602659..6de65e9 100644 --- a/build/blogroll/index-rtl.css +++ b/build/blogroll/index-rtl.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 8209dc7..279029a 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'f360bc8f81ff92b6a2c1'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'b30ca27182b5430c7c86'); diff --git a/build/blogroll/index.css b/build/blogroll/index.css index e602659..6de65e9 100644 --- a/build/blogroll/index.css +++ b/build/blogroll/index.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/build/blogroll/index.js b/build/blogroll/index.js index 8777db5..b477074 100644 --- a/build/blogroll/index.js +++ b/build/blogroll/index.js @@ -1,6 +1,6 @@ -(()=>{"use strict";var e={3(e,l,o){const t=window.wp.blocks,n=window.wp.blockEditor,r=JSON.parse('{"UU":"blockroll/blogroll"}'),s=window.wp.i18n,a=window.wp.element,c=window.wp.data,i=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function b(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}const _=window.ReactJSXRuntime;function m({anchor:e,onAdd:l,onClose:o}){const[t,n]=(0,a.useState)(""),[r,c]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,_.jsx)(i.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,_.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!r&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(c(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(b({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),c(!1)}))})()},children:[d&&(0,_.jsx)(i.Notice,{status:"error",isDismissible:!1,children:d}),(0,_.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",spellCheck:!1,value:t,disabled:r,onChange:e=>{n(e),h(null)}}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:r,disabled:!p||r,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const k=window.wp.primitives;var x=(0,_.jsx)(k.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,_.jsx)(k.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function f({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[n,r]=(0,a.useState)(""),[c,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[m,k]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),r(e.name))},S={file:(0,_.jsxs)("div",{className:"blockroll-import-upload",children:[(0,_.jsx)(i.DropZone,{onFilesDrop:e=>j(e[0])}),(0,_.jsx)(i.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:n||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,_.jsx)(i.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),r("")}}),url:(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:c,onChange:d})};return(0,_.jsx)(i.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,_.jsxs)("div",{className:"blockroll-form",children:[v&&(0,_.jsx)(i.Notice,{status:"error",isDismissible:!1,children:v}),(0,_.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,_.jsx)(i.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,_.jsx)("div",{className:"blockroll-import-tab",children:S[e.name]})}),(0,_.jsx)(i.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,_.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,_.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ -(0,s.__)("%d links processed…","blockroll"),f)}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,_.jsx)(i.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:m,disabled:!o&&!c||m,onClick:async()=>{k(!0),w(null);try{const t=o?{opml:o}:{url:c},{links:n}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),r=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(b(t,e))}catch{o.push(t)}l(o.length)}return o}(n,g):n;e(r),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),k(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const g=window.wp.url;function v(e){return(0,g.cleanForSlug)(e)||"blogroll"}function w(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const j=(0,_.jsx)(k.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,_.jsx)(k.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(r.UU,{icon:j,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:r}){const{anchor:d,links:b,source:k,sortBy:x,perPage:g,showAvatars:j,showSort:S,showOpml:C,metadata:y}=e,B=y?.name||"",N=(0,c.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:M}=(0,c.useDispatch)(n.store),O=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(n.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return M(),void l({anchor:w(v(B),O())});if(r)return;const e=O();e.includes(d)&&(M(),l({anchor:w(d,e)}))},[d,r]);const A=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(A.current===B)return;const e=!d||function(e,l){const o=v(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,A.current);A.current=B,e&&l({anchor:w(v(B),O())})},[B]);const T=(0,c.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(n.store);if(!d||r)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,r]),{createWarningNotice:D,removeNotice:H}=(0,c.useDispatch)(h.store),P=(0,a.useRef)(null);(0,a.useEffect)(()=>{P.current&&(H(P.current),P.current=null),T&&(P.current=`blockroll-anchor-${d}`,D((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ -(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:P.current,isDismissible:!0,__unstableHTML:!0}))},[T,d]),(0,a.useEffect)(()=>()=>{P.current&&H(P.current)},[]);const[I,L]=(0,a.useState)(!1),[U,z]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[W,G]=(0,a.useState)(!1),J="manual",q=U.some(e=>k===e.value),X=!k||W&&!q?J:k,K=JSON.stringify(e),Q=U.filter(e=>J!==e.value),Y=U.find(e=>X===e.value)||U[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(z(e),G(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return J===X?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${X}/links?attributes=`+encodeURIComponent(K)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[X,K]);const ee=(0,c.useSelect)(e=>e(n.store).getBlockCount(o),[o]),{insertBlock:le}=(0,c.useDispatch)(n.store),[oe,te]=(0,a.useState)(!1),[ne,re]=(0,a.useState)(),se=(0,n.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ae=(0,_.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,_.jsx)(i.Button,{variant:"primary",ref:re,"aria-expanded":oe,onClick:()=>te(!oe),children:(0,s.__)("Add link","blockroll")}),oe&&(0,_.jsx)(m,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),te(!1)},onClose:()=>te(!1)}),(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>L(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&J===X&&Q.map(e=>(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ -(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;J!==X||ee||(ce=(0,_.jsx)(i.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ae}));const ie=(0,_.jsx)("div",{className:"blockroll-editor-actions",children:(0,_.jsx)(i.Button,{variant:"secondary",onClick:()=>l({source:J}),children:(0,s.__)("Use manual links","blockroll")})});return(0,_.jsxs)("div",{...(0,n.useBlockProps)(),children:[(0,_.jsx)(n.InspectorControls,{children:(0,_.jsxs)(i.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,_.jsx)(i.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ -(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),d||v(B)),{code:(0,_.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:B,onChange:e=>{const o={...y};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,_.jsx)(i.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:x,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,_.jsx)(i.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:g,onChange:e=>l({perPage:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:j,onChange:e=>l({showAvatars:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:S,onChange:e=>l({showSort:e})}),(0,_.jsx)(i.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:C,onChange:e=>l({showOpml:e})})]})}),I&&(0,_.jsx)(f,{onImport:e=>{const o=new Set(b.map(e=>e.url));l({source:J,links:[...b,...e.filter(e=>!o.has(e.url))]})},onClose:()=>L(!1)}),J!==X?(0,_.jsxs)("div",{className:"blockroll-editor",children:[(0,_.jsx)(i.Placeholder,{icon:"admin-links",label:Y.label,instructions:F?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ -(0,s.__)("%d links will be shown when the block is rendered.","blockroll"),V.length),children:ie}),Y.help&&(0,_.jsxs)("p",{children:[Y.help,Y.helpUrl&&(0,_.jsxs)(_.Fragment,{children:[" ",(0,_.jsx)("a",{href:Y.helpUrl,children:(0,s.__)("Manage source","blockroll")})]})]}),Z&&(0,_.jsx)("p",{children:Z}),!Z&&!F&&!!V.length&&(de=V,(0,_.jsx)("ul",{className:"blockroll-editor-list",children:de.map(e=>(0,_.jsxs)("li",{children:[j&&(e.photo?(0,_.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,_.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,_.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,_.jsx)("strong",{children:e.name||e.url}),(0,_.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]})]},e.url))}))]}):(0,_.jsxs)("div",{className:"blockroll-editor",children:[ce||(0,_.jsx)("div",{...se}),!ce&&ae]})]});var de},save:()=>(0,_.jsx)(n.InnerBlocks.Content,{})})}};const l={};function o(t){const n=l[t];if(void 0!==n)return n.exports;const r=l[t]={exports:{}};return e[t](r,r.exports,o),r.exports}o.m=e,(()=>{const e=[];o.O=(l,t,n,r)=>{if(t){r=r||0;for(var s=e.length;s>0&&e[s-1][2]>r;s--)e[s]=e[s-1];return void(e[s]=[t,n,r])}let a=1/0;for(s=0;s=r)&&Object.keys(o.O).every(e=>o.O[e](t[c]))?t.splice(c--,1):(i=!1,r{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[n,r,s]=t;var a,c,i=0;if(n.some(l=>0!==e[l])){for(a in r)o.o(r,a)&&(o.m[a]=r[a]);if(s)var d=s(o)}for(l&&l(t);io(3));t=o.O(t)})(); \ No newline at end of file +(()=>{"use strict";var e={704(e,l,o){const t=window.wp.blocks,r=window.wp.blockEditor,n=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function m(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function b(e){return{url:e?.url||"",name:e?.name||"",description:e?.description||"",feedUrl:e?.feedUrl||"",photo:e?.photo||"",xfn:Array.isArray(e?.xfn)?e.xfn:[],added:e?.added||""}}const k=window.ReactJSXRuntime;function _({anchor:e,onAdd:l,onClose:o}){const[t,r]=(0,a.useState)(""),[n,i]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,k.jsx)(c.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,k.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!n&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(i(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(m({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),i(!1)}))})()},children:[d&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,k.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",spellCheck:!1,value:t,disabled:n,onChange:e=>{r(e),h(null)}}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:n,disabled:!p||n,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const f=window.wp.primitives;var x=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function g({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),n(e.name))},y={file:(0,k.jsxs)("div",{className:"blockroll-import-upload",children:[(0,k.jsx)(c.DropZone,{onFilesDrop:e=>j(e[0])}),(0,k.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:r||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,k.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),n("")}}),url:(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,k.jsx)(c.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,k.jsxs)("div",{className:"blockroll-form",children:[v&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:v}),(0,k.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,k.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,k.jsx)("div",{className:"blockroll-import-tab",children:y[e.name]})}),(0,k.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,k.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,k.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ +(0,s.__)("%d links processed…","blockroll"),f)}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:b,disabled:!o&&!i||b,onClick:async()=>{_(!0),w(null);try{const t=o?{opml:o}:{url:i},{links:r}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),n=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(m(t,e))}catch{o.push(t)}l(o.length)}return o}(r,g):r;e(n),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),_(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const v=window.wp.url;function w(e){return(0,v.cleanForSlug)(e)||"blogroll"}function j(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const y=[{attributes:n.uK,supports:n.xY,save:()=>null,isEligible:(e,l)=>function(e){return Array.isArray(e?.links)&&e.links.length>0}(e)&&!l?.length,migrate:(e,l)=>{const[o,r]=function(e){return[{...e,links:[]},(e.links||[]).filter(e=>e?.url).map(b)]}(e);return[o,[...l||[],...r.map(e=>(0,t.createBlock)("blockroll/link",e))]]}}],S=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(n.UU,{icon:S,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:n}){const{anchor:d,source:m,sortBy:f,perPage:x,showAvatars:v,showSort:y,showOpml:S,metadata:C}=e,B=C?.name||"",N=(0,i.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:A}=(0,i.useDispatch)(r.store),M=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(r.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return A(),void l({anchor:j(w(B),M())});if(n)return;const e=M();e.includes(d)&&(A(),l({anchor:j(d,e)}))},[d,n]);const O=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(O.current===B)return;const e=!d||function(e,l){const o=w(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,O.current);O.current=B,e&&l({anchor:j(w(B),M())})},[B]);const H=(0,i.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(r.store);if(!d||n)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,n]),{createWarningNotice:P,removeNotice:T}=(0,i.useDispatch)(h.store),D=(0,a.useRef)(null);(0,a.useEffect)(()=>{D.current&&(T(D.current),D.current=null),H&&(D.current=`blockroll-anchor-${d}`,P((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ +(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:D.current,isDismissible:!0,__unstableHTML:!0}))},[H,d]),(0,a.useEffect)(()=>()=>{D.current&&T(D.current)},[]);const[I,L]=(0,a.useState)(!1),[U,z]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[W,G]=(0,a.useState)(!1),J="manual",K=U.some(e=>m===e.value),Y=!m||W&&!K?J:m,q=JSON.stringify(e),X=U.filter(e=>J!==e.value),Q=U.find(e=>Y===e.value)||U[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(z(e),G(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return J===Y?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${Y}/links?attributes=`+encodeURIComponent(q)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[Y,q]);const ee=(0,i.useSelect)(e=>e(r.store).getBlockCount(o),[o]),{insertBlock:le,insertBlocks:oe}=(0,i.useDispatch)(r.store),[te,re]=(0,a.useState)(!1),[ne,se]=(0,a.useState)(),ae=(0,r.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ie=(0,k.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,k.jsx)(c.Button,{variant:"primary",ref:se,"aria-expanded":te,onClick:()=>re(!te),children:(0,s.__)("Add link","blockroll")}),te&&(0,k.jsx)(_,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),re(!1)},onClose:()=>re(!1)}),(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>L(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&J===Y&&X.map(e=>(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ +(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;J!==Y||ee||(ce=(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ie}));const de=(0,k.jsx)("div",{className:"blockroll-editor-actions",children:(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:J}),children:(0,s.__)("Use manual links","blockroll")})});return(0,k.jsxs)("div",{...(0,r.useBlockProps)(),children:[(0,k.jsx)(r.InspectorControls,{children:(0,k.jsxs)(c.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ +(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),d||w(B)),{code:(0,k.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:B,onChange:e=>{const o={...C};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,k.jsx)(c.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:f,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,k.jsx)(c.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:x,onChange:e=>l({perPage:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:v,onChange:e=>l({showAvatars:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:y,onChange:e=>l({showSort:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:S,onChange:e=>l({showOpml:e})})]})}),I&&(0,k.jsx)(g,{onImport:e=>{const n=new Set(N.select(r.store).getBlock(o).innerBlocks.map(e=>e.attributes.url)),s=e.filter(e=>e.url&&!n.has(e.url)).map(e=>(0,t.createBlock)("blockroll/link",b(e)));J!==Y&&l({source:J}),s.length&&oe(s,void 0,o)},onClose:()=>L(!1)}),J!==Y?(0,k.jsxs)("div",{className:"blockroll-editor",children:[(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:Q.label,instructions:F?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ +(0,s.__)("%d links will be shown when the block is rendered.","blockroll"),V.length),children:de}),Q.help&&(0,k.jsxs)("p",{children:[Q.help,Q.helpUrl&&(0,k.jsxs)(k.Fragment,{children:[" ",(0,k.jsx)("a",{href:Q.helpUrl,children:(0,s.__)("Manage source","blockroll")})]})]}),Z&&(0,k.jsx)("p",{children:Z}),!Z&&!F&&!!V.length&&(ue=V,(0,k.jsx)("ul",{className:"blockroll-editor-list",children:ue.map(e=>(0,k.jsxs)("li",{children:[v&&(e.photo?(0,k.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,k.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,k.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,k.jsx)("strong",{children:e.name||e.url}),(0,k.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]})]},e.url))}))]}):(0,k.jsxs)("div",{className:"blockroll-editor",children:[ce,(0,k.jsx)("div",{...ae}),!ce&&ie]})]});var ue},save:()=>(0,k.jsx)(r.InnerBlocks.Content,{}),deprecated:y})}};const l={};function o(t){const r=l[t];if(void 0!==r)return r.exports;const n=l[t]={exports:{}};return e[t](n,n.exports,o),n.exports}o.m=e,(()=>{const e=[];o.O=(l,t,r,n)=>{if(t){n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];return void(e[s]=[t,r,n])}let a=1/0;for(s=0;s=n)&&Object.keys(o.O).every(e=>o.O[e](t[i]))?t.splice(i--,1):(c=!1,n{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[r,n,s]=t;var a,i,c=0;if(r.some(l=>0!==e[l])){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(s)var d=s(o)}for(l&&l(t);co(704));t=o.O(t)})(); \ No newline at end of file diff --git a/src/blogroll/__tests__/migrate.test.js b/src/blogroll/__tests__/migrate.test.js new file mode 100644 index 0000000..0594bbd --- /dev/null +++ b/src/blogroll/__tests__/migrate.test.js @@ -0,0 +1,68 @@ +import { hasLegacyLinks, linkBlockAttributes, migrateLinks } from '../utils'; + +describe( 'linkBlockAttributes', () => { + it( 'fills every field', () => { + expect( linkBlockAttributes( { url: 'https://a.example/' } ) ).toEqual( + { + url: 'https://a.example/', + name: '', + description: '', + feedUrl: '', + photo: '', + xfn: [], + added: '', + } + ); + } ); + + it( 'drops unknown fields and keeps known ones', () => { + expect( + linkBlockAttributes( { + url: 'https://a.example/', + name: 'A', + xfn: [ 'friend' ], + extra: 1, + } ) + ).toEqual( { + url: 'https://a.example/', + name: 'A', + description: '', + feedUrl: '', + photo: '', + xfn: [ 'friend' ], + added: '', + } ); + } ); +} ); + +describe( 'hasLegacyLinks', () => { + it( 'is true only for a non-empty links array', () => { + expect( hasLegacyLinks( { links: [ { url: 'x' } ] } ) ).toBe( true ); + expect( hasLegacyLinks( { links: [] } ) ).toBe( false ); + expect( hasLegacyLinks( {} ) ).toBe( false ); + } ); +} ); + +describe( 'migrateLinks', () => { + it( 'empties the attribute and returns the links as block attributes', () => { + const [ attributes, links ] = migrateLinks( { + anchor: 'blogroll', + sortBy: 'manual', + links: [ + { url: 'https://a.example/', name: 'A' }, + { name: 'no url' }, + { url: 'https://b.example/', xfn: [ 'me' ] }, + ], + } ); + expect( attributes ).toEqual( { + anchor: 'blogroll', + sortBy: 'manual', + links: [], + } ); + expect( links.map( ( link ) => link.url ) ).toEqual( [ + 'https://a.example/', + 'https://b.example/', + ] ); + expect( links[ 1 ].xfn ).toEqual( [ 'me' ] ); + } ); +} ); diff --git a/src/blogroll/deprecated.js b/src/blogroll/deprecated.js new file mode 100644 index 0000000..b211863 --- /dev/null +++ b/src/blogroll/deprecated.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { hasLegacyLinks, migrateLinks } from './utils'; + +/** + * Before 1.3 the links were an attribute of the blogroll; now each is a + * block of its own inside it. The saved markup was empty either way, so + * the old block is still valid; isEligible makes the editor migrate it + * anyway. The server keeps reading the attribute for posts nobody opens. + */ +const v1 = { + attributes: metadata.attributes, + supports: metadata.supports, + save: () => null, + isEligible: ( attributes, innerBlocks ) => + hasLegacyLinks( attributes ) && ! innerBlocks?.length, + migrate: ( attributes, innerBlocks ) => { + const [ next, links ] = migrateLinks( attributes ); + return [ + next, + [ + ...( innerBlocks || [] ), + ...links.map( ( link ) => + createBlock( 'blockroll/link', link ) + ), + ], + ]; + }, +}; + +export default [ v1 ]; diff --git a/src/blogroll/edit.js b/src/blogroll/edit.js index 1d1e4a9..ef53899 100644 --- a/src/blogroll/edit.js +++ b/src/blogroll/edit.js @@ -33,6 +33,7 @@ import { escapeHTML } from '@wordpress/escape-html'; * Internal dependencies */ import AddLink from './components/add-link'; +import { linkBlockAttributes } from './utils'; import ImportModal from './components/import-modal'; import { isGeneratedFrom, slugOf, uniqueAnchor } from './anchors'; @@ -53,7 +54,6 @@ export default function Edit( { } ) { const { anchor, - links, source, sortBy, perPage, @@ -292,7 +292,7 @@ export default function Edit( { ( select ) => select( blockEditorStore ).getBlockCount( clientId ), [ clientId ] ); - const { insertBlock } = useDispatch( blockEditorStore ); + const { insertBlock, insertBlocks } = useDispatch( blockEditorStore ); const [ isAdding, setIsAdding ] = useState( false ); const [ addAnchor, setAddAnchor ] = useState(); const addLink = ( link ) => { @@ -315,15 +315,26 @@ export default function Edit( { } ); + // Imported links become link blocks at the end of the list; addresses + // already in it are skipped. const importLinks = ( imported ) => { - const known = new Set( links.map( ( link ) => link.url ) ); - setAttributes( { - source: manualSource, - links: [ - ...links, - ...imported.filter( ( link ) => ! known.has( link.url ) ), - ], - } ); + const known = new Set( + registry + .select( blockEditorStore ) + .getBlock( clientId ) + .innerBlocks.map( ( block ) => block.attributes.url ) + ); + const blocks = imported + .filter( ( link ) => link.url && ! known.has( link.url ) ) + .map( ( link ) => + createBlock( 'blockroll/link', linkBlockAttributes( link ) ) + ); + if ( manualSource !== currentSource ) { + setAttributes( { source: manualSource } ); + } + if ( blocks.length ) { + insertBlocks( blocks, undefined, clientId ); + } }; const actions = ( @@ -563,7 +574,10 @@ export default function Edit( { ) : (
- { emptyState ||
} + { emptyState } + { /* Always rendered: it is what registers the block + list settings a link block needs to be inserted. */ } +
{ ! emptyState && actions }
) } diff --git a/src/blogroll/editor.scss b/src/blogroll/editor.scss index 5b45af4..e2fe125 100644 --- a/src/blogroll/editor.scss +++ b/src/blogroll/editor.scss @@ -10,6 +10,10 @@ gap: 12px; } + .blockroll-editor-links:empty { + display: none; + } + .blockroll-editor-list { list-style: none; margin: 0; diff --git a/src/blogroll/index.js b/src/blogroll/index.js index 7923519..154ddbf 100644 --- a/src/blogroll/index.js +++ b/src/blogroll/index.js @@ -2,6 +2,7 @@ import { registerBlockType } from '@wordpress/blocks'; import { InnerBlocks } from '@wordpress/block-editor'; import metadata from './block.json'; import Edit from './edit'; +import deprecated from './deprecated'; import { blogroll as icon } from '../icons'; import './style.scss'; import './editor.scss'; @@ -11,4 +12,5 @@ registerBlockType( metadata.name, { icon, edit: Edit, save: () => , + deprecated, } ); diff --git a/src/blogroll/utils.js b/src/blogroll/utils.js index 9f41e0e..ae8ee9c 100644 --- a/src/blogroll/utils.js +++ b/src/blogroll/utils.js @@ -39,3 +39,48 @@ export function toUrl( input ) { const value = input.trim(); return /^[a-z][a-z0-9+.-]*:/i.test( value ) ? value : `https://${ value }`; } + +/** + * The attributes of a link block, from a link the way the block stored + * it in its own attributes before links became blocks, or the way an + * import or a source delivers it. Only the known fields, filled in. + * + * @param {Object} link Raw link. + * @return {Object} Link block attributes. + */ +export function linkBlockAttributes( link ) { + return { + url: link?.url || '', + name: link?.name || '', + description: link?.description || '', + feedUrl: link?.feedUrl || '', + photo: link?.photo || '', + xfn: Array.isArray( link?.xfn ) ? link.xfn : [], + added: link?.added || '', + }; +} + +/** + * Whether a blogroll block still has its links in the `links` attribute. + * + * @param {Object} attributes Block attributes. + * @return {boolean} True for a block saved before links were blocks. + */ +export function hasLegacyLinks( attributes ) { + return Array.isArray( attributes?.links ) && attributes.links.length > 0; +} + +/** + * Move the links of a block's `links` attribute out of it. + * + * @param {Object} attributes Block attributes. + * @return {Array} The attributes without links, and the link block attributes. + */ +export function migrateLinks( attributes ) { + return [ + { ...attributes, links: [] }, + ( attributes.links || [] ) + .filter( ( link ) => link?.url ) + .map( linkBlockAttributes ), + ]; +} From c2b9af33f92037dcbd2d2215d5ac2c2d7508c71f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:32:59 +0200 Subject: [PATCH 04/23] remove the old link form --- build/blogroll/index-rtl.css | 2 +- build/blogroll/index.asset.php | 2 +- build/blogroll/index.css | 2 +- src/blogroll/__tests__/utils.test.js | 18 +-- src/blogroll/components/link-form.js | 161 --------------------------- src/blogroll/editor.scss | 27 ----- src/blogroll/utils.js | 14 --- 7 files changed, 4 insertions(+), 222 deletions(-) delete mode 100644 src/blogroll/components/link-form.js diff --git a/build/blogroll/index-rtl.css b/build/blogroll/index-rtl.css index 6de65e9..7974b79 100644 --- a/build/blogroll/index-rtl.css +++ b/build/blogroll/index-rtl.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 279029a..0c1676a 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'b30ca27182b5430c7c86'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'a4b8bf32f8b8c9b1a924'); diff --git a/build/blogroll/index.css b/build/blogroll/index.css index 6de65e9..7974b79 100644 --- a/build/blogroll/index.css +++ b/build/blogroll/index.css @@ -1 +1 @@ -.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__actions{display:flex;flex-shrink:0}.blockroll-form{display:grid;gap:16px}.blockroll-form__row{align-items:flex-end;display:flex;gap:8px}.blockroll-form__row>:first-child{flex-grow:1}.blockroll-form__footer{align-items:center;display:flex;gap:8px;justify-content:flex-end;margin-top:8px}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} +.wp-block-blockroll-blogroll .blockroll-editor-actions{display:flex;gap:8px}.wp-block-blockroll-blogroll .blockroll-editor{display:grid;gap:12px}.wp-block-blockroll-blogroll .blockroll-editor-links:empty{display:none}.wp-block-blockroll-blogroll .blockroll-editor-list{border:1px solid #ddd;border-radius:4px;list-style:none;margin:0;padding:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li{align-items:center;display:flex;gap:12px;padding:8px 12px}.wp-block-blockroll-blogroll .blockroll-editor-list>li:not(:last-child){border-bottom:1px solid #eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo,.wp-block-blockroll-blogroll .blockroll-editor-list>li img{border-radius:50%;flex-shrink:0;height:32px;object-fit:cover;width:32px}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__no-photo{background:#eee}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text{display:grid;flex-grow:1;min-width:0}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small,.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wp-block-blockroll-blogroll .blockroll-editor-list>li .blockroll-editor-list__text small{opacity:.7}.blockroll-import-intro{margin:0}.blockroll-import-tab{padding:16px 0}.blockroll-import-upload{align-items:center;border:1px dashed #949494;border-radius:4px;display:flex;justify-content:center;min-height:100px;position:relative}.blockroll-add-link .blockroll-add-link__form{display:grid;gap:8px;max-width:90vw;padding:8px;width:360px}.blockroll-add-link .blockroll-add-link__row{display:flex;gap:8px}.blockroll-add-link .blockroll-add-link__row .components-base-control{flex-grow:1} diff --git a/src/blogroll/__tests__/utils.test.js b/src/blogroll/__tests__/utils.test.js index 82f5e65..b886c93 100644 --- a/src/blogroll/__tests__/utils.test.js +++ b/src/blogroll/__tests__/utils.test.js @@ -1,23 +1,7 @@ /** * Internal dependencies */ -import { move, mergeDiscovered } from '../utils'; - -describe( 'move', () => { - it( 'moves an element down', () => { - expect( move( [ 'a', 'b', 'c' ], 0, 1 ) ).toEqual( [ 'b', 'a', 'c' ] ); - } ); - - it( 'moves an element up', () => { - expect( move( [ 'a', 'b', 'c' ], 2, 0 ) ).toEqual( [ 'c', 'a', 'b' ] ); - } ); - - it( 'does not mutate the original', () => { - const original = [ 'a', 'b' ]; - move( original, 0, 1 ); - expect( original ).toEqual( [ 'a', 'b' ] ); - } ); -} ); +import { mergeDiscovered } from '../utils'; describe( 'mergeDiscovered', () => { it( 'fills empty fields only', () => { diff --git a/src/blogroll/components/link-form.js b/src/blogroll/components/link-form.js deleted file mode 100644 index b50862b..0000000 --- a/src/blogroll/components/link-form.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; -import { - Button, - Modal, - Notice, - TextControl, - TextareaControl, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import XfnControl from './xfn-control'; -import { mergeDiscovered } from '../utils'; - -const EMPTY = { - url: '', - name: '', - description: '', - feedUrl: '', - photo: '', - xfn: [], - added: '', -}; - -/** - * Modal to add or edit a single link. - * - * @param {Object} props Component props. - * @param {Object} props.link Link to edit, or undefined for a new one. - * @param {Function} props.onSave Called with the finished link. - * @param {Function} props.onCancel Called when the user cancels. - */ -export default function LinkForm( { link, onSave, onCancel } ) { - const [ draft, setDraft ] = useState( { ...EMPTY, ...link } ); - const [ isFetching, setIsFetching ] = useState( false ); - const [ error, setError ] = useState( null ); - - const set = ( field ) => ( fieldValue ) => - setDraft( ( current ) => ( { ...current, [ field ]: fieldValue } ) ); - - const fetchDetails = () => { - setIsFetching( true ); - setError( null ); - apiFetch( { - path: '/blockroll/v1/discover', - method: 'POST', - data: { url: draft.url }, - } ) - .then( ( found ) => - setDraft( ( current ) => mergeDiscovered( current, found ) ) - ) - .catch( ( fetchError ) => - setError( - fetchError.message || - __( 'The site could not be reached.', 'blockroll' ) - ) - ) - .finally( () => setIsFetching( false ) ); - }; - - const save = () => - onSave( { - ...draft, - added: draft.added || new Date().toISOString().slice( 0, 10 ), - } ); - - return ( - -
- { error && ( - - { error } - - ) } -
- - -
- - - - - -
- - -
-
-
- ); -} diff --git a/src/blogroll/editor.scss b/src/blogroll/editor.scss index e2fe125..09c84f1 100644 --- a/src/blogroll/editor.scss +++ b/src/blogroll/editor.scss @@ -61,37 +61,10 @@ } } - .blockroll-editor-list__actions { - display: flex; - flex-shrink: 0; - } } } } -.blockroll-form { - display: grid; - gap: 16px; -} - -.blockroll-form__row { - display: flex; - align-items: flex-end; - gap: 8px; - - > :first-child { - flex-grow: 1; - } -} - -.blockroll-form__footer { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 8px; - margin-top: 8px; -} - .blockroll-import-intro { margin: 0; } diff --git a/src/blogroll/utils.js b/src/blogroll/utils.js index ae8ee9c..7d924c6 100644 --- a/src/blogroll/utils.js +++ b/src/blogroll/utils.js @@ -1,17 +1,3 @@ -/** - * Move an array element. - * - * @param {Array} list Array. - * @param {number} from Index to move. - * @param {number} to Target index. - * @return {Array} New array. - */ -export function move( list, from, to ) { - const next = [ ...list ]; - next.splice( to, 0, ...next.splice( from, 1 ) ); - return next; -} - /** * Fill empty link fields with discovered values; existing values win. * From a0a0ddaa5e7f621f1011aea4fbe336ae2bddc373 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:33:18 +0200 Subject: [PATCH 05/23] document link blocks --- docs/developers.md | 12 +++++++++--- readme.md | 6 ++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/developers.md b/docs/developers.md index 3c61413..ea2ff6b 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -4,9 +4,15 @@ Notes for people who build on the plugin or work on it. Everything a user needs ## How it works -The links live in the block attributes, so the whole blogroll travels with the post -content. Each link holds `url`, `name`, `description`, `feedUrl`, `photo`, an `xfn` -array, and the date it was added. +Every link is a block of its own, `blockroll/link`, inside the `blockroll/blogroll` +block, so the whole blogroll travels with the post content. A link block holds `url`, +`name`, `description`, `feedUrl`, `photo`, an `xfn` array, and the date it was `added`. +It has no output of its own: the blogroll renders the whole list on the server, because +sorting and paging reorder and cut it. Blogrolls saved before 1.3 kept their links in a +`links` attribute of the blogroll block. The server still reads that attribute when a +block has no link blocks, so such a post renders without being opened; the editor +migrates it to link blocks when it is opened (a block deprecation, see +`src/blogroll/deprecated.js`). Every entry is marked up as an [h-card](https://microformats.org/wiki/h-card) with [XFN](https://gmpg.org/xfn/) relationships on the link, in an diff --git a/readme.md b/readme.md index 5349c75..e5d8b2b 100644 --- a/readme.md +++ b/readme.md @@ -66,8 +66,10 @@ this version get their anchor the first time the page is viewed. ### How do I add a link? Paste the address of the site and the plugin fills in the rest: name, description, icon, -and the feed, if it finds one. Everything stays editable, and links can be reordered by -dragging. +and the feed, if it finds one. Every link is a block of its own inside the list, so it +can be moved, duplicated, copied to another page and edited right where it is: name and +description in place, the address behind the name, the feed and the relationship in the +line below, the icon by clicking it. ### Can I import the subscriptions from my feed reader? From f9839158e99fbe9b60d5e02c231b91b674436307 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:43:39 +0200 Subject: [PATCH 06/23] no autocomplete on the address field --- build/blogroll/index.asset.php | 2 +- build/blogroll/index.js | 2 +- src/blogroll/components/add-link.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 0c1676a..2dfbdf8 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'a4b8bf32f8b8c9b1a924'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '5b958431e9418addfb3a'); diff --git a/build/blogroll/index.js b/build/blogroll/index.js index b477074..976f316 100644 --- a/build/blogroll/index.js +++ b/build/blogroll/index.js @@ -1,4 +1,4 @@ -(()=>{"use strict";var e={704(e,l,o){const t=window.wp.blocks,r=window.wp.blockEditor,n=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function m(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function b(e){return{url:e?.url||"",name:e?.name||"",description:e?.description||"",feedUrl:e?.feedUrl||"",photo:e?.photo||"",xfn:Array.isArray(e?.xfn)?e.xfn:[],added:e?.added||""}}const k=window.ReactJSXRuntime;function _({anchor:e,onAdd:l,onClose:o}){const[t,r]=(0,a.useState)(""),[n,i]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,k.jsx)(c.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,k.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!n&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(i(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(m({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),i(!1)}))})()},children:[d&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,k.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",spellCheck:!1,value:t,disabled:n,onChange:e=>{r(e),h(null)}}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:n,disabled:!p||n,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const f=window.wp.primitives;var x=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function g({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),n(e.name))},y={file:(0,k.jsxs)("div",{className:"blockroll-import-upload",children:[(0,k.jsx)(c.DropZone,{onFilesDrop:e=>j(e[0])}),(0,k.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:r||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,k.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),n("")}}),url:(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,k.jsx)(c.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,k.jsxs)("div",{className:"blockroll-form",children:[v&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:v}),(0,k.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,k.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,k.jsx)("div",{className:"blockroll-import-tab",children:y[e.name]})}),(0,k.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,k.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,k.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ +(()=>{"use strict";var e={704(e,l,o){const t=window.wp.blocks,r=window.wp.blockEditor,n=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function m(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function b(e){return{url:e?.url||"",name:e?.name||"",description:e?.description||"",feedUrl:e?.feedUrl||"",photo:e?.photo||"",xfn:Array.isArray(e?.xfn)?e.xfn:[],added:e?.added||""}}const k=window.ReactJSXRuntime;function _({anchor:e,onAdd:l,onClose:o}){const[t,r]=(0,a.useState)(""),[n,i]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,k.jsx)(c.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,k.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!n&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(i(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(m({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),i(!1)}))})()},children:[d&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,k.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",autoComplete:"off",spellCheck:!1,value:t,disabled:n,onChange:e=>{r(e),h(null)}}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:n,disabled:!p||n,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const f=window.wp.primitives;var x=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function g({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),n(e.name))},y={file:(0,k.jsxs)("div",{className:"blockroll-import-upload",children:[(0,k.jsx)(c.DropZone,{onFilesDrop:e=>j(e[0])}),(0,k.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:r||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,k.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),n("")}}),url:(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,k.jsx)(c.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,k.jsxs)("div",{className:"blockroll-form",children:[v&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:v}),(0,k.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,k.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,k.jsx)("div",{className:"blockroll-import-tab",children:y[e.name]})}),(0,k.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,k.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,k.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ (0,s.__)("%d links processed…","blockroll"),f)}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:b,disabled:!o&&!i||b,onClick:async()=>{_(!0),w(null);try{const t=o?{opml:o}:{url:i},{links:r}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),n=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(m(t,e))}catch{o.push(t)}l(o.length)}return o}(r,g):r;e(n),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),_(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const v=window.wp.url;function w(e){return(0,v.cleanForSlug)(e)||"blogroll"}function j(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const y=[{attributes:n.uK,supports:n.xY,save:()=>null,isEligible:(e,l)=>function(e){return Array.isArray(e?.links)&&e.links.length>0}(e)&&!l?.length,migrate:(e,l)=>{const[o,r]=function(e){return[{...e,links:[]},(e.links||[]).filter(e=>e?.url).map(b)]}(e);return[o,[...l||[],...r.map(e=>(0,t.createBlock)("blockroll/link",e))]]}}],S=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(n.UU,{icon:S,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:n}){const{anchor:d,source:m,sortBy:f,perPage:x,showAvatars:v,showSort:y,showOpml:S,metadata:C}=e,B=C?.name||"",N=(0,i.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:A}=(0,i.useDispatch)(r.store),M=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(r.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return A(),void l({anchor:j(w(B),M())});if(n)return;const e=M();e.includes(d)&&(A(),l({anchor:j(d,e)}))},[d,n]);const O=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(O.current===B)return;const e=!d||function(e,l){const o=w(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,O.current);O.current=B,e&&l({anchor:j(w(B),M())})},[B]);const H=(0,i.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(r.store);if(!d||n)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,n]),{createWarningNotice:P,removeNotice:T}=(0,i.useDispatch)(h.store),D=(0,a.useRef)(null);(0,a.useEffect)(()=>{D.current&&(T(D.current),D.current=null),H&&(D.current=`blockroll-anchor-${d}`,P((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ (0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:D.current,isDismissible:!0,__unstableHTML:!0}))},[H,d]),(0,a.useEffect)(()=>()=>{D.current&&T(D.current)},[]);const[I,L]=(0,a.useState)(!1),[U,z]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[W,G]=(0,a.useState)(!1),J="manual",K=U.some(e=>m===e.value),Y=!m||W&&!K?J:m,q=JSON.stringify(e),X=U.filter(e=>J!==e.value),Q=U.find(e=>Y===e.value)||U[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(z(e),G(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return J===Y?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${Y}/links?attributes=`+encodeURIComponent(q)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[Y,q]);const ee=(0,i.useSelect)(e=>e(r.store).getBlockCount(o),[o]),{insertBlock:le,insertBlocks:oe}=(0,i.useDispatch)(r.store),[te,re]=(0,a.useState)(!1),[ne,se]=(0,a.useState)(),ae=(0,r.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ie=(0,k.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,k.jsx)(c.Button,{variant:"primary",ref:se,"aria-expanded":te,onClick:()=>re(!te),children:(0,s.__)("Add link","blockroll")}),te&&(0,k.jsx)(_,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),re(!1)},onClose:()=>re(!1)}),(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>L(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&J===Y&&X.map(e=>(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ (0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;J!==Y||ee||(ce=(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ie}));const de=(0,k.jsx)("div",{className:"blockroll-editor-actions",children:(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:J}),children:(0,s.__)("Use manual links","blockroll")})});return(0,k.jsxs)("div",{...(0,r.useBlockProps)(),children:[(0,k.jsx)(r.InspectorControls,{children:(0,k.jsxs)(c.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ diff --git a/src/blogroll/components/add-link.js b/src/blogroll/components/add-link.js index 152ea41..a4f32fd 100644 --- a/src/blogroll/components/add-link.js +++ b/src/blogroll/components/add-link.js @@ -83,6 +83,7 @@ export default function AddLink( { anchor, onAdd, onClose } ) { placeholder="example.com" type="text" inputMode="url" + autoComplete="off" spellCheck={ false } value={ input } disabled={ isBusy } From 0c5507280b030ab68e4b9e5de9de98a4a125103b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 21 Sep 2026 18:45:46 +0200 Subject: [PATCH 07/23] do not add a site twice --- build/blogroll/index.asset.php | 2 +- build/blogroll/index.js | 10 +++++----- src/blogroll/__tests__/same-site.test.js | 21 +++++++++++++++++++++ src/blogroll/components/add-link.js | 19 +++++++++++++++++-- src/blogroll/edit.js | 11 ++++++++++- src/blogroll/utils.js | 19 +++++++++++++++++++ 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 src/blogroll/__tests__/same-site.test.js diff --git a/build/blogroll/index.asset.php b/build/blogroll/index.asset.php index 2dfbdf8..45fc288 100644 --- a/build/blogroll/index.asset.php +++ b/build/blogroll/index.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '5b958431e9418addfb3a'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '7d36f51929a1d530591b'); diff --git a/build/blogroll/index.js b/build/blogroll/index.js index 976f316..fb84f86 100644 --- a/build/blogroll/index.js +++ b/build/blogroll/index.js @@ -1,6 +1,6 @@ -(()=>{"use strict";var e={704(e,l,o){const t=window.wp.blocks,r=window.wp.blockEditor,n=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function m(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function b(e){return{url:e?.url||"",name:e?.name||"",description:e?.description||"",feedUrl:e?.feedUrl||"",photo:e?.photo||"",xfn:Array.isArray(e?.xfn)?e.xfn:[],added:e?.added||""}}const k=window.ReactJSXRuntime;function _({anchor:e,onAdd:l,onClose:o}){const[t,r]=(0,a.useState)(""),[n,i]=(0,a.useState)(!1),[d,h]=(0,a.useState)(null),p=t.trim();return(0,k.jsx)(c.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:o,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,k.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),p&&!n&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(p);d?l({url:e}):(i(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(m({url:e},o))).catch(e=>{h(e.message||(0,s.__)("The site could not be reached.","blockroll")),i(!1)}))})()},children:[d&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:d}),(0,k.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",autoComplete:"off",spellCheck:!1,value:t,disabled:n,onChange:e=>{r(e),h(null)}}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:n,disabled:!p||n,children:d?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const f=window.wp.primitives;var x=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function g({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),n(e.name))},y={file:(0,k.jsxs)("div",{className:"blockroll-import-upload",children:[(0,k.jsx)(c.DropZone,{onFilesDrop:e=>j(e[0])}),(0,k.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:r||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,k.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),n("")}}),url:(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,k.jsx)(c.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,k.jsxs)("div",{className:"blockroll-form",children:[v&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:v}),(0,k.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,k.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,k.jsx)("div",{className:"blockroll-import-tab",children:y[e.name]})}),(0,k.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,k.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,k.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ -(0,s.__)("%d links processed…","blockroll"),f)}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:b,disabled:!o&&!i||b,onClick:async()=>{_(!0),w(null);try{const t=o?{opml:o}:{url:i},{links:r}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),n=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(m(t,e))}catch{o.push(t)}l(o.length)}return o}(r,g):r;e(n),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),_(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const v=window.wp.url;function w(e){return(0,v.cleanForSlug)(e)||"blogroll"}function j(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const y=[{attributes:n.uK,supports:n.xY,save:()=>null,isEligible:(e,l)=>function(e){return Array.isArray(e?.links)&&e.links.length>0}(e)&&!l?.length,migrate:(e,l)=>{const[o,r]=function(e){return[{...e,links:[]},(e.links||[]).filter(e=>e?.url).map(b)]}(e);return[o,[...l||[],...r.map(e=>(0,t.createBlock)("blockroll/link",e))]]}}],S=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(n.UU,{icon:S,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:n}){const{anchor:d,source:m,sortBy:f,perPage:x,showAvatars:v,showSort:y,showOpml:S,metadata:C}=e,B=C?.name||"",N=(0,i.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:A}=(0,i.useDispatch)(r.store),M=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(r.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return A(),void l({anchor:j(w(B),M())});if(n)return;const e=M();e.includes(d)&&(A(),l({anchor:j(d,e)}))},[d,n]);const O=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(O.current===B)return;const e=!d||function(e,l){const o=w(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,O.current);O.current=B,e&&l({anchor:j(w(B),M())})},[B]);const H=(0,i.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(r.store);if(!d||n)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,n]),{createWarningNotice:P,removeNotice:T}=(0,i.useDispatch)(h.store),D=(0,a.useRef)(null);(0,a.useEffect)(()=>{D.current&&(T(D.current),D.current=null),H&&(D.current=`blockroll-anchor-${d}`,P((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ -(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:D.current,isDismissible:!0,__unstableHTML:!0}))},[H,d]),(0,a.useEffect)(()=>()=>{D.current&&T(D.current)},[]);const[I,L]=(0,a.useState)(!1),[U,z]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[W,G]=(0,a.useState)(!1),J="manual",K=U.some(e=>m===e.value),Y=!m||W&&!K?J:m,q=JSON.stringify(e),X=U.filter(e=>J!==e.value),Q=U.find(e=>Y===e.value)||U[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(z(e),G(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return J===Y?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${Y}/links?attributes=`+encodeURIComponent(q)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[Y,q]);const ee=(0,i.useSelect)(e=>e(r.store).getBlockCount(o),[o]),{insertBlock:le,insertBlocks:oe}=(0,i.useDispatch)(r.store),[te,re]=(0,a.useState)(!1),[ne,se]=(0,a.useState)(),ae=(0,r.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ie=(0,k.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,k.jsx)(c.Button,{variant:"primary",ref:se,"aria-expanded":te,onClick:()=>re(!te),children:(0,s.__)("Add link","blockroll")}),te&&(0,k.jsx)(_,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),re(!1)},onClose:()=>re(!1)}),(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>L(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&J===Y&&X.map(e=>(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ -(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;J!==Y||ee||(ce=(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ie}));const de=(0,k.jsx)("div",{className:"blockroll-editor-actions",children:(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:J}),children:(0,s.__)("Use manual links","blockroll")})});return(0,k.jsxs)("div",{...(0,r.useBlockProps)(),children:[(0,k.jsx)(r.InspectorControls,{children:(0,k.jsxs)(c.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ -(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),d||w(B)),{code:(0,k.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:B,onChange:e=>{const o={...C};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,k.jsx)(c.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:f,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,k.jsx)(c.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:x,onChange:e=>l({perPage:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:v,onChange:e=>l({showAvatars:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:y,onChange:e=>l({showSort:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:S,onChange:e=>l({showOpml:e})})]})}),I&&(0,k.jsx)(g,{onImport:e=>{const n=new Set(N.select(r.store).getBlock(o).innerBlocks.map(e=>e.attributes.url)),s=e.filter(e=>e.url&&!n.has(e.url)).map(e=>(0,t.createBlock)("blockroll/link",b(e)));J!==Y&&l({source:J}),s.length&&oe(s,void 0,o)},onClose:()=>L(!1)}),J!==Y?(0,k.jsxs)("div",{className:"blockroll-editor",children:[(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:Q.label,instructions:F?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ +(()=>{"use strict";var e={704(e,l,o){const t=window.wp.blocks,r=window.wp.blockEditor,n=JSON.parse('{"UU":"blockroll/blogroll","xY":{"html":false,"align":true,"anchor":true,"color":{"text":true,"background":true,"link":true,"gradients":true},"spacing":{"margin":true,"padding":true},"typography":{"fontSize":true,"lineHeight":true}},"uK":{"anchor":{"type":"string"},"links":{"type":"array","default":[]},"source":{"type":"string","default":"manual"},"sortBy":{"type":"string","default":"name","enum":["name","added","manual"]},"perPage":{"type":"number","default":0},"showAvatars":{"type":"boolean","default":true},"showSort":{"type":"boolean","default":true},"showOpml":{"type":"boolean","default":true}}}'),s=window.wp.i18n,a=window.wp.element,i=window.wp.data,c=window.wp.components,d=window.wp.apiFetch;var u=o.n(d);const h=window.wp.notices,p=window.wp.escapeHtml;function m(e,l){return{...e,name:e.name||l.name,description:e.description||l.description,feedUrl:e.feedUrl||l.feedUrl,photo:e.photo||l.photo}}function b(e){return{url:e?.url||"",name:e?.name||"",description:e?.description||"",feedUrl:e?.feedUrl||"",photo:e?.photo||"",xfn:Array.isArray(e?.xfn)?e.xfn:[],added:e?.added||""}}const k=window.ReactJSXRuntime;function _({anchor:e,onAdd:l,isKnown:o,onClose:t}){const[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(!1),[h,p]=(0,a.useState)(null),[b,_]=(0,a.useState)(!1),f=r.trim();return(0,k.jsx)(c.Popover,{anchor:e,placement:"bottom-start",offset:8,onClose:t,focusOnMount:"firstElement",className:"blockroll-add-link",children:(0,k.jsxs)("form",{className:"blockroll-add-link__form",onSubmit:e=>{e.preventDefault(),f&&!i&&(()=>{const e=function(e){const l=e.trim();return/^[a-z][a-z0-9+.-]*:/i.test(l)?l:`https://${l}`}(f);o?.(e)?_(!0):h?l({url:e}):(d(!0),u()({path:"/blockroll/v1/discover",method:"POST",data:{url:e}}).then(o=>l(m({url:e},o))).catch(e=>{p(e.message||(0,s.__)("The site could not be reached.","blockroll")),d(!1)}))})()},children:[h&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:h}),b&&(0,k.jsx)(c.Notice,{status:"warning",isDismissible:!1,children:(0,s.__)("This site is in the list already.","blockroll")}),(0,k.jsxs)("div",{className:"blockroll-add-link__row",children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,placeholder:"example.com",type:"text",inputMode:"url",autoComplete:"off",spellCheck:!1,value:r,disabled:i,onChange:e=>{n(e),p(null),_(!1)}}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",type:"submit",isBusy:i,disabled:!f||i||b,children:h?(0,s.__)("Add anyway","blockroll"):(0,s.__)("Add","blockroll")})]})]})})}const f=window.wp.primitives;var x=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18.5 15v3.5H13V6.7l4.5 4.1 1-1.1-6.2-5.8-5.8 5.8 1 1.1 4-4v11.7h-6V15H4v5h16v-5z"})});function g({onImport:e,onClose:l}){const[o,t]=(0,a.useState)(""),[r,n]=(0,a.useState)(""),[i,d]=(0,a.useState)(""),[h,p]=(0,a.useState)(!1),[b,_]=(0,a.useState)(!1),[f,g]=(0,a.useState)(null),[v,w]=(0,a.useState)(null),j=e=>{e&&(e.text().then(t),n(e.name))},y={file:(0,k.jsxs)("div",{className:"blockroll-import-upload",children:[(0,k.jsx)(c.DropZone,{onFilesDrop:e=>j(e[0])}),(0,k.jsx)(c.FormFileUpload,{__next40pxDefaultSize:!0,accept:".opml,.xml,text/xml,text/x-opml",icon:x,variant:"secondary",onChange:e=>j(e.target.files?.[0]),children:r||(0,s.__)("Choose or drop a file","blockroll")})]}),paste:(0,k.jsx)(c.TextareaControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("File content","blockroll"),hideLabelFromVision:!0,rows:6,value:o,onChange:e=>{t(e),n("")}}),url:(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Address","blockroll"),hideLabelFromVision:!0,type:"url",placeholder:"https://example.com/subscriptions.opml",value:i,onChange:d})};return(0,k.jsx)(c.Modal,{title:(0,s.__)("Import links","blockroll"),size:"medium",onRequestClose:l,focusOnMount:"firstContentElement",children:(0,k.jsxs)("div",{className:"blockroll-form",children:[v&&(0,k.jsx)(c.Notice,{status:"error",isDismissible:!1,children:v}),(0,k.jsx)("p",{className:"blockroll-import-intro",children:(0,s.__)("Import the subscription list of your feed reader (an OPML file).","blockroll")}),(0,k.jsx)(c.TabPanel,{tabs:[{name:"file",title:(0,s.__)("Upload","blockroll")},{name:"paste",title:(0,s.__)("Paste","blockroll")},{name:"url",title:(0,s.__)("Address","blockroll")}],children:e=>(0,k.jsx)("div",{className:"blockroll-import-tab",children:y[e.name]})}),(0,k.jsx)(c.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Fetch details for imported links","blockroll"),help:(0,s.__)("Looks up names, descriptions, and images. Takes a moment for long lists.","blockroll"),checked:h,onChange:p}),(0,k.jsxs)("div",{className:"blockroll-form__footer",children:[null!==f&&(0,k.jsx)("span",{children:(0,s.sprintf)(/* translators: %d: number of links processed. */ /* translators: %d: number of links processed. */ +(0,s.__)("%d links processed…","blockroll"),f)}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"tertiary",onClick:l,children:(0,s.__)("Cancel","blockroll")}),(0,k.jsx)(c.Button,{__next40pxDefaultSize:!0,variant:"primary",isBusy:b,disabled:!o&&!i||b,onClick:async()=>{_(!0),w(null);try{const t=o?{opml:o}:{url:i},{links:r}=await u()({path:"/blockroll/v1/import",method:"POST",data:t}),n=h?await async function(e,l){const o=[];for(const t of e){try{const e=await u()({path:"/blockroll/v1/discover",method:"POST",data:{url:t.url}});o.push(m(t,e))}catch{o.push(t)}l(o.length)}return o}(r,g):r;e(n),l()}catch(e){w(e.message||(0,s.__)("The file could not be imported.","blockroll")),_(!1),g(null)}},children:(0,s.__)("Import","blockroll")})]})]})})}const v=window.wp.url;function w(e){return(0,v.cleanForSlug)(e)||"blogroll"}function j(e,l){let o=e,t=0;for(;l.includes(o);)t+=1,o=`${e}-${t}`;return o}const y=[{attributes:n.uK,supports:n.xY,save:()=>null,isEligible:(e,l)=>function(e){return Array.isArray(e?.links)&&e.links.length>0}(e)&&!l?.length,migrate:(e,l)=>{const[o,r]=function(e){return[{...e,links:[]},(e.links||[]).filter(e=>e?.url).map(b)]}(e);return[o,[...l||[],...r.map(e=>(0,t.createBlock)("blockroll/link",e))]]}}],S=(0,k.jsx)(f.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",children:(0,k.jsx)(f.Path,{d:"M18 5.5H6a.5.5 0 0 0-.5.5v12a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V6a.5.5 0 0 0-.5-.5ZM6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 5h1.5v1.5H7V9Zm1.5 4.5H7V15h1.5v-1.5ZM10 9h7v1.5h-7V9Zm7 4.5h-7V15h7v-1.5Z"})});(0,t.registerBlockType)(n.UU,{icon:S,edit:function({attributes:e,setAttributes:l,clientId:o,isSelected:n}){const{anchor:d,source:m,sortBy:f,perPage:x,showAvatars:v,showSort:y,showOpml:S,metadata:C}=e,B=C?.name||"",N=(0,i.useRegistry)(),{__unstableMarkNextChangeAsNotPersistent:A}=(0,i.useDispatch)(r.store),M=()=>{const{getClientIdsWithDescendants:e,getBlockAttributes:l}=N.select(r.store);return e().filter(e=>e!==o).map(e=>l(e)?.anchor).filter(Boolean)};(0,a.useEffect)(()=>{if(!d)return A(),void l({anchor:j(w(B),M())});if(n)return;const e=M();e.includes(d)&&(A(),l({anchor:j(d,e)}))},[d,n]);const O=(0,a.useRef)(B);(0,a.useEffect)(()=>{if(O.current===B)return;const e=!d||function(e,l){const o=w(l);return e===o||e.startsWith(o+"-")&&/^\d+$/.test(e.slice(o.length+1))}(d,O.current);O.current=B,e&&l({anchor:j(w(B),M())})},[B]);const T=(0,i.useSelect)(e=>{const{getClientIdsWithDescendants:l,getBlockAttributes:t,getSelectedBlockClientId:s}=e(r.store);if(!d||n)return!1;const a=s();return l().some(e=>e!==o&&e!==a&&t(e)?.anchor===d)},[o,d,n]),{createWarningNotice:D,removeNotice:H}=(0,i.useDispatch)(h.store),P=(0,a.useRef)(null);(0,a.useEffect)(()=>{P.current&&(H(P.current),P.current=null),T&&(P.current=`blockroll-anchor-${d}`,D((0,s.sprintf)(/* translators: %s: the anchor of the blocks */ /* translators: %s: the anchor of the blocks */ +(0,s.__)("A Blogroll block and another block on this page both use the anchor #%s. Change the HTML anchor under Advanced, so that each block has its own.","blockroll"),(0,p.escapeHTML)(d)),{id:P.current,isDismissible:!0,__unstableHTML:!0}))},[T,d]),(0,a.useEffect)(()=>()=>{P.current&&H(P.current)},[]);const[L,z]=(0,a.useState)(!1),[I,U]=(0,a.useState)([{label:(0,s.__)("Manual links","blockroll"),value:"manual"}]),[V,E]=(0,a.useState)([]),[F,R]=(0,a.useState)(!1),[Z,$]=(0,a.useState)(null),[K,W]=(0,a.useState)(!1),G="manual",J=I.some(e=>m===e.value),Y=!m||K&&!J?G:m,q=JSON.stringify(e),X=I.filter(e=>G!==e.value),Q=I.find(e=>Y===e.value)||I[0];(0,a.useEffect)(()=>{u()({path:"/blockroll/v1/sources"}).then(e=>{Array.isArray(e)&&(U(e),W(!0))}).catch(()=>{})},[]),(0,a.useEffect)(()=>{let e=!0;return G===Y?(E([]),$(null),R(!1),()=>{e=!1}):(R(!0),$(null),u()({path:`/blockroll/v1/sources/${Y}/links?attributes=`+encodeURIComponent(q)}).then(l=>{e&&E(Array.isArray(l)?l:[])}).catch(l=>{e&&(E([]),$(l.message||(0,s.__)("The source preview could not be loaded.","blockroll")))}).finally(()=>{e&&R(!1)}),()=>{e=!1})},[Y,q]);const ee=(0,i.useSelect)(e=>e(r.store).getBlockCount(o),[o]),{insertBlock:le,insertBlocks:oe}=(0,i.useDispatch)(r.store),[te,re]=(0,a.useState)(!1),[ne,se]=(0,a.useState)(),ae=(0,r.useInnerBlocksProps)({className:"blockroll-editor-links"},{allowedBlocks:["blockroll/link"],templateLock:!1,renderAppender:!1}),ie=(0,k.jsxs)("div",{className:"blockroll-editor-actions",children:[(0,k.jsx)(c.Button,{variant:"primary",ref:se,"aria-expanded":te,onClick:()=>re(!te),children:(0,s.__)("Add link","blockroll")}),te&&(0,k.jsx)(_,{anchor:ne,onAdd:e=>{le((0,t.createBlock)("blockroll/link",{...e,added:(new Date).toISOString().slice(0,10)}),void 0,o),re(!1)},isKnown:e=>N.select(r.store).getBlock(o).innerBlocks.some(l=>function(e,l){const o=e=>String(e||"").trim().toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//,"").replace(/^www\./,"").replace(/\/+$/,"");return""!==o(e)&&o(e)===o(l)}(l.attributes.url,e)),onClose:()=>re(!1)}),(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>z(!0),children:(0,s.__)("Import links","blockroll")}),!ee&&G===Y&&X.map(e=>(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:e.value}),children:(0,s.sprintf)(/* translators: %s: Source name. */ /* translators: %s: Source name. */ +(0,s.__)("Use %s","blockroll"),e.label)},e.value))]});let ce=null;G!==Y||ee||(ce=(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:(0,s.__)("Blogroll","blockroll"),instructions:(0,s.__)("Share a list of the blogs and sites you follow.","blockroll"),children:ie}));const de=(0,k.jsx)("div",{className:"blockroll-editor-actions",children:(0,k.jsx)(c.Button,{variant:"secondary",onClick:()=>l({source:G}),children:(0,s.__)("Use manual links","blockroll")})});return(0,k.jsxs)("div",{...(0,r.useBlockProps)(),children:[(0,k.jsx)(r.InspectorControls,{children:(0,k.jsxs)(c.PanelBody,{title:(0,s.__)("Blogroll settings","blockroll"),children:[(0,k.jsx)(c.TextControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Name","blockroll"),help:(0,a.createInterpolateElement)((0,s.sprintf)(/* translators: %s: the anchor of the block */ /* translators: %s: the anchor of the block */ +(0,s.__)("Groups this list when a page has more than one blogroll, and sets its anchor: #%s. Renaming the block does the same. The HTML anchor can be changed under Advanced.","blockroll"),d||w(B)),{code:(0,k.jsx)("code",{})}),placeholder:(0,s.__)("Blogroll","blockroll"),value:B,onChange:e=>{const o={...C};e?o.name=e:delete o.name,l({metadata:Object.keys(o).length?o:void 0})}}),(0,k.jsx)(c.SelectControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Sort by","blockroll"),value:f,options:[{label:(0,s.__)("Name","blockroll"),value:"name"},{label:(0,s.__)("Newest first","blockroll"),value:"added"},{label:(0,s.__)("List order","blockroll"),value:"manual"}],onChange:e=>l({sortBy:e})}),(0,k.jsx)(c.RangeControl,{__next40pxDefaultSize:!0,__nextHasNoMarginBottom:!0,label:(0,s.__)("Links per page","blockroll"),help:(0,s.__)("0 shows all links on one page.","blockroll"),min:0,max:50,value:x,onChange:e=>l({perPage:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show images","blockroll"),checked:v,onChange:e=>l({showAvatars:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Let visitors sort the list","blockroll"),checked:y,onChange:e=>l({showSort:e})}),(0,k.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,s.__)("Show download link","blockroll"),help:(0,s.__)("Links to the list as an OPML file.","blockroll"),checked:S,onChange:e=>l({showOpml:e})})]})}),L&&(0,k.jsx)(g,{onImport:e=>{const n=new Set(N.select(r.store).getBlock(o).innerBlocks.map(e=>e.attributes.url)),s=e.filter(e=>e.url&&!n.has(e.url)).map(e=>(0,t.createBlock)("blockroll/link",b(e)));G!==Y&&l({source:G}),s.length&&oe(s,void 0,o)},onClose:()=>z(!1)}),G!==Y?(0,k.jsxs)("div",{className:"blockroll-editor",children:[(0,k.jsx)(c.Placeholder,{icon:"admin-links",label:Q.label,instructions:F?(0,s.__)("Loading source preview…","blockroll"):(0,s.sprintf)(/* translators: %d: Number of previewed links. */ /* translators: %d: Number of previewed links. */ (0,s.__)("%d links will be shown when the block is rendered.","blockroll"),V.length),children:de}),Q.help&&(0,k.jsxs)("p",{children:[Q.help,Q.helpUrl&&(0,k.jsxs)(k.Fragment,{children:[" ",(0,k.jsx)("a",{href:Q.helpUrl,children:(0,s.__)("Manage source","blockroll")})]})]}),Z&&(0,k.jsx)("p",{children:Z}),!Z&&!F&&!!V.length&&(ue=V,(0,k.jsx)("ul",{className:"blockroll-editor-list",children:ue.map(e=>(0,k.jsxs)("li",{children:[v&&(e.photo?(0,k.jsx)("img",{src:e.photo,alt:"",width:"32",height:"32"}):(0,k.jsx)("span",{className:"blockroll-editor-list__no-photo"})),(0,k.jsxs)("span",{className:"blockroll-editor-list__text",children:[(0,k.jsx)("strong",{children:e.name||e.url}),(0,k.jsxs)("small",{children:[e.url,e.xfn?.length>0&&" · "+e.xfn.join(" ")]})]})]},e.url))}))]}):(0,k.jsxs)("div",{className:"blockroll-editor",children:[ce,(0,k.jsx)("div",{...ae}),!ce&&ie]})]});var ue},save:()=>(0,k.jsx)(r.InnerBlocks.Content,{}),deprecated:y})}};const l={};function o(t){const r=l[t];if(void 0!==r)return r.exports;const n=l[t]={exports:{}};return e[t](n,n.exports,o),n.exports}o.m=e,(()=>{const e=[];o.O=(l,t,r,n)=>{if(t){n=n||0;for(var s=e.length;s>0&&e[s-1][2]>n;s--)e[s]=e[s-1];return void(e[s]=[t,r,n])}let a=1/0;for(s=0;s=n)&&Object.keys(o.O).every(e=>o.O[e](t[i]))?t.splice(i--,1):(c=!1,n{const l=e&&e.__esModule?()=>e.default:()=>e;return o.d(l,{a:l}),l},o.d=(e,l)=>{if(Array.isArray(l))for(var t=0;tObject.hasOwn(e,l),(()=>{const e={39:0,375:0};o.O.j=l=>0===e[l];const l=(l,t)=>{let[r,n,s]=t;var a,i,c=0;if(r.some(l=>0!==e[l])){for(a in n)o.o(n,a)&&(o.m[a]=n[a]);if(s)var d=s(o)}for(l&&l(t);co(704));t=o.O(t)})(); \ No newline at end of file diff --git a/src/blogroll/__tests__/same-site.test.js b/src/blogroll/__tests__/same-site.test.js new file mode 100644 index 0000000..4a02bb7 --- /dev/null +++ b/src/blogroll/__tests__/same-site.test.js @@ -0,0 +1,21 @@ +import { sameSite } from '../utils'; + +describe( 'sameSite', () => { + it( 'ignores scheme, www, case and the trailing slash', () => { + expect( + sameSite( 'https://notiz.blog/', 'http://www.Notiz.blog' ) + ).toBe( true ); + expect( sameSite( 'notiz.blog', 'https://notiz.blog/' ) ).toBe( true ); + } ); + + it( 'keeps paths apart', () => { + expect( + sameSite( 'https://a.example/blog', 'https://a.example/' ) + ).toBe( false ); + } ); + + it( 'never matches an empty address', () => { + expect( sameSite( '', '' ) ).toBe( false ); + expect( sameSite( undefined, 'https://a.example/' ) ).toBe( false ); + } ); +} ); diff --git a/src/blogroll/components/add-link.js b/src/blogroll/components/add-link.js index a4f32fd..166bb9d 100644 --- a/src/blogroll/components/add-link.js +++ b/src/blogroll/components/add-link.js @@ -21,16 +21,22 @@ import { mergeDiscovered, toUrl } from '../utils'; * @param {Object} props Component props. * @param {Element} props.anchor Element the popover is anchored at. * @param {Function} props.onAdd Called with the attributes of the new link. + * @param {Function} props.isKnown Whether an address is in the list already. * @param {Function} props.onClose Called when the overlay closes. */ -export default function AddLink( { anchor, onAdd, onClose } ) { +export default function AddLink( { anchor, onAdd, isKnown, onClose } ) { const [ input, setInput ] = useState( '' ); const [ isBusy, setIsBusy ] = useState( false ); const [ error, setError ] = useState( null ); + const [ isDuplicate, setIsDuplicate ] = useState( false ); const value = input.trim(); const add = () => { const url = toUrl( value ); + if ( isKnown?.( url ) ) { + setIsDuplicate( true ); + return; + } if ( error ) { onAdd( { url } ); return; @@ -74,6 +80,14 @@ export default function AddLink( { anchor, onAdd, onClose } ) { { error } ) } + { isDuplicate && ( + + { __( + 'This site is in the list already.', + 'blockroll' + ) } + + ) }
{ setInput( next ); setError( null ); + setIsDuplicate( false ); } } />