Skip to content

Commit

Permalink
First init
Browse files Browse the repository at this point in the history
  • Loading branch information
pKallert committed Jan 26, 2024
0 parents commit 39c45bd
Show file tree
Hide file tree
Showing 24 changed files with 1,235 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
33 changes: 33 additions & 0 deletions Classes/AnchorLinkResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Paessler\AnchorLink;

use Neos\ContentRepository\Domain\Model\NodeInterface;

interface AnchorLinkResolverInterface
{
/**
* Return an array of options for the anchor link selectbox:
*
* [
* [
* 'icon' => 'icon-foo', // optional
* 'group' => 'first', // optional
* 'value' => 'bar',
* 'label' => 'Bar',
* ],
* [
* 'icon' => 'icon-foo', // optional
* 'group' => 'second',
* 'value' => 'baz',
* 'label' => 'Baz',
* ]
* ];
*
* @param NodeInterface $node Currently focused node
* @param string $link Current link target (for example "node://<some-identifier>" or "https://www.external.url")
* @param string $searchTerm Search term (term that has been entered in the "Choose link anchor" search field, defaults to an empty string)
* @return array
*/
public function resolve(NodeInterface $node, string $link, string $searchTerm): array;
}
111 changes: 111 additions & 0 deletions Classes/ContentNodeAnchorLinkResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Paessler\AnchorLink;

use Neos\Eel\Exception as EelException;
use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy;
use Neos\Neos\Service\LinkingService;
use Neos\Eel\EelEvaluatorInterface;
use Neos\Eel\Utility;
use Neos\Eel\FlowQuery\FlowQuery;
use Neos\Neos\Domain\Service\NodeSearchService;

/**
* Create link anchors based on all matching nodes within the target link node
*
* @see Paessler:AnchorLink:* Settings
*/
class ContentNodeAnchorLinkResolver implements AnchorLinkResolverInterface
{
/**
* @Flow\Inject
* @var NodeSearchService
*/
protected $nodeSearchService;

/**
* @Flow\Inject
* @var EelEvaluatorInterface
*/
protected $eelEvaluator;

/**
* @Flow\InjectConfiguration("eelContext")
* @var array
*/
protected $contextConfiguration;

/**
* @Flow\InjectConfiguration(path="contentNodeType")
* @var string
*/
protected $contentNodeType;

/**
* @Flow\InjectConfiguration(path="anchor")
* @var string
*/
protected $anchor;

/**
* @Flow\InjectConfiguration(path="label")
* @var string
*/
protected $label;

/**
* @Flow\InjectConfiguration(path="group")
* @var string
*/
protected $group;

/**
* @Flow\InjectConfiguration(path="icon")
* @var string
*/
protected $icon;

/**
* @inheritDoc
* @throws EelException
*/
public function resolve(NodeInterface $node, string $link, string $searchTerm): array
{
$context = $node->getContext();
$targetNode = null;

if ((preg_match(LinkingService::PATTERN_SUPPORTED_URIS, $link, $matches) === 1) && $matches[1] === 'node') {
$targetNode = $context->getNodeByIdentifier($matches[2]) ?? $node;
}
if ($targetNode === null) {
return [];
}

if ($searchTerm !== '') {
$nodes = $this->nodeSearchService->findByProperties($searchTerm, [$this->contentNodeType], $context, $targetNode->getPrimaryChildNode());
} else {
$q = new FlowQuery([$targetNode]);
/** @noinspection PhpUndefinedMethodInspection */
$nodes = $q->children('[instanceof Neos.Neos:ContentCollection]')->find('[instanceof ' . $this->contentNodeType . ']')->get();
}

if ($this->eelEvaluator instanceof DependencyProxy) {
$this->eelEvaluator->_activateDependency();
}

return array_values(array_map(function (NodeInterface $node) {
$anchor = (string)Utility::evaluateEelExpression($this->anchor, $this->eelEvaluator, ['node' => $node], $this->contextConfiguration);
$label = (string)Utility::evaluateEelExpression($this->label, $this->eelEvaluator, ['node' => $node], $this->contextConfiguration);
$group = (string)Utility::evaluateEelExpression($this->group, $this->eelEvaluator, ['node' => $node], $this->contextConfiguration);
$icon = (string)Utility::evaluateEelExpression($this->icon, $this->eelEvaluator, ['node' => $node], $this->contextConfiguration);
return [
'icon' => $icon,
'group' => $group,
'value' => $anchor,
'label' => $label,
];
}, $nodes));
}
}
37 changes: 37 additions & 0 deletions Classes/Controller/AnchorLinkController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Paessler\AnchorLink\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\View\JsonView;
use Neos\Flow\Mvc\Controller\ActionController;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Paessler\AnchorLink\AnchorLinkResolverInterface;

class AnchorLinkController extends ActionController
{
/**
* @var AnchorLinkResolverInterface
* @Flow\Inject
*/
protected AnchorLinkResolverInterface $resolver;

/**
* @var array
*/
protected $viewFormatToObjectNameMap = array(
'json' => JsonView::class
);

/**
* @param NodeInterface $node
* @param string $link
* @param string $searchTerm
* @return void
*/
public function resolveAnchorsAction(NodeInterface $node, string $link, string $searchTerm): void
{
$options = $this->resolver->resolve($node, $link, $searchTerm);
$this->view->assign('value', $options);
}
}
4 changes: 4 additions & 0 deletions Configuration/Objects.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'Paessler\AnchorLink\Controller\AnchorLinkController':
properties:
resolver:
object: Paessler\AnchorLink\ContentNodeAnchorLinkResolver
10 changes: 10 additions & 0 deletions Configuration/Policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
privilegeTargets:
Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege:
"Paessler.AnchorLink:Backend":
matcher: 'method(Paessler\AnchorLink\Controller\AnchorLinkController->(.*)Action())'

roles:
"Neos.Neos:AbstractEditor":
privileges:
- privilegeTarget: "Paessler.AnchorLink:Backend"
permission: GRANT
8 changes: 8 additions & 0 deletions Configuration/Routes.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- name: "Paessler.AnchorLink options resolver route"
uriPattern: "link-resolver/{@action}"
defaults:
"@package": "Paessler.AnchorLink"
"@controller": "AnchorLink"
"@format": "json"
"@action": "resolveAnchors"
appendExceedingArguments: TRUE
58 changes: 58 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Neos:
Neos:
fusion:
autoInclude:
Paessler.AnchorLink: true
userInterface:
translation:
autoInclude:
Paessler.AnchorLink:
- Main
- 'NodeTypes/*'
Ui:
resources:
javascript:
'Paessler.AnchorLink':
resource: 'resource://Paessler.AnchorLink/Public/JavaScript/AnchorLink/Plugin.js'
frontendConfiguration:
'Paessler.AnchorLink':
displaySearchBox: true
threshold: 0
Flow:
security:
authentication:
providers:
'Neos.Neos:Backend':
requestPatterns:
'Paessler.AnchorLink:Backend':
pattern: ControllerObjectName
patternOptions:
controllerObjectNamePattern: 'Paessler\AnchorLink\Controller\.*'
mvc:
routes:
'Paessler.AnchorLink':
position: 'before Neos.Neos'
Paessler:
AnchorLink:
# The following configuration is only considered if the default ContentNodeAnchorLinkResolver is used:

# Only nodes of this type will appear in the "Choose link anchor" selector
contentNodeType: 'Paessler.AnchorLink:AnchorMixin'
# Eel Expression that returns the anchor (without leading "#") for a given node
anchor: ${node.properties.anchor || node.name}
# Eel Expression that returns the label to be rendered in the anchor selector in the Backend
label: ${node.label}
# Eel Expression that returns a group for the anchor selector (empty string == no grouping)
group: ${I18n.translate(node.nodeType.label)}
# Eel Expression that returns an icon for the anchor selector (empty string = no icon)
icon: ${node.nodeType.fullConfiguration.ui.icon}

# Eel Helpers that are available in the Eel expressions above
eelContext:
String: Neos\Eel\Helper\StringHelper
Array: Neos\Eel\Helper\ArrayHelper
Date: Neos\Eel\Helper\DateHelper
Configuration: Neos\Eel\Helper\ConfigurationHelper
Math: Neos\Eel\Helper\MathHelper
Json: Neos\Eel\Helper\JsonHelper
I18n: Neos\Flow\I18n\EelHelper\TranslationHelper
20 changes: 20 additions & 0 deletions NodeTypes/Mixin/Anchor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'Paessler.AnchorLink:AnchorMixin':
abstract: true
ui:
inspector:
groups:
anchor:
label: i18n
properties:
anchor:
type: string
ui:
reloadIfChanged: true
label: i18n
inspector:
group: anchor
help:
message: i18n
validation:
'Neos.Neos/Validation/RegularExpressionValidator':
regularExpression: '/^([a-zA-Z\d\-._~\!$&()*+,;=:@%\/?]*)$/'
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Paessler.AnchorLink

Extends the Neos CKE5 link-editor with server-side resolvable anchor links.

![anchorlink](https://user-images.githubusercontent.com/837032/72664973-de351880-3a14-11ea-8d2b-a379b7c7bb47.gif)

## Installation

1. Install the package: `composer require paessler/anchorlink`

2. Enable additional linking options with such config:

```yaml
"Neos.NodeTypes.BaseMixins:TextMixin": # Or other nodetype
properties:
text:
ui:
inline:
editorOptions:
linking:
anchorLink: true
```
3. For all content nodetypes that you would like to be able to link to, inherit from `Paessler.AnchorLink:AnchorMixin`, e.g.:

```yaml
Neos.Neos:Content: # Or other nodetype
superTypes:
Paessler.AnchorLink:AnchorMixin: true
```

4. Adjust the rendering for those nodes to insert anchors before them, e.g. there is included a Fusion processor to help with that:

```
prototype(Neos.Neos:Content).@process.anchor = Paessler.AnchorLink:AnchorLinkAugmenter
```

Note: this will add an `id` attribute to the corresponding output. For this to work reliably the corresponding prototype should render
a single root element. Otherwise an additional wrapping `div` element will be rendered.
Also the rendered content must not already contain an `id` attribute because it would be merged with the one from the augmentor in that case.

## Configuration

It's possible to configure the content node nodetype that is used for linking. Also it's possible to use a different property for the anchor value and the label via Settings.yaml.

These are the defaults:

```yaml
Paessler:
AnchorLink:
# Only nodes of this type will appear in the "Choose link anchor" selector
contentNodeType: "Paessler.AnchorLink:AnchorMixin"
# Eel Expression that returns the anchor (without leading "#") for a given node
anchor: ${node.properties.anchor || node.name}
# Eel Expression that returns the label to be rendered in the anchor selector in the Backend
label: ${node.label}
# Eel Expression that returns a group for the anchor selector (empty string == no grouping)
group: ${I18n.translate(node.nodeType.label)}
# Eel Expression that returns an icon for the anchor selector (empty string = no icon)
icon: ${node.nodeType.fullConfiguration.ui.icon}
```

It's possible to disable the searchbox or adjust its threshold via Settings.yaml, the default settings are:

```yaml
Neos:
Neos:
Ui:
frontendConfiguration:
"Paessler.AnchorLink":
displaySearchBox: true
threshold: 0
```

## Low-level Customization

Finally, it is possible to create a completely custom anchor nodes resolver.

Create a class implementing `AnchorLinkResolverInterface` that would take the current content node, link and a searchTerm and return an array of options for the link anchor selector and configure it in `Objects.yaml` like this:

```
'Paessler\AnchorLink\Controller\AnchorLinkController':
properties:
resolver:
object: Your\Custom\AnchorLinkResolver
```

## Development

If you need to adjust anything in this package, just do so and then rebuild the code like this:

```
cd Resources/Private/JavaScript/AnchorLink
yarn && yarn build
```

And then commit changed filed including Plugin.js

## About

The package is based on the `DIU.Neos.AnchorLink` package. We thank the DIU team for all the efforts.
6 changes: 6 additions & 0 deletions Resources/Private/Fusion/Argumenter/AnchorLink.fusion
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Meant to be used as a processor, e.g.:
# prototype(Neos.Neos:Content).@process.anchor = Paessler.AnchorLink:AnchorLinkAugmenter
# @neoscs-ignore-next-line
prototype(Paessler.AnchorLink:AnchorLinkAugmenter) < prototype(Neos.Fusion:Augmenter) {
id = ${Neos.Node.isOfType(node, 'Paessler.AnchorLink:AnchorMixin') && !String.isBlank(q(node).property('anchor')) ? q(node).property('anchor') : node.name}
}
Loading

0 comments on commit 39c45bd

Please sign in to comment.