From 2ddc79ca04dbe43c96b23b9f26f8b3caa6deec1f Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Wed, 25 May 2022 15:06:02 +1200 Subject: [PATCH] NEW Get anchor links from elemental pages Co-Authored-By: Steve Boyd --- composer.json | 2 +- src/Extensions/ElementalPageExtension.php | 55 +++++++++++----- src/Models/BaseElement.php | 36 ++++++++++- tests/BaseElementTest.php | 34 ++++++++++ tests/Behat/Context/FixtureContext.php | 2 +- .../Behat/features/add-link-to-anchor.feature | 62 +++++++++++++++++++ tests/ElementalAreaDataObjectTest.yml | 11 +++- tests/Src/TestMultiHtmlFieldsElement.php | 18 ++++++ 8 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 tests/Behat/features/add-link-to-anchor.feature create mode 100644 tests/Src/TestMultiHtmlFieldsElement.php diff --git a/composer.json b/composer.json index f1c4b023..80604b18 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ }, "require-dev": { "silverstripe/recipe-testing": "^2", - "silverstripe/frameworktest": "4.x-dev" + "silverstripe/frameworktest": "^0.4.5" }, "suggest": { "silverstripe/elemental-blocks": "Adds a set of common SilverStripe content block types", diff --git a/src/Extensions/ElementalPageExtension.php b/src/Extensions/ElementalPageExtension.php index dee8010b..d9c750c9 100644 --- a/src/Extensions/ElementalPageExtension.php +++ b/src/Extensions/ElementalPageExtension.php @@ -2,7 +2,9 @@ namespace DNADesign\Elemental\Extensions; +use DNADesign\Elemental\Models\BaseElement; use DNADesign\Elemental\Models\ElementalArea; +use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Controller; use SilverStripe\View\Parsers\HTML4Value; use SilverStripe\View\SSViewer; @@ -47,23 +49,14 @@ public function getElementsForSearch() SSViewer::set_themes(SSViewer::config()->get('themes')); try { $output = []; - foreach ($this->owner->hasOne() as $key => $class) { - if ($class !== ElementalArea::class) { - continue; - } - /** @var ElementalArea $area */ - $area = $this->owner->$key(); - if ($area) { - foreach ($area->Elements() as $element) { - if ($element->getSearchIndexable()) { - $content = $element->getContentForSearchIndex(); - if ($content) { - $output[] = $content; - } - } + $this->loopThroughElements(function (BaseElement $element) use (&$output) { + if ($element->getSearchIndexable()) { + $content = $element->getContentForSearchIndex(); + if ($content) { + $output[] = $content; } } - } + }); } finally { // Reset theme if an exception occurs, if you don't have a // try / finally around code that might throw an Exception, @@ -73,6 +66,19 @@ public function getElementsForSearch() return implode($this->owner->config()->get('search_index_element_delimiter') ?? '', $output); } + /** + * @see SiteTree::getAnchorsOnPage() + */ + public function updateAnchorsOnPage(array &$anchors): void + { + if (!($this->owner instanceof SiteTree)) { + return; + } + $this->loopThroughElements(function (BaseElement $element) use (&$anchors) { + $anchors = array_merge($anchors, $element->getAnchorsInContent()); + }); + } + public function MetaTags(&$tags) { if (!Controller::has_curr()) { @@ -91,4 +97,23 @@ public function MetaTags(&$tags) $tags = $html->getContent(); } } + + /** + * Call some function over all elements belonging to this page + */ + private function loopThroughElements(callable $callback): void + { + foreach ($this->owner->hasOne() as $key => $class) { + if ($class !== ElementalArea::class) { + continue; + } + /** @var ElementalArea $area */ + $area = $this->owner->$key(); + if ($area) { + foreach ($area->Elements() as $element) { + $callback($element); + } + } + } + } } diff --git a/src/Models/BaseElement.php b/src/Models/BaseElement.php index 519fe913..1b43cb66 100644 --- a/src/Models/BaseElement.php +++ b/src/Models/BaseElement.php @@ -257,7 +257,7 @@ public function canEdit($member = null) public function canDelete($member = null) { $extended = $this->extendedCan(__FUNCTION__, $member); - + if ($extended !== null) { return $extended; } @@ -679,6 +679,38 @@ public function getAnchor() return $this->anchor = $result; } + /** + * Get anchors in this block's content. + * Used to populate the "anchor on a page" link in the WYSIWYG + * + * By default, this finds anchors in any HTMLText field on the block, but + * this method should be overridden if anchors are provided in other ways + * for this block or if not all HTMLText fields for this block are + * displayed on the front-end. + */ + public function getAnchorsInContent(): array + { + $anchors = [$this->getAnchor()]; + $anchorRegex = "/\\s+(name|id)\\s*=\\s*([\"'])([^\\2\\s>]*?)\\2|\\s+(name|id)\\s*=\\s*([^\"']+)[\\s +>]/im"; + $allFields = DataObject::getSchema()->fieldSpecs($this); + foreach ($allFields as $field => $fieldSpec) { + $fieldObj = $this->owner->dbObject($field); + if ($fieldObj instanceof DBHTMLText) { + $parseSuccess = preg_match_all($anchorRegex, $fieldObj->getValue() ?? '', $matches); + if ($parseSuccess >= 1) { + $fieldAnchors = array_values(array_filter( + array_merge($matches[3], $matches[5]) + )); + $anchors = array_merge($anchors, $fieldAnchors); + } + } + } + $anchors = array_unique($anchors); + + $this->extend('updateAnchorsInContent', $anchors); + return $anchors; + } + /** * @param string|null $action * @return string|null @@ -688,7 +720,7 @@ public function getAnchor() public function AbsoluteLink($action = null) { $page = $this->getPage(); - + if ($page && ClassInfo::hasMethod($page, 'AbsoluteLink')) { $link = $page->AbsoluteLink($action) . '#' . $this->getAnchor(); $this->extend('updateAbsoluteLink', $link); diff --git a/tests/BaseElementTest.php b/tests/BaseElementTest.php index 0de95c6b..b29adaf6 100644 --- a/tests/BaseElementTest.php +++ b/tests/BaseElementTest.php @@ -12,6 +12,7 @@ use DNADesign\Elemental\Tests\Src\TestElement; use DNADesign\Elemental\Tests\Src\TestElementDataObject; use DNADesign\Elemental\Tests\Src\TestDataObjectWithCMSEditLink; +use DNADesign\Elemental\Tests\Src\TestMultipleHtmlFieldsElement; use DNADesign\Elemental\Tests\Src\TestPage; use Page; use ReflectionClass; @@ -46,6 +47,7 @@ class BaseElementTest extends FunctionalTest TestDataObject::class, TestDataObjectWithCMSEditLink::class, TestElementDataObject::class, + TestMultipleHtmlFieldsElement::class, ]; public function testSimpleClassName() @@ -271,6 +273,38 @@ public function testUpdateContentForSearchIndex() ElementContent::remove_extension(TestContentForSearchIndexExtension::class); } + public function getElementAnchorDataProvider(): array + { + return [ + [ + TestMultipleHtmlFieldsElement::class, + 'multiHtmlFields1', + [ + 'anchor1', + 'anchor2', + 'anchor3', + 'anchor4', + ] + ], + [ + TestMultipleHtmlFieldsElement::class, + 'multiHtmlFields2', + [] + ], + ]; + } + + /** + * @dataProvider getElementAnchorDataProvider + */ + public function testGetAnchorsInContent(string $elementClass, string $elementName, array $expectedAnchors): void + { + $element = $this->objFromFixture($elementClass, $elementName); + array_unshift($expectedAnchors, $element->getAnchor()); + // We use array values here because `array_unique` in `getAnchorsInContent()` doesn't reset array indices + $this->assertSame($expectedAnchors, array_values($element->getAnchorsInContent())); + } + public function getElementCMSLinkDataProvider() { return [ diff --git a/tests/Behat/Context/FixtureContext.php b/tests/Behat/Context/FixtureContext.php index 29cc0232..07de7ec3 100644 --- a/tests/Behat/Context/FixtureContext.php +++ b/tests/Behat/Context/FixtureContext.php @@ -13,7 +13,7 @@ class FixtureContext extends BaseFixtureContext { /** - * @Given /(?:the|a) "([^"]+)" "([^"]+)" (?:with|has) a "([^"]+)" content element with "([^"]+)" content/ + * @Given /(?:the|a) "([^"]+)" "([^"]+)" (?:with|has) a "([^"]+)" content element with "(.*)" content/ * * @param string $pageTitle * @param string $type diff --git a/tests/Behat/features/add-link-to-anchor.feature b/tests/Behat/features/add-link-to-anchor.feature new file mode 100644 index 00000000..66ddf10e --- /dev/null +++ b/tests/Behat/features/add-link-to-anchor.feature @@ -0,0 +1,62 @@ +Feature: Link to anchors in elements +As a cms author +I want to link to anchors in my content +So that I can direct users directly to the relevant information + + Background: + Given a "page" "No Blocks" has the "Content" "

My awesome content

" + And a "BasicElementalPage" "Elemental" with a "Anchor Test Block" content element with "

My awesomer content

" content + And the "BasicElementalPage" "Elemental" has a "Same Page Anchor Block" content element with "

" content + And I am logged in with "ADMIN" permissions + And I go to "/admin/pages" + + Scenario: I can link to anchors in an elemental block from a normal page + Given I left click on "No Blocks" in the tree + And I select "awesome" in the "Content" HTML field + And I press the "Insert link" HTML field button + And I click "Anchor on a page" in the ".mce-menu" element + Then I should see an "form#Form_editorAnchorLink" element + And I should see "No Blocks" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element + When I click "No Blocks" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element + And I click "Elemental" in the "#Form_editorAnchorLink_PageID_Holder .Select-menu-outer" element + And I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element + And I click "element-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element + Then I should see "element-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element + # Close the dialog now that we're done with it. + When I click on the "button.close" element + + Scenario: I can link to anchors on a normal page from an elemental block + Given I left click on "Elemental" in the tree + Then I should see a list of blocks + And I should see "Anchor Test Block" + Given I click on block 1 + Then the "Content" field for block 1 should contain "My awesomer content" + When I select "awesomer" in the "Content" HTML field + And I press the "Insert link" HTML field button + And I click "Anchor on a page" in the ".mce-menu" element + Then I should see an "form#Form_editorAnchorLink" element + And I should see "Elemental" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element + When I click "Elemental" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element + And I click "No Blocks" in the "#Form_editorAnchorLink_PageID_Holder .Select-menu-outer" element + And I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element + And I click "normal-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element + Then I should see "normal-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element + # Close the dialog now that we're done with it. + When I click on the "button.close" element + + Scenario: I can link to anchors in an elemental block from another elemental block + Given I left click on "Elemental" in the tree + And I should see a list of blocks + And I should see "Anchor Test Block" + Given I click on block 1 + Then the "Content" field for block 1 should contain "My awesomer content" + When I select "awesomer" in the "Content" HTML field + And I press the "Insert link" HTML field button + And I click "Anchor on a page" in the ".mce-menu" element + Then I should see an "form#Form_editorAnchorLink" element + And I should see "Elemental" in the "#Form_editorAnchorLink_PageID_Holder .Select-multi-value-wrapper" element + When I click "Select or enter anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-multi-value-wrapper" element + And I click "another-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-menu-outer" element + Then I should see "another-anchor" in the "#Form_editorAnchorLink_Anchor_Holder .Select-value" element + # Close the dialog now that we're done with it. + When I click on the "button.close" element diff --git a/tests/ElementalAreaDataObjectTest.yml b/tests/ElementalAreaDataObjectTest.yml index 0681ea5f..cfe3799b 100644 --- a/tests/ElementalAreaDataObjectTest.yml +++ b/tests/ElementalAreaDataObjectTest.yml @@ -14,7 +14,7 @@ DNADesign\Elemental\Models\ElementalArea: DNADesign\Elemental\Tests\Src\TestDataObjectWithCMSEditLink: dataObject1: - Title: DataObject with CMSEditLink method + Title: DataObject with CMSEditLink method ElementalAreaID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 DNADesign\Elemental\Tests\Src\TestDataObject: @@ -60,3 +60,12 @@ DNADesign\Elemental\Models\ElementContent: contentDataObject1: HTML: Some content ParentID: =>DNADesign\Elemental\Models\ElementalArea.areaDataObject1 + +DNADesign\Elemental\Tests\Src\TestMultipleHtmlFieldsElement: + multiHtmlFields1: + Field1: '

' + Field2: '

' + Field3: '

' + multiHtmlFields2: + Field1: '

id="not-anchor"

' + Field2: '

name="not-anchor2"

' diff --git a/tests/Src/TestMultiHtmlFieldsElement.php b/tests/Src/TestMultiHtmlFieldsElement.php new file mode 100644 index 00000000..6995ee5a --- /dev/null +++ b/tests/Src/TestMultiHtmlFieldsElement.php @@ -0,0 +1,18 @@ + 'HTMLText', + 'Field2' => 'HTMLText', + 'Field3' => 'HTMLText', + ]; +}