Skip to content

Commit

Permalink
NEW Get anchor links from elemental pages
Browse files Browse the repository at this point in the history
Co-Authored-By: Steve Boyd <[email protected]>
  • Loading branch information
GuySartorelli and emteknetnz committed May 26, 2022
1 parent 38bd12b commit 388152c
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 19 deletions.
55 changes: 40 additions & 15 deletions src/Extensions/ElementalPageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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()) {
Expand All @@ -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);
}
}
}
}
}
36 changes: 34 additions & 2 deletions src/Models/BaseElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public function canEdit($member = null)
public function canDelete($member = null)
{
$extended = $this->extendedCan(__FUNCTION__, $member);

if ($extended !== null) {
return $extended;
}
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions tests/BaseElementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,6 +47,7 @@ class BaseElementTest extends FunctionalTest
TestDataObject::class,
TestDataObjectWithCMSEditLink::class,
TestElementDataObject::class,
TestMultipleHtmlFieldsElement::class,
];

public function testSimpleClassName()
Expand Down Expand Up @@ -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 [
Expand Down
2 changes: 1 addition & 1 deletion tests/Behat/Context/FixtureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/Behat/features/add-link-to-anchor.feature
Original file line number Diff line number Diff line change
@@ -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" "<p>My awesome content<a name="normal-anchor"></a></p>"
And a "SilverStripe\FrameworkTest\Elemental\ElementalBlocksTestPage" "Elemental" with a "Anchor Test Block" content element with "<p>My awesomer content<a name="element-anchor"></a></p>" content
And the "SilverStripe\FrameworkTest\Elemental\ElementalBlocksTestPage" "Elemental" has a "Same Page Anchor Block" content element with "<p><a id="another-anchor"></a></p>" 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
11 changes: 10 additions & 1 deletion tests/ElementalAreaDataObjectTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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: '<p><a id="anchor1"></a><span name="anchor2"></span></p>'
Field2: '<p><a id="anchor1"></a><span name="anchor3"></span></p>'
Field3: '<p><a id="anchor4"></a><span name="anchor3"></span></p>'
multiHtmlFields2:
Field1: '<p>id="not-anchor"</p>'
Field2: '<p>name="not-anchor2"</p>'
18 changes: 18 additions & 0 deletions tests/Src/TestMultiHtmlFieldsElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace DNADesign\Elemental\Tests\Src;

use DNADesign\Elemental\Models\BaseElement;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Security\Permission;

class TestMultipleHtmlFieldsElement extends BaseElement implements TestOnly
{
private static $table_name = 'TestMultipleHtmlFieldsElement';

private static $db = [
'Field1' => 'HTMLText',
'Field2' => 'HTMLText',
'Field3' => 'HTMLText',
];
}

0 comments on commit 388152c

Please sign in to comment.