├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ ├── config.yml
│ ├── feature_request.yaml
│ └── support_request.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config.codekit3
├── docs
├── .sidebar.json
├── README.md
├── developers
│ ├── events.md
│ ├── extensibility.md
│ ├── graphql.md
│ ├── nav.md
│ └── node.md
├── feature-tour
│ └── overview.md
├── get-started
│ ├── configuration.md
│ ├── installation-setup.md
│ └── requirements.md
├── getting-elements
│ └── node-queries.md
└── template-guides
│ ├── available-variables.md
│ ├── breadcrumbs.md
│ ├── eager-loading.md
│ ├── navigation-field.md
│ └── rendering-nodes.md
└── src
├── Navigation.php
├── assetbundles
└── NavigationAsset.php
├── base
├── NodeType.php
├── NodeTypeInterface.php
└── PluginTrait.php
├── console
└── controllers
│ └── NavsController.php
├── controllers
├── BaseController.php
├── NavsController.php
└── NodesController.php
├── elements
├── Node.php
├── conditions
│ ├── NodeCondition.php
│ └── TypeConditionRule.php
└── db
│ └── NodeQuery.php
├── events
├── NavEvent.php
├── NodeActiveEvent.php
├── NodeEvent.php
├── RegisterElementEvent.php
└── RegisterNodeTypeEvent.php
├── fieldlayoutelements
├── ClassesField.php
├── CustomAttributesField.php
├── NewWindowField.php
├── NodeTypeElements.php
└── UrlSuffixField.php
├── fields
└── NavigationField.php
├── gql
├── arguments
│ └── NodeArguments.php
├── interfaces
│ └── NodeInterface.php
├── queries
│ └── NodeQuery.php
├── resolvers
│ └── NodeResolver.php
└── types
│ ├── CustomAttributeType.php
│ ├── NodeType.php
│ └── generators
│ ├── CustomAttributeGenerator.php
│ └── NodeGenerator.php
├── helpers
├── Gql.php
├── Plugin.php
└── ProjectConfigData.php
├── icon-mask.svg
├── icon.svg
├── integrations
└── NodeFeedMeElement.php
├── migrations
├── Install.php
└── m231229_000000_content_refactor.php
├── models
├── Nav.php
├── Nav_SiteSettings.php
└── Settings.php
├── nodetypes
├── CustomType.php
├── PassiveType.php
└── SiteType.php
├── records
├── Nav.php
├── Nav_SiteSettings.php
└── Node.php
├── resources
├── dist
│ ├── css
│ │ └── navigation.css
│ ├── fonts
│ │ ├── fa-regular-400.eot
│ │ ├── fa-regular-400.svg
│ │ ├── fa-regular-400.ttf
│ │ ├── fa-regular-400.woff
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-solid-900.eot
│ │ ├── fa-solid-900.svg
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-solid-900.woff
│ │ └── fa-solid-900.woff2
│ └── js
│ │ ├── navigation.js
│ │ └── navigation.js.map
└── src
│ ├── js
│ ├── _jquery.serializejson.min.js
│ └── navigation.js
│ └── scss
│ ├── _font-awesome.scss
│ └── navigation.scss
├── services
├── Breadcrumbs.php
├── Elements.php
├── Navs.php
├── NodeTypes.php
└── Nodes.php
├── templates
├── _field
│ ├── input.html
│ └── settings.html
├── _integrations
│ └── feed-me
│ │ ├── column.html
│ │ ├── fields
│ │ └── nested-node.html
│ │ ├── groups.html
│ │ └── map.html
├── _layouts
│ └── index.html
├── _special
│ └── render.html
├── _types
│ ├── custom
│ │ └── modal.html
│ └── site
│ │ ├── modal.html
│ │ └── settings.html
├── navs
│ ├── _build.html
│ ├── _edit.html
│ └── index.html
└── settings
│ ├── _panes
│ └── general.html
│ └── index.html
├── translations
├── en
│ └── navigation.php
├── fr
│ └── navigation.php
├── hu
│ └── navigation.php
└── nl
│ └── navigation.php
└── variables
└── NavigationVariable.php
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us improve.
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Before you send through your bug report, please ensure you have taken these steps first:
8 |
9 | ✅ I‘ve searched the [documentation](https://verbb.io/craft-plugins/navigation/docs).
10 | ✅ I‘ve searched open and closed issues.
11 |
12 | - type: textarea
13 | id: bug-description
14 | attributes:
15 | label: Describe the bug
16 | description: Describe the bug and what behaviour you expect if the bug is fixed.
17 | placeholder: "I have an issue where..."
18 | validations:
19 | required: true
20 | - type: textarea
21 | id: steps-to-reproduce
22 | attributes:
23 | label: Steps to reproduce
24 | description: Detail how we can reproduce this issue.
25 | value: |
26 | 1.
27 | 2.
28 | validations:
29 | required: true
30 | - type: input
31 | id: craft-version
32 | attributes:
33 | label: Craft CMS version
34 | description: What version of Craft CMS you‘re using. **Do not write "latest"**.
35 | validations:
36 | required: true
37 | - type: input
38 | id: plugin-version
39 | attributes:
40 | label: Plugin version
41 | description: What version of the plugin you‘re using. **Do not write "latest"**.
42 | validations:
43 | required: true
44 | - type: input
45 | id: multi-site
46 | attributes:
47 | label: Multi-site?
48 | description: Whether your install is a multi-site.
49 | placeholder: |
50 | "Yes" or "No"
51 | - type: textarea
52 | id: additional-context
53 | attributes:
54 | label: Additional context
55 | description: Provide any additional information you think might be useful. The more information you provide the easier it‘ll be for use to fix this bug!
56 | placeholder: |
57 | "I also have X plugin installed..." or "This only happens on production..."
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Documentation
4 | url: https://verbb.io/craft-plugins/navigation/docs
5 | about: Read the official documentation.
6 | - name: Craft Discord
7 | url: https://craftcms.com/discord
8 | about: Community discussion and support.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea or enhancement.
3 | labels: 'feature request'
4 | body:
5 | - type: textarea
6 | id: feature-description
7 | attributes:
8 | label: What are you trying to do?
9 | description: A description of what you want to happen.
10 | placeholder: "I would like to see..."
11 | validations:
12 | required: true
13 | - type: textarea
14 | id: proposed-solution
15 | attributes:
16 | label: What's your proposed solution?
17 | description: A description of how you think this could be solved, including any alternatives that you considered.
18 | validations:
19 | required: true
20 | - type: textarea
21 | id: additional-context
22 | attributes:
23 | label: Additional context
24 | description: Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support_request.yaml:
--------------------------------------------------------------------------------
1 | name: Question
2 | description: Ask a question about this plugin. DO NOT use this to submit bug reports.
3 | labels: 'question'
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Before you send through your question, please ensure you have taken these steps first:
9 |
10 | ✅ I‘ve searched the [documentation](https://verbb.io/craft-plugins/navigation/docs).
11 | ✅ I‘ve searched open and closed issues.
12 | ✅ This is not a bug report, just a general question.
13 |
14 | - type: textarea
15 | id: question
16 | attributes:
17 | label: Question
18 | description: A question about the plugin or how it works.
19 | placeholder: "Is it possible to do..."
20 | validations:
21 | required: true
22 | - type: textarea
23 | id: additional-context
24 | attributes:
25 | label: Additional context
26 | description: Add any other context or screenshots about your question here.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # CRAFT ENVIRONMENT
2 | .env.php
3 | .env.sh
4 | .env
5 |
6 | # COMPOSER
7 | /vendor
8 |
9 | # BUILD FILES
10 | /bower_components/*
11 | /node_modules/*
12 | /build/*
13 | /yarn-error.log
14 |
15 | # MISC FILES
16 | .cache
17 | .DS_Store
18 | .idea
19 | .project
20 | .settings
21 | .map
22 | *.esproj
23 | *.sublime-workspace
24 | *.sublime-project
25 | *.tmproj
26 | *.tmproject
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © Verbb
2 |
3 | Permission is hereby granted to any person obtaining a copy of this software
4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies
5 | of the Software, and to permit persons to whom the Software is furnished to do
6 | so, subject to the following conditions:
7 |
8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be
9 | included in all copies or substantial portions of the Software.
10 |
11 | 2. **Don’t use the same license on more than one project.** Each licensed copy
12 | of the Software shall be actively installed in no more than one production
13 | environment at a time.
14 |
15 | 3. **Don’t mess with the licensing features.** Software features related to
16 | licensing shall not be altered or circumvented in any way, including (but
17 | not limited to) license validation, payment prompts, feature restrictions,
18 | and update eligibility.
19 |
20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
21 | prompt, reminder, or other message indicating that a payment is owed.
22 |
23 | 5. **Follow the law.** All use of the Software shall not violate any applicable
24 | law or regulation, nor infringe the rights of any other person or entity.
25 |
26 | Failure to comply with the foregoing conditions will automatically and
27 | immediately result in termination of the permission granted hereby. This
28 | license does not include any right to receive updates to the Software or
29 | technical support. Licensees bear all risk related to the quality and
30 | performance of the Software and any modifications made or obtained to it,
31 | including liability for actual and consequential harm, such as loss or
32 | corruption of data, and any necessary service, repair, or correction.
33 |
34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Navigation for Craft CMS
3 |
4 | Navigation is a Craft CMS plugin to help manage navigation menus for you site. Supports linking to existing elements like entries, categories and products, as well as custom URLs.
5 |
6 | ## Features
7 | - Create multiple navigations
8 | - Create navigation nodes for entries, assets, categories and Commerce products
9 | - Create custom URLs
10 | - Enable/disable, open links in a new window, or apply additional CSS classes
11 | - Automatically updates nodes when linked elements status or title changes
12 | - Navigation nodes are elements for flexible querying
13 | - Support for third-party elements with hooks
14 | - Support for multi-site navigations
15 | - Simple `render()` Twig function, or roll your own
16 | - Generate breadcrumbs easily based on your URL segments
17 | - Tool to migrate your menus if you've used [A&M Nav for Craft 2](https://github.com/am-impact/amnav) or [Navee for Craft 2](https://github.com/fromtheoutfit/navee)
18 |
19 | ## Documentation
20 | Visit the [Navigation Plugin page](https://verbb.io/craft-plugins/navigation) for all documentation, guides, pricing and developer resources.
21 |
22 | ## Credit & Thanks
23 | A big shoutout to [A&M Nav](https://github.com/am-impact/amnav) for their awesome plugin for Craft 2.
24 |
25 | ## Support
26 | Get in touch with us via the [Navigation Support page](https://verbb.io/craft-plugins/navigation/support) or by [creating a Github issue](/verbb/navigation/issues)
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "verbb/navigation",
3 | "description": "Create navigation menus for your site.",
4 | "type": "craft-plugin",
5 | "version": "3.0.8",
6 | "keywords": [
7 | "craft",
8 | "cms",
9 | "craftcms",
10 | "craft-plugin",
11 | "navigation",
12 | "menu"
13 | ],
14 | "support": {
15 | "email": "support@verbb.io",
16 | "issues": "https://github.com/verbb/navigation/issues?state=open",
17 | "source": "https://github.com/verbb/navigation",
18 | "docs": "https://github.com/verbb/navigation",
19 | "rss": "https://github.com/verbb/navigation/commits/v2.atom"
20 | },
21 | "license": "proprietary",
22 | "authors": [
23 | {
24 | "name": "Verbb",
25 | "homepage": "https://verbb.io"
26 | }
27 | ],
28 | "require": {
29 | "php": "^8.2",
30 | "craftcms/cms": "^5.0.0",
31 | "verbb/base": "^3.0.0"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "verbb\\navigation\\": "src/"
36 | }
37 | },
38 | "extra": {
39 | "name": "Navigation",
40 | "handle": "navigation",
41 | "changelogUrl": "https://raw.githubusercontent.com/verbb/navigation/craft-5/CHANGELOG.md",
42 | "class": "verbb\\navigation\\Navigation"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/docs/.sidebar.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "Get Started",
4 | "collapsable": false,
5 | "children": [
6 | "get-started/installation-setup",
7 | "get-started/requirements",
8 | "get-started/configuration"
9 | ]
10 | },
11 | {
12 | "title": "Feature Tour",
13 | "collapsable": false,
14 | "children": [
15 | "feature-tour/overview"
16 | ]
17 | },
18 | {
19 | "title": "Template Guides",
20 | "collapsable": false,
21 | "children": [
22 | "template-guides/available-variables",
23 | "template-guides/rendering-nodes",
24 | "template-guides/breadcrumbs",
25 | "template-guides/navigation-field",
26 | "template-guides/eager-loading"
27 | ]
28 | },
29 | {
30 | "title": "Getting Elements",
31 | "collapsable": false,
32 | "children": [
33 | "getting-elements/node-queries"
34 | ]
35 | },
36 | {
37 | "title": "Developers",
38 | "collapsable": false,
39 | "children": [
40 | "developers/node",
41 | "developers/nav",
42 | "developers/events",
43 | "developers/extensibility",
44 | "developers/graphql"
45 | ]
46 | }
47 | ]
48 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/docs/README.md
--------------------------------------------------------------------------------
/docs/developers/events.md:
--------------------------------------------------------------------------------
1 | # Events
2 | Events can be used to extend the functionality of Navigation.
3 |
4 | ## Nav related events
5 |
6 | ### The `beforeSaveNav` event
7 | Plugins can get notified before a navigation is saved
8 |
9 | ```php
10 | use verbb\navigation\events\NavEvent;
11 | use verbb\navigation\services\Navs;
12 | use yii\base\Event;
13 |
14 | Event::on(Navs::class, Navs::EVENT_BEFORE_SAVE_NAV, function(NavEvent $e) {
15 | // Do something
16 | });
17 | ```
18 |
19 | ### The `afterSaveNav` event
20 | Plugins can get notified after a navigation has been saved
21 |
22 | ```php
23 | use verbb\navigation\events\NavEvent;
24 | use verbb\navigation\services\Navs;
25 | use yii\base\Event;
26 |
27 | Event::on(Navs::class, Navs::EVENT_AFTER_SAVE_NAV, function(NavEvent $e) {
28 | // Do something
29 | });
30 | ```
31 |
32 | ### The `beforeDeleteNav` event
33 | Plugins can get notified before a navigation is deleted
34 |
35 | ```php
36 | use verbb\navigation\events\NavEvent;
37 | use verbb\navigation\services\Navs;
38 | use yii\base\Event;
39 |
40 | Event::on(Navs::class, Navs::EVENT_BEFORE_DELETE_NAV, function(NavEvent $event) {
41 | // Do something
42 | });
43 | ```
44 |
45 | ### The `afterDeleteNav` event
46 | Plugins can get notified after a navigation has been deleted
47 |
48 | ```php
49 | use verbb\navigation\events\NavEvent;
50 | use verbb\navigation\services\Navs;
51 | use yii\base\Event;
52 |
53 | Event::on(Navs::class, Navs::EVENT_AFTER_DELETE_NAV, function(NavEvent $event) {
54 | // Do something
55 | });
56 | ```
57 |
58 |
59 | ## Node related events
60 |
61 | ### The `beforeSaveNode` event
62 | Plugins can get notified before a node is saved. Event handlers can prevent the node from getting sent by setting `$event->isValid` to false.
63 |
64 | ```php
65 | use craft\events\ModelEvent;
66 | use verbb\navigation\elements\Node;
67 | use yii\base\Event;
68 |
69 | Event::on(Node::class, Node::EVENT_BEFORE_SAVE, function(ModelEvent $event) {
70 | $node = $event->sender;
71 | $event->isValid = false;
72 | });
73 | ```
74 |
75 | ### The `afterSaveNode` event
76 | Plugins can get notified after a node has been saved
77 |
78 | ```php
79 | use craft\events\ModelEvent;
80 | use verbb\navigation\elements\Node;
81 | use yii\base\Event;
82 |
83 | Event::on(Node::class, Node::EVENT_AFTER_SAVE, function(ModelEvent $event) {
84 | $node = $event->sender;
85 | });
86 | ```
87 |
88 | ### The `modifyNodeActive` event
89 | Plugins can modify the active state of a node.
90 |
91 | ```php
92 | use verbb\navigation\elements\Node;
93 | use yii\base\Event;
94 |
95 | Event::on(Node::class, Node::EVENT_NODE_ACTIVE, function(Event $event) {
96 | $node = $event->node;
97 | $event->isActive = true;
98 | });
99 | ```
100 |
101 | ### The `beforeMoveElement` event
102 | Plugins can get notified before a node is moved in its structure.
103 |
104 | ```php
105 | use verbb\navigation\elements\Node;
106 | use craft\events\MoveElementEvent;
107 | use craft\services\Structures;
108 | use yii\base\Event;
109 |
110 | Event::on(Structures::class, Structures::EVENT_BEFORE_MOVE_ELEMENT, function(MoveElementEvent $event) {
111 | $element = $event->element;
112 |
113 | if ($element instanceof Node) {
114 | // ...
115 | }
116 | });
117 | ```
118 |
119 | ### The `afterMoveElement` event
120 | Plugins can get notified after a node is moved in its structure.
121 |
122 | ```php
123 | use verbb\navigation\elements\Node;
124 | use craft\events\MoveElementEvent;
125 | use craft\services\Structures;
126 | use yii\base\Event;
127 |
128 | Event::on(Structures::class, Structures::EVENT_AFTER_MOVE_ELEMENT, function(MoveElementEvent $event) {
129 | $element = $event->element;
130 |
131 | if ($element instanceof Node) {
132 | // ...
133 | }
134 | });
135 | ```
136 |
--------------------------------------------------------------------------------
/docs/developers/extensibility.md:
--------------------------------------------------------------------------------
1 | # Extensibility
2 |
3 | ## Elements
4 | You can add your own custom elements to be compatible with Navigation by using the provided events. The below shows an example of how entries are set up.
5 |
6 | ```php
7 | namespace modules\sitemodule;
8 |
9 | use verbb\navigation\services\Elements;
10 | use verbb\navigation\events\RegisterElementEvent;
11 | use yii\base\Event;
12 |
13 | Event::on(Elements::class, Elements::EVENT_REGISTER_NAVIGATION_ELEMENT, function(RegisterElementEvent $event) {
14 | $event->elements['entries'] = [
15 | 'label' => Craft::t('navigation', 'Entries'),
16 | 'button' => Craft::t('navigation', 'Add an entry'),
17 | 'type' => 'craft\\elements\\Entry',
18 | ];
19 | });
20 | ```
21 |
22 | ## Node Types
23 | Node types allow you to define your own type of nodes for various cases. You might like to have certain types of "Custom URL" nodes for instance.
24 |
25 | You'll need to first create a class to implement your node type. Here's an example for a Group.
26 |
27 | ```php
28 | types[] = Group::class;
76 | });
77 | ```
78 |
--------------------------------------------------------------------------------
/docs/developers/nav.md:
--------------------------------------------------------------------------------
1 | # Nav
2 | A `Nav` object contains multiple navigation nodes. Whenever you're dealing with a nav in your template, you have access to the following.
3 |
4 | ## Attributes
5 |
6 | Attribute | Description
7 | --- | ---
8 | `id` | ID for the nav.
9 | `name` | The name of the nav.
10 | `handle` | The handle of the nav.
11 | `instructions` | Any additional instructions for the nav, often for internal use in the control panel.
12 | `maxLevels` | The maximum number of nested level of nodes a nav can allow.
13 | `maxnodes` | The maximum number of nodes a nav can allow.
14 | `propagateNodes` | Whether the propagate (copy) nodes across all your sites.
15 |
--------------------------------------------------------------------------------
/docs/developers/node.md:
--------------------------------------------------------------------------------
1 | # Node
2 | Whenever you're dealing with a node in your template, you're actually working with a `Node` object.
3 |
4 | ## Attributes
5 |
6 | Attribute | Description
7 | --- | ---
8 | `id` | ID for the node.
9 | `elementId` | The linked element ID (if not custom).
10 | `element` | The linked element (if not custom).
11 | `navId` | The ID for the nav this node belongs to.
12 | `url` | URL for this node. Either the linked element or custom.
13 | `nodeUri` | URI for this node. Either the linked element or custom.
14 | `title` | Title for this node. Either the linked element or custom.
15 | `link` | Full HTML link (combined url and title).
16 | `type` | The class name for the type of node.
17 | `classes` | Any additional CSS classes added to the node.
18 | `customAttributes` | A list of attributes as provided in the table. Use `attribute` and `value` for each row.
19 | `urlSuffix` | If provided, a suffix (think anchor or query string) added on to the URL.
20 | `target` | Returns either `_blank` or an empty string, should this node open in a new window.
21 | `newWindow` | Whether this node should open in a new window.
22 | `active` | Whether the URL matches the current URL.
23 | `hasActiveChild` | Whether the node has an active child.
24 | `nav` | The [Navigation](docs:developers/nav) model this node belongs to.
25 | `status` | The current status of the node.
26 | `children` | A collection of child nodes (if any).
27 | `level` | The level this node resides in, if using nested nodes.
28 |
29 | ## Methods
30 |
31 | Method | Description
32 | --- | ---
33 | `getTypeLabel()` | The display name for the type of node.
34 | `isElement()` | Whether the node is an "Element" node type (it links to an Entry, Category, etc).
35 | `isCustom()` | Whether the node is a "Custom URL" node type.
36 | `isPassive()` | Whether the node is a "Passive" node type.
37 | `isSite()` | Whether the node is a "Site" node type.
38 |
39 | ### `customAttributes`
40 | As attributes are stored in a table for the node, you'll need to loop through them to output them. Each row has an `attribute` and `value` property, as defined in the table field for the node. These correspond with the column names.
41 |
42 | ```twig
43 |
44 | {{- node.title -}}
45 |
46 | ```
47 |
48 | ### `linkAttributes`
49 | A helper function to assist with generating attributes for an anchor tag.
50 |
51 | ```twig
52 |
53 |
54 | {# Would produce the following HTML #}
55 |
56 |
57 | {# For a node that opens in a new window #}
58 |
59 |
60 | {# For a node with a custom class #}
61 |
62 |
63 | {# For a node with a custom attributes #}
64 |
65 | ```
66 |
67 | You can also pass in any additional attributes you require at the template level:
68 |
69 | ```twig
70 |
74 |
75 | {# Would produce the following HTML #}
76 |
77 | ```
78 |
79 | These will be merged recursively with attributes defined in the node. For example, we might have a class `node-class` defined in the node's settings. As you can see, this is merged in with `another-class` we define in our templates.
80 |
81 | ## Custom Fields
82 | As you can have custom fields attached to each node, you can access their content via their field handles. For instance, you might have added a Plain Text field to your navigation's field layout, with a handle `myPlainTextfield`, which you could access via:
83 |
84 | ```twig
85 | {{ node.myPlainTextfield }}
86 | ```
87 |
88 | ## Element Custom Fields
89 | As nodes can be linked to an element, you can also fetch those custom fields attached to that element. For example, you might have a Homepage entry, which you've added as a node to your navigation. On this entry, you have a Plain Text field with a handle of `myPlainTextfield`. You could access it via:
90 |
91 | ```twig
92 | {{ node.element.myPlainTextfield }}
93 | ```
94 |
95 | However, you'll want to be mindful that when looping through all the other nodes in your navigation that not all nodes are linked to entries, and not all linked entries contain this field. You'll likely receive errors that `myPlainTextfield` is not a valid attribute. So, you'll want to provide some conditional handling of this.
96 |
97 | ```twig
98 | {# Check that this node links to an element, and it has the field we want #}
99 | {% if node.element and node.element.myPlainTextfield %}
100 | {{ node.element.myPlainTextfield }}
101 | {% endif %}
102 |
103 | {# Check for a specific element, via its slug #}
104 | {% if node.element and node.element.slug == 'homepage' %}
105 | {{ node.element.myPlainTextfield }}
106 | {% endif %}
107 | ```
108 |
--------------------------------------------------------------------------------
/docs/feature-tour/overview.md:
--------------------------------------------------------------------------------
1 | # Overview
2 | You'll first want to create a navigation - this contains your navigation nodes with links to elements or custom URLs. Head to "Navigation" on the main sidebar menu, and click "New Navigation" on the top-right.
3 |
4 | Once saved, click on the navigation title to start adding your nodes.
5 |
6 | Using the right-hand side menu, add either elements or custom URLs. Once added, they'll appear on the left-hand side in a structure. Hover over any navigation node to see some actions - move, edit and delete. Any changes you make to this structure will reflect the changes on your live site.
7 |
8 | When linking to an existing element, the Title and Enabled settings will always reflect the original element's state. So, if you change the linked element's title or disable it, it'll reflect this change in the node. You can override the Title and Enabled settings to become independent of your element changes - if you wanted a custom title for instance.
--------------------------------------------------------------------------------
/docs/get-started/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 | Create a `navigation.php` file under your `/config` directory with the following options available to you. You can also use multi-environment options to change these per environment.
3 |
4 | The below shows the defaults already used by Navigation, so you don't need to add these options unless you want to modify the values.
5 |
6 | ```php
7 | [
11 | 'pluginName' => 'Navigation',
12 | 'bypassProjectConfig' => false,
13 | ]
14 | ];
15 | ```
16 |
17 | ## Configuration options
18 | - `pluginName` - Optionally change the name of the plugin.
19 | - `bypassProjectConfig` - Prevents navigations from being saved to Project Config. Be sure you know what you're doing with this!
20 |
21 | ## Control Panel
22 | You can also manage configuration settings through the Control Panel by visiting Settings → Navigation.
23 |
--------------------------------------------------------------------------------
/docs/get-started/installation-setup.md:
--------------------------------------------------------------------------------
1 | # Installation & Setup
2 | You can install Navigation via the plugin store, or through Composer.
3 |
4 | ## Craft Plugin Store
5 | To install **Navigation**, navigate to the _Plugin Store_ section of your Craft control panel, search for `Navigation`, and click the _Try_ button.
6 |
7 | ## Composer
8 | You can also add the package to your project using Composer and the command line.
9 |
10 | 1. Open your terminal and go to your Craft project:
11 | ```shell
12 | cd /path/to/project
13 | ```
14 |
15 | 2. Then tell Composer to require the plugin, and Craft to install it:
16 | ```shell
17 | composer require verbb/navigation && php craft plugin/install navigation
18 | ```
19 |
20 | ## Licensing
21 | You can try Navigation in a development environment for as long as you like. Once your site goes live, you are required to purchase a license for the plugin.
22 |
23 | For more information, see [Craft's Commercial Plugin Licensing](https://craftcms.com/docs/4.x/plugins.html#commercial-plugin-licensing).
24 |
--------------------------------------------------------------------------------
/docs/get-started/requirements.md:
--------------------------------------------------------------------------------
1 | # Requirements
2 |
3 | ## Craft CMS
4 | Navigation requires Craft CMS 5.0 or greater.
5 |
6 | ## PHP
7 | Navigation requires PHP 8.2 or greater.
--------------------------------------------------------------------------------
/docs/template-guides/available-variables.md:
--------------------------------------------------------------------------------
1 | # Available Variables
2 |
3 | The following methods are available to call in your Twig templates:
4 |
5 | ### `craft.navigation.nodes(params)`
6 | The `params` parameter can be either a string for the [Nav](docs:developers/nav) handle, or an object of [NodeQuery](docs:getting-elements/node-queries) params. You can also chain these same params to this function call.
7 |
8 | ```twig
9 | {# Fetch the `mainMenu` nodes #}
10 | {% set nodes = craft.navigation.nodes('mainMenu').all() %}
11 |
12 | {# Chain params to the `nodes()` function #}
13 | {% set nodes = craft.navigation.nodes()
14 | .handle('mainMenu')
15 | .site('default')
16 | .all() %}
17 |
18 | {# Or, pass them as an object #}
19 | {% set nodes = craft.navigation.nodes({
20 | handle: 'mainMenu',
21 | site: 'default',
22 | }).all() %}
23 | ```
24 |
25 | See [Node Queries](docs:getting-elements/node-queries)
26 |
27 | ### `craft.navigation.render(params, options)`
28 | The `params` parameter can be either a string for the [Nav](docs:developers/nav) handle, an object of [NodeQuery](docs:getting-elements/node-queries) params or a [NodeQuery](docs:getting-elements/node-queries) itself.
29 |
30 | ```twig
31 | {# Render the `mainMenu` navigation #}
32 | {{ craft.navigation.render('mainMenu') }}
33 |
34 | {# Render the `mainMenu` navigation for the `default` site #}
35 | {{ craft.navigation.render({
36 | handle: 'mainMenu',
37 | site: 'default',
38 | }) }}
39 |
40 | {# The same as above, but using a `NodeQuery` #}
41 | {% set nodeQuery = craft.navigation.nodes('mainMenu').site('default') %}
42 |
43 | {{ craft.navigation.render(nodeQuery) }}
44 | ```
45 |
46 | See [Rendering Nodes](docs:template-guides/rendering-nodes)
47 |
48 | ### `craft.navigation.breadcrumbs(options)`
49 | See [Breadcrumbs](docs:template-guides/breadcrumbs)
50 |
51 | ### `craft.navigation.getActiveNode(params, includeChildren)`
52 | The `params` parameter can be either a string for the [Nav](docs:developers/nav) handle, an object of [NodeQuery](docs:getting-elements/node-queries) params or a [NodeQuery](docs:getting-elements/node-queries) itself.
53 |
54 | See [Rendering Nodes](docs:template-guides/rendering-nodes)
55 |
56 | ### `craft.navigation.tree(params)`
57 | Returns a full tree structure of nodes as a nested array.
58 |
59 | The `params` parameter can be either a string for the [Nav](docs:developers/nav) handle, an object of [NodeQuery](docs:getting-elements/node-queries) params or a [NodeQuery](docs:getting-elements/node-queries) itself.
60 |
61 | ### `craft.navigation.getNavById(id)`
62 | Returns the navigation for the provided id.
63 |
64 | ### `craft.navigation.getNavByHandle(handle)`
65 | Returns the navigation for the provided handle.
66 |
--------------------------------------------------------------------------------
/docs/template-guides/breadcrumbs.md:
--------------------------------------------------------------------------------
1 | # Breadcrumbs
2 |
3 | ## `craft.navigation.breadcrumbs(options)`
4 | You can retrieve a list of breadcrumbs based on the current URL. They are not based on your navigation items, and instead use the current URL segments. The function will look up any element that matches the URI for the segment. If not found, the segment itself will be used.
5 |
6 | ```twig
7 | {% for crumb in craft.navigation.breadcrumbs() %}
8 | {{ crumb.title }}
9 | {% endfor %}
10 | ```
11 |
12 | The `crumb` variable returned from the `breadcrumbs()` function will be an array with the following options. This will either contain information on a matched element, or information derived from the segment.
13 |
14 | ### Properties
15 |
16 | | Property | Description
17 | | - | -
18 | | `title` | The title of the segment. Either the element's title, or derived from the segment.
19 | | `url` | The absolute URL for the segment, for the current site.
20 | | `segment` | The segment portion of the current URL.
21 | | `isElement` | Whether the segment is an element or not.
22 | | `element` | The element object (if an element).
23 | | `elementId` | The ID of the element (if an element).
24 | | `elementType` | The type of element (if an element).
25 |
26 | You can also pass in options to the `breadcrumbs()` function. For example, you could limit the number of breadcrumb items returned.
27 |
28 | ```twig
29 | {% for crumb in craft.navigation.breadcrumbs({ limit: 10 }) %}
30 | {{ crumb.title }}
31 | {% endfor %}
32 | ```
33 |
34 | ### Available Options
35 |
36 | | Options | Description
37 | | - | -
38 | | `limit` | The number to limit returned breadcrumbs item by.
39 |
--------------------------------------------------------------------------------
/docs/template-guides/eager-loading.md:
--------------------------------------------------------------------------------
1 | # Eager-Loading
2 | Craft features a concept called [Eager-Loading](https://craftcms.com/docs/3.x/dev/eager-loading-elements.html), allowing some significant performance benefits when dealing with elements.
3 |
4 | We can make use of this too, to speed up rendering of navigation nodes. However, you'll only really see benefits from eager-loading when your navigation have multiple levels. A single level navigation won't get any benefit from eager-loading.
5 |
6 | ## craft.navigation.render()
7 | If you're using the `craft.navigation.render()` Twig function, there's nothing you need to do! Navigation eager-loads nested navigations automatically.
8 |
9 | ## craft.navigation.nodes()
10 | Let's take a look at an example navigation setup. We have the following navigation structure, consisting of 3-levels of nodes.
11 |
12 | ```
13 | - Node 1
14 | - Node 1-1
15 | - Node 1-2
16 | - Node 1-3
17 | - Node 1-4
18 | - Node 2
19 | - Node 3
20 | - Node 3-1
21 | - Node 3-2
22 | - Node 3-3
23 | - Node 4
24 | - Node 5
25 | - Node 6
26 | - Node 6-1
27 | - Node 6-2
28 | - Node 6-3
29 | - Node 7
30 | - Node 8
31 | - Node 9
32 | - Node 10
33 | ```
34 |
35 | And we'll use the following Twig to output the nodes:
36 |
37 | ```twig
38 | {% set nodes = craft.navigation.nodes('mainMenu').level(1).all() %}
39 |
40 | {% for node in nodes %}
41 | {{ node.link }}
42 |
43 | {% for subnode in node.children.all() %}
44 | {{ subnode.link }}
45 | {% endfor %}
46 | {% endfor %}
47 | ```
48 |
49 | Whilst this will work fine, we're also producing a lot of database queries. The above should generate close to **32 queries** to fetch nested nodes. We can improve this with eager-loading the children and descendants.
50 |
51 | ```twig
52 | {% set nodes = craft.navigation.nodes('mainMenu').level(1).with(['children']).all() %}
53 |
54 | {% for node in nodes %}
55 | {{ node.link }}
56 |
57 | {% for subnode in node.children %}
58 | {{ subnode.link }}
59 | {% endfor %}
60 | {% endfor %}
61 | ```
62 |
63 | There's two main things to note here, we're using `with(['children'])` in our query, and we're not using `all()` to loop through sub nodes. This will bring our query count down to **10 queries** - a vast improvement over the former template.
64 |
65 | If you have a third-level in your navigation, you'll need to eager-load those to, and so on - depending on how many levels your navigation has.
66 |
67 | ```twig
68 | {% set nodes = craft.navigation.nodes('mainMenu').level(1).with(['children.children']).all() %}
69 |
70 | {% for node in nodes %}
71 | {{ node.link }}
72 |
73 | {% for subnode in node.children %}
74 | {{ subnode.link }}
75 |
76 | {% for innernode in subnode.children %}
77 | {{ innernode.link }}
78 | {% endfor %}
79 | {% endfor %}
80 | {% endfor %}
81 | ```
--------------------------------------------------------------------------------
/docs/template-guides/navigation-field.md:
--------------------------------------------------------------------------------
1 | # Navigation Field
2 |
3 | You can use the navigation field to allow entries and other elements to select a navigation to show. In your templates, when calling the field (ie `{{ entry.navigationField }}`) you'll be returned the handle for the navigation.
4 |
5 | ```twig
6 | {{ craft.navigation.render(entry.navigationField) }}
7 | ```
--------------------------------------------------------------------------------
/docs/template-guides/rendering-nodes.md:
--------------------------------------------------------------------------------
1 | # Rendering Nodes
2 | You have two options for outputting your menu:
3 |
4 | ## Render Function
5 |
6 | ### craft.navigation.render()
7 | The easy option - let Navigation output the list items for you. This will generate a nested `` list of navigation items. You can also pass in additional classes for each element.
8 |
9 | ```twig
10 | {{ craft.navigation.render('navHandle', {
11 | ulClass: 'nav-items',
12 | liClass: 'nav-item',
13 | aClass: 'nav-link',
14 | activeClass: 'nav-active',
15 | hasChildrenClass: 'nav-has-children',
16 | ulAttributes: {
17 | 'data-attr': 'Some value',
18 | },
19 | liAttributes: {
20 | 'data-attr': 'Some value',
21 | },
22 | aAttributes: {
23 | 'data-attr': 'Some value',
24 | },
25 | }) }}
26 | ```
27 |
28 | ## Querying Nodes
29 |
30 | ### craft.navigation.nodes()
31 | For more fine-grained control over the navigation output, you can call nodes directly. As nodes are elements, output is a breeze using Craft's `{% nav %}` tag, so you don't have to deal with recursive macros.
32 |
33 | :::tip
34 | Once you've mastered rendering your nodes, check out the [Eager-Loading](docs:template-guides/eager-loading) docs for performance gains.
35 | :::
36 |
37 | ```twig
38 | {% set nodes = craft.navigation.nodes()
39 | .handle('mainMenu')
40 | .all() %}
41 |
42 | {# Or - alternatively #}
43 | {% set nodes = craft.navigation.nodes('mainMenu').all() %}
44 |
45 |
46 | {% nav node in nodes %}
47 |
48 | {{ node.link }}
49 |
50 | {% ifchildren %}
51 |
54 | {% endifchildren %}
55 |
56 | {% endnav %}
57 |
58 | ```
59 |
60 | If you'd rather not use the `{% nav %}` functionality, you can create your own recursive macro to loop through nodes. In addition, you'll also only want to initially output the first level of nodes using the `level: 1` parameter.
61 |
62 | ```twig
63 | {% import _self as macros %}
64 |
65 | {% set nodes = craft.navigation.nodes()
66 | .handle('mainMenu')
67 | .level(1)
68 | .all() %}
69 |
70 |
71 | {% for node in nodes %}
72 | {{ macros.navigationNodes(node) }}
73 | {% endfor %}
74 |
75 |
76 | {% macro navigationNodes(node) %}
77 | {% import _self as macros %}
78 |
79 |
80 | {{ node.link }}
81 |
82 | {% if node.children %}
83 |
84 | {% for subnode in node.children %}
85 | {{ macros.navigationNodes(subnode) }}
86 | {% endfor %}
87 |
88 | {% endif %}
89 |
90 | {% endmacro %}
91 | ```
92 |
93 | Don't forget, that calling `craft.navigation.nodes()` means you're querying Nodes, so it's a good idea to brush up on [querying elements](docs:getting-elements/node-queries).
94 |
95 | ### Custom rendering
96 | When looping through each node, you'll have access to all the attributes of a [Node](docs:developers/node), and you have full control over what to show. Take a look at the following example, that the `craft.navigation.render()` function uses under the hood:
97 |
98 | ```twig
99 | {% set nodes = craft.navigation.nodes('mainMenu').all() %}
100 |
101 |
102 | {% nav node in nodes %}
103 |
104 |
105 | {{- node.title -}}
106 |
107 |
108 | {% ifchildren %}
109 |
110 | {% children %}
111 |
112 | {% endifchildren %}
113 |
114 | {% endnav %}
115 |
116 | ```
117 |
118 | ## craft.navigation.getActiveNode()
119 | You can get the active node of any navigation through this tag. Often useful if you want to output an additional navigation area on your site that's contextual to the current node you're on.
120 |
121 | You can also provide any of the normal query parameters you normally would with `craft.navigation.nodes()`.
122 |
123 | ```twig
124 | {# Represents a Node element #}
125 | {% set activeNode = craft.navigation.getActiveNode({ handle: 'mainMenu' }) %}
126 |
127 |
128 | {# Start looping through any nested nodes, starting at the currently active one #}
129 | {% set nodes = craft.navigation.nodes()
130 | .descendantOf(activeNode)
131 | .all() %}
132 |
133 | {% nav node in nodes %}
134 |
135 | {{ node.title }}
136 | {{ node.active }}
137 |
138 | {% ifchildren %}
139 |
140 | {% children %}
141 |
142 | {% endifchildren %}
143 |
144 | {% endnav %}
145 |
146 | ```
147 |
148 | Do note that this will only match against the exact node matching the current URL. If you're on a child of a parent that matches as active, this function will not return the parent as being active.
149 |
150 | To illustrate, take for example two URLs:
151 | - my-site.com/news
152 | - my-site.com/news/some-article
153 |
154 | And the navigation included a node with the URL for `/news` (either a manual link, or linked to an entry element). You output the following in your templates:
155 |
156 | ```twig
157 | {{ craft.navigation.getActiveNode({ handle: 'mainMenu' }) }}
158 | ```
159 |
160 | If you were on the URL `/news` it would return that you're on the active node. If you were on `/news/some-article` it would return that this is **not** the active node. Navigation would be looking for a node with a URL that matches `/news/some-article`, and because it can't find one, it will not return an active page.
161 |
162 | However, its common you'll want to highlight the News node as being active, if your site uses nested navigation. That way, it shows to your users that you're in the "News" section of the site. In this instance you can pass a second attribute to `getActiveNode()` to include child and parent matching. For example:
163 |
164 | ```twig
165 | {{ craft.navigation.getActiveNode({ handle: 'mainMenu' }, true) }}
166 | ```
167 |
168 | In this case, when you are on the URL `/news`, `/news/some-article` or any other URL that includes `/news` it would return that "News" is the active node.
169 |
--------------------------------------------------------------------------------
/src/assetbundles/NavigationAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@verbb/navigation/resources/dist";
17 |
18 | $this->depends = [
19 | VerbbCpAsset::class,
20 | CpAsset::class,
21 | ];
22 |
23 | $this->css = [
24 | 'css/navigation.css',
25 | ];
26 |
27 | $this->js = [
28 | 'js/navigation.js',
29 | ];
30 |
31 | parent::init();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/base/NodeType.php:
--------------------------------------------------------------------------------
1 | [
37 | 'breadcrumbs' => Breadcrumbs::class,
38 | 'elements' => Elements::class,
39 | 'navs' => Navs::class,
40 | 'nodes' => Nodes::class,
41 | 'nodeTypes' => NodeTypes::class,
42 | ],
43 | ];
44 | }
45 |
46 |
47 | // Public Methods
48 | // =========================================================================
49 |
50 | public function getBreadcrumbs(): Breadcrumbs
51 | {
52 | return $this->get('breadcrumbs');
53 | }
54 |
55 | public function getElements(): Elements
56 | {
57 | return $this->get('elements');
58 | }
59 |
60 | public function getNavs(): Navs
61 | {
62 | return $this->get('navs');
63 | }
64 |
65 | public function getNodes(): Nodes
66 | {
67 | return $this->get('nodes');
68 | }
69 |
70 | public function getNodeTypes(): NodeTypes
71 | {
72 | return $this->get('nodeTypes');
73 | }
74 |
75 | }
--------------------------------------------------------------------------------
/src/console/controllers/NavsController.php:
--------------------------------------------------------------------------------
1 | Craft 4 migration issue with empty sites.
25 | */
26 | public function actionFixSites(): int
27 | {
28 | $navs = (new Query())
29 | ->select(['*'])
30 | ->from('{{%navigation_navs}}')
31 | ->all();
32 |
33 | foreach ($navs as $nav) {
34 | $navSite = (new Query())
35 | ->select(['*'])
36 | ->from('{{%navigation_navs_sites}}')
37 | ->where(['navId' => $nav['id']])
38 | ->all();
39 |
40 | if (!$navSite) {
41 | foreach (Craft::$app->getSites()->getAllSites() as $site) {
42 | Db::insert('{{%navigation_navs_sites}}', [
43 | 'navId' => $nav['id'],
44 | 'siteId' => $site->id,
45 | 'enabled' => true,
46 | 'dateCreated' => Db::prepareDateForDb(new DateTime()),
47 | 'dateUpdated' => Db::prepareDateForDb(new DateTime()),
48 | 'uid' => StringHelper::UUID(),
49 | ]);
50 | }
51 | }
52 | }
53 |
54 | return ExitCode::OK;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/controllers/BaseController.php:
--------------------------------------------------------------------------------
1 | getSettings();
23 |
24 | return $this->renderTemplate('navigation/settings', [
25 | 'settings' => $settings,
26 | ]);
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/src/controllers/NodesController.php:
--------------------------------------------------------------------------------
1 | requirePostRequest();
21 | $this->requireAcceptsJson();
22 |
23 | $nodesService = Navigation::$plugin->getNodes();
24 |
25 | $nodesPost = $this->request->getRequiredParam('nodes');
26 |
27 | foreach ($nodesPost as $key => $nodePost) {
28 | $node = $this->_setNodeFromPost("nodes.{$key}.");
29 |
30 | // Add this new node to the nav, to assist with validation
31 | $nodesService->setTempNodes([$node]);
32 |
33 | if (!Craft::$app->getElements()->saveElement($node, true)) {
34 | return $this->asModelFailure($node, Craft::t('navigation', 'Couldn’t add node.'), 'node');
35 | }
36 | }
37 |
38 | return $this->asSuccess(Craft::t('navigation', 'Node{plural} added.', ['plural' => count($nodesPost) > 1 ? 's' : '']));
39 | }
40 |
41 | public function actionGetParentOptions(): Response
42 | {
43 | $this->requirePostRequest();
44 | $this->requireAcceptsJson();
45 |
46 | $nodesService = Navigation::$plugin->getNodes();
47 | $navId = $this->request->getRequiredParam('navId');
48 | $siteId = $this->request->getParam('siteId');
49 |
50 | $nodes = $nodesService->getNodesForNav($navId, $siteId);
51 |
52 | $options = [];
53 |
54 | if ($nodes) {
55 | $options = $nodesService->getParentOptions($nodes, $nodes[0]->nav);
56 | }
57 |
58 | return $this->asJson(['options' => $options]);
59 | }
60 |
61 |
62 | // Private Methods
63 | // =========================================================================
64 |
65 | private function _setNodeFromPost($prefix = ''): Node
66 | {
67 | // Because adding multiple nodes and saving a single node use this same function, we have to jump
68 | // through some hoops to get the correct post params properties.
69 | $node = new Node();
70 | $node->title = $this->request->getParam("{$prefix}title", $node->title);
71 | $node->enabled = (bool)$this->request->getParam("{$prefix}enabled", $node->enabled);
72 | $node->enabledForSite = (bool)$this->request->getParam("{$prefix}enabledForSite", $node->enabledForSite);
73 |
74 | $elementId = $this->request->getParam("{$prefix}elementId", $node->elementId);
75 |
76 | // Handle elementselect field
77 | if (is_array($elementId)) {
78 | $elementId = $elementId[0] ?? null;
79 | }
80 |
81 | $node->elementId = $elementId;
82 | $node->elementSiteId = $this->request->getParam("{$prefix}elementSiteId", $node->elementSiteId);
83 | $node->siteId = $this->request->getParam("{$prefix}siteId", $node->siteId);
84 | $node->navId = $this->request->getParam("{$prefix}navId", $node->navId);
85 | $node->url = $this->request->getParam("{$prefix}url", $node->url);
86 | $node->type = $this->request->getParam("{$prefix}type", $node->type);
87 | $node->classes = $this->request->getParam("{$prefix}classes", $node->classes);
88 | $node->urlSuffix = $this->request->getParam("{$prefix}urlSuffix", $node->urlSuffix);
89 | $node->customAttributes = Json::decodeIfJson($this->request->getParam("{$prefix}customAttributes")) ?? $node->customAttributes;
90 | $node->data = Json::decodeIfJson($this->request->getParam("{$prefix}data")) ?? $node->data;
91 | $node->newWindow = (bool)$this->request->getParam("{$prefix}newWindow", $node->newWindow);
92 |
93 | $node->parentId = $this->request->getParam("{$prefix}parentId");
94 |
95 | // Set field values.
96 | $node->setFieldValuesFromRequest('fields');
97 |
98 | return $node;
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/src/elements/conditions/NodeCondition.php:
--------------------------------------------------------------------------------
1 | matchValue((string)$element->type);
36 | }
37 |
38 |
39 | // Protected Methods
40 | // =========================================================================
41 |
42 | protected function options(): array
43 | {
44 | $options = [];
45 |
46 | $registeredElements = Navigation::$plugin->getElements()->getRegisteredElements();
47 | $registeredNodeTypes = Navigation::$plugin->getNodeTypes()->getRegisteredNodeTypes();
48 |
49 | foreach ($registeredElements as $registeredElement) {
50 | $options[$registeredElement['type']] = $registeredElement['label'];
51 | }
52 |
53 | foreach ($registeredNodeTypes as $nodeType) {
54 | $options[get_class($nodeType)] = $nodeType->displayName();
55 | }
56 |
57 | return $options;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/elements/db/NodeQuery.php:
--------------------------------------------------------------------------------
1 | withStructure)) {
40 | $this->withStructure = true;
41 | }
42 |
43 | parent::init();
44 | }
45 |
46 | public function elementId($value): static
47 | {
48 | $this->elementId = $value;
49 | return $this;
50 | }
51 |
52 | public function elementSiteId($value): static
53 | {
54 | $this->slug = $value;
55 | return $this;
56 | }
57 |
58 | public function navId($value): static
59 | {
60 | $this->navId = $value;
61 | return $this;
62 | }
63 |
64 | public function navHandle($value): static
65 | {
66 | $this->handle = $value;
67 | return $this;
68 | }
69 |
70 | public function nav($value): static
71 | {
72 | if ($value instanceof NavModel) {
73 | $this->structureId = ($value->structureId ?: false);
74 | $this->navId = $value->id;
75 | } else if ($value !== null) {
76 | $this->navId = (new Query())
77 | ->select(['id'])
78 | ->from('{{%navigation_navs}}')
79 | ->where(Db::parseParam('handle', $value))
80 | ->column();
81 | } else {
82 | $this->navId = null;
83 | }
84 |
85 | return $this;
86 | }
87 |
88 | public function type($value): static
89 | {
90 | $this->type = $value;
91 | return $this;
92 | }
93 |
94 | public function element($value): static
95 | {
96 | $this->element = $value;
97 | return $this;
98 | }
99 |
100 | public function handle($value): static
101 | {
102 | $this->handle = $value;
103 | return $this;
104 | }
105 |
106 | public function hasUrl(bool $value = false): static
107 | {
108 | $this->hasUrl = $value;
109 | return $this;
110 | }
111 |
112 | // We set the active state on each node, however it gets trickier when trying to do things like settings the active
113 | // state when a child is active, which involves firing off additional element queries for each node's children,
114 | // which quickly blow out queries. So instead, do this when the elements are populated
115 | public function populate($rows): array
116 | {
117 | // Let the parent class handle this like normal
118 | $rows = parent::populate($rows);
119 |
120 | // Store all processed items by their ID, we need to lookup parents later
121 | $processedRows = ArrayHelper::index($rows, 'id');
122 |
123 | foreach ($rows as $row) {
124 | // If the current node is active, and it has a parent, set its active state
125 | if (is_a($row, Node::class) && $row->active) {
126 | $ancestors = $row->ancestors->all();
127 |
128 | foreach ($ancestors as $ancestor) {
129 | if (isset($processedRows[$ancestor->id])) {
130 | $processedRows[$ancestor->id]->isActive = true;
131 | }
132 | }
133 | }
134 | }
135 |
136 | return $rows;
137 | }
138 |
139 |
140 | // Protected Methods
141 | // =========================================================================
142 |
143 | protected function beforePrepare(): bool
144 | {
145 | $this->joinElementTable('navigation_nodes');
146 | $this->subQuery->innerJoin('{{%navigation_navs}} navigation_navs', '[[navigation_nodes.navId]] = [[navigation_navs.id]]');
147 |
148 | $this->query->select([
149 | 'navigation_nodes.id',
150 | 'navigation_nodes.elementId',
151 | 'navigation_nodes.navId',
152 | 'navigation_nodes.url',
153 | 'navigation_nodes.type',
154 | 'navigation_nodes.classes',
155 | 'navigation_nodes.newWindow',
156 | 'navigation_nodes.customAttributes',
157 | 'navigation_nodes.urlSuffix',
158 | 'navigation_nodes.data',
159 |
160 | // Join the element's uri onto the same query
161 | 'element_item_sites.uri AS elementUrl',
162 | ]);
163 |
164 | if ($this->id) {
165 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.id', $this->id));
166 | }
167 |
168 | if ($this->elementId) {
169 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.elementId', $this->elementId));
170 | }
171 |
172 | if ($this->navId) {
173 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.navId', $this->navId));
174 | }
175 |
176 | if ($this->type) {
177 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.type', $this->type));
178 | }
179 |
180 | if ($this->classes) {
181 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.classes', $this->classes));
182 | }
183 |
184 | if ($this->urlSuffix) {
185 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.urlSuffix', $this->urlSuffix));
186 | }
187 |
188 | if ($this->customAttributes) {
189 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.customAttributes', $this->customAttributes));
190 | }
191 |
192 | if ($this->data) {
193 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.data', $this->data));
194 | }
195 |
196 | if ($this->newWindow) {
197 | $this->subQuery->andWhere(Db::parseParam('navigation_nodes.newWindow', $this->newWindow));
198 | }
199 |
200 | if ($this->handle) {
201 | $this->subQuery->andWhere(Db::parseParam('navigation_navs.handle', $this->handle));
202 | }
203 |
204 | if ($this->hasUrl) {
205 | $this->subQuery->andWhere(['or', ['not', ['navigation_nodes.elementId' => null, 'navigation_nodes.elementId' => '']], ['not', ['navigation_nodes.url' => null, 'navigation_nodes.url' => '']]]);
206 | }
207 |
208 | return parent::beforePrepare();
209 | }
210 |
211 | protected function afterPrepare(): bool
212 | {
213 | if (Craft::$app->getDb()->getIsMysql()) {
214 | $sql = 'CAST([[elements_sites.slug]] AS UNSIGNED)';
215 | } else {
216 | $sql = 'CAST([[elements_sites.slug]] AS INTEGER)';
217 | }
218 |
219 | // Join the element sites table (again) for the linked element
220 | $this->query->leftJoin('{{%elements_sites}} element_item_sites', '[[navigation_nodes.elementId]] = [[element_item_sites.elementId]] AND ' . $sql . ' = [[element_item_sites.siteId]]');
221 |
222 | return parent::afterPrepare();
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/events/NavEvent.php:
--------------------------------------------------------------------------------
1 | $this->attribute,
35 | 'name' => $this->attribute,
36 | 'cols' => [
37 | 'attribute' => [
38 | 'type' => 'singleline',
39 | 'heading' => Craft::t('navigation', 'Attribute'),
40 | ],
41 | 'value' => [
42 | 'type' => 'singleline',
43 | 'heading' => Craft::t('navigation', 'Value'),
44 | 'code' => true,
45 | ],
46 | ],
47 | 'rows' => $element->customAttributes ?? [],
48 | 'allowAdd' => true,
49 | 'allowDelete' => true,
50 | 'allowReorder' => true,
51 | ]);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/fieldlayoutelements/NewWindowField.php:
--------------------------------------------------------------------------------
1 | $this->attribute,
35 | 'on' => $element->newWindow,
36 | ]);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/fieldlayoutelements/NodeTypeElements.php:
--------------------------------------------------------------------------------
1 | isElement()) {
48 | $classNameParts = explode('\\', $element->type);
49 | $typeClass = array_pop($classNameParts);
50 |
51 | $siteId = $element->getElement()->siteId ?? null;
52 | $html = Html::hiddenInput('linkedElementSiteId', $siteId, [
53 | 'id' => 'linkedElementSiteId',
54 | ]);
55 |
56 | $nav = $element->getNav();
57 | $sources = $nav->permissions[$element->type]['permissions'] ?? '*';
58 |
59 | $html .= Cp::elementSelectFieldHtml([
60 | 'label' => Craft::t('navigation', 'Linked to {element}', ['element' => $typeClass]),
61 | 'instructions' => Craft::t('navigation', 'The element this node is linked to.'),
62 | 'id' => 'linkedElementId',
63 | 'name' => 'linkedElementId',
64 | 'elements' => $element->getElement() ? [$element->getElement()] : [],
65 | 'elementType' => $element->type,
66 | 'sources' => $sources,
67 | 'showSiteMenu' => true,
68 | 'required' => true,
69 | 'limit' => 1,
70 | 'modalStorageKey' => 'navigation.linkedElementId',
71 | ]);
72 |
73 | $namespace = Craft::$app->getView()->getNamespace();
74 |
75 | $html .= "";
76 |
77 | return $html;
78 | }
79 |
80 | if ($nodeType = $element->nodeType()) {
81 | return $nodeType->getModalHtml();
82 | }
83 |
84 | return null;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/fieldlayoutelements/UrlSuffixField.php:
--------------------------------------------------------------------------------
1 | getView()->renderTemplate('navigation/_field/settings', [
50 |
51 | ]);
52 | }
53 |
54 | public function getContentGqlType(): Type|array
55 | {
56 | return [
57 | 'name' => $this->handle,
58 | 'type' => Type::listOf(NodeInterface::getType()),
59 | 'args' => NodeArguments::getArguments(),
60 | 'resolve' => NodeResolver::class . '::resolve',
61 | ];
62 | }
63 |
64 |
65 | // Protected Methods
66 | // =========================================================================
67 |
68 | protected function inputHtml(mixed $value, ?ElementInterface $element, bool $inline): string
69 | {
70 | $navs = Navigation::$plugin->getNavs()->getAllNavs();
71 |
72 | $options = [
73 | '' => Craft::t('navigation', 'Select a navigation'),
74 | ];
75 |
76 | foreach ($navs as $nav) {
77 | $options[$nav->handle] = $nav->name;
78 | }
79 |
80 | $id = Html::id($this->handle);
81 |
82 | return Craft::$app->getView()->renderTemplate('navigation/_field/input', [
83 | 'id' => $id,
84 | 'name' => $this->handle,
85 | 'value' => $value,
86 | 'options' => $options,
87 | ]);
88 | }
89 |
90 | protected function optionsSettingLabel(): string
91 | {
92 | return Craft::t('navigation', 'Navigation Options');
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/gql/arguments/NodeArguments.php:
--------------------------------------------------------------------------------
1 | [
21 | 'name' => 'nav',
22 | 'type' => Type::listOf(Type::string()),
23 | 'description' => 'Narrows the query results based on the navigation the node belongs to.',
24 | ],
25 | 'navHandle' => [
26 | 'name' => 'navHandle',
27 | 'type' => Type::string(),
28 | 'description' => 'Narrows the query results based on the provided navigation handle.',
29 | ],
30 | 'navId' => [
31 | 'name' => 'navId',
32 | 'type' => Type::int(),
33 | 'description' => 'Narrows the query results based on the provided navigation ID.',
34 | ],
35 | 'type' => [
36 | 'name' => 'type',
37 | 'type' => Type::listOf(Type::string()),
38 | 'description' => 'Narrows the query results based on the node’s type.',
39 | ],
40 | ]);
41 | }
42 |
43 | public static function getContentArguments(): array
44 | {
45 | $navFieldArguments = Craft::$app->getGql()->getContentArguments(Navigation::$plugin->getNavs()->getAllNavs(), Node::class);
46 |
47 | return array_merge(parent::getContentArguments(), $navFieldArguments);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/gql/interfaces/NodeInterface.php:
--------------------------------------------------------------------------------
1 | static::getName(),
37 | 'fields' => self::class . '::getFieldDefinitions',
38 | 'description' => 'This is the interface implemented by all nodes.',
39 | 'resolveType' => function(Node $value) {
40 | return $value->getGqlTypeName();
41 | },
42 | ]));
43 |
44 | NodeGenerator::generateTypes();
45 |
46 | return $type;
47 | }
48 |
49 | public static function getName(): string
50 | {
51 | return 'NodeInterface';
52 | }
53 |
54 | public static function getFieldDefinitions(): array
55 | {
56 | return Craft::$app->getGql()->prepareFieldDefinitions(array_merge(parent::getFieldDefinitions(), [
57 | 'elementId' => [
58 | 'name' => 'elementId',
59 | 'type' => Type::int(),
60 | 'description' => 'The ID of the element this node is linked to.',
61 | ],
62 | 'navId' => [
63 | 'name' => 'navId',
64 | 'type' => Type::int(),
65 | 'description' => 'The ID of the navigation this node belongs to.',
66 | ],
67 | 'navHandle' => [
68 | 'name' => 'navHandle',
69 | 'type' => Type::string(),
70 | 'description' => 'The handle of the navigation this node belongs to.',
71 | 'resolve' => function($node) {
72 | return $node->nav->handle;
73 | },
74 | ],
75 | 'navName' => [
76 | 'name' => 'navName',
77 | 'type' => Type::string(),
78 | 'description' => 'The name of the navigation this node belongs to.',
79 | 'resolve' => function($node) {
80 | return $node->nav->name;
81 | },
82 | ],
83 | 'type' => [
84 | 'name' => 'type',
85 | 'type' => Type::string(),
86 | 'description' => 'The type of node this is.',
87 | ],
88 | 'typeLabel' => [
89 | 'name' => 'typeLabel',
90 | 'type' => Type::string(),
91 | 'description' => 'The display name for the type of node this is.',
92 | ],
93 | 'classes' => [
94 | 'name' => 'classes',
95 | 'type' => Type::string(),
96 | 'description' => 'Any additional classes for the node.',
97 | ],
98 | 'urlSuffix' => [
99 | 'name' => 'urlSuffix',
100 | 'type' => Type::string(),
101 | 'description' => 'The URL for this navigation item.',
102 | ],
103 | 'customAttributes' => [
104 | 'name' => 'customAttributes',
105 | 'type' => Type::listOf(CustomAttributeGenerator::generateType()),
106 | 'description' => 'Any additional custom attributes for the node.',
107 | ],
108 | 'data' => [
109 | 'name' => 'data',
110 | 'type' => Type::string(),
111 | 'description' => 'Any additional data for the node.',
112 | ],
113 | 'newWindow' => [
114 | 'name' => 'newWindow',
115 | 'type' => Type::string(),
116 | 'description' => 'Whether this node should open in a new window.',
117 | ],
118 | 'url' => [
119 | 'name' => 'url',
120 | 'type' => Type::string(),
121 | 'description' => 'The node’s full URL',
122 | ],
123 | 'nodeUri' => [
124 | 'name' => 'nodeUri',
125 | 'type' => Type::string(),
126 | 'description' => 'The node’s URI',
127 | ],
128 | 'children' => [
129 | 'name' => 'children',
130 | 'args' => NodeArguments::getArguments(),
131 | 'type' => Type::listOf(NodeInterfaceLocal::getType()),
132 | 'description' => 'The node’s children. Accepts the same arguments as the `nodes` query.',
133 | ],
134 | 'parent' => [
135 | 'name' => 'parent',
136 | 'type' => NodeInterfaceLocal::getType(),
137 | 'description' => 'The node’s parent.',
138 | ],
139 | 'element' => [
140 | 'name' => 'element',
141 | 'type' => Element::getType(),
142 | 'description' => 'The element the node links to.',
143 | 'resolve' => function($node) {
144 | // Ensure we have permission to query the element type, to prevent errors thrown
145 | if (GqlHelper::canQueryNodeElement($node)) {
146 | return $node->getElement();
147 | }
148 |
149 | return null;
150 | },
151 | ],
152 | ]), self::getName());
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/gql/queries/NodeQuery.php:
--------------------------------------------------------------------------------
1 | [
26 | 'type' => Type::listOf(NodeInterface::getType()),
27 | 'args' => NodeArguments::getArguments(),
28 | 'resolve' => NodeResolver::class . '::resolve',
29 | 'description' => 'This query is used to query for nodes.',
30 | ],
31 | 'navigationNode' => [
32 | 'type' => NodeInterface::getType(),
33 | 'args' => NodeArguments::getArguments(),
34 | 'resolve' => NodeResolver::class . '::resolveOne',
35 | 'description' => 'This query is used to query for a single node.',
36 | ],
37 | ];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/gql/resolvers/NodeResolver.php:
--------------------------------------------------------------------------------
1 | $fieldName) {
24 | return ElementCollection::empty();
25 | }
26 |
27 | $query = Node::find()->navHandle($source->$fieldName);
28 | }
29 |
30 | if (!$query instanceof ElementQuery) {
31 | return $query;
32 | }
33 |
34 | foreach ($arguments as $key => $value) {
35 | $query->$key($value);
36 | }
37 |
38 | $pairs = GqlHelper::extractAllowedEntitiesFromSchema('read');
39 |
40 | if (!GqlHelper::canQueryNavigation()) {
41 | return ElementCollection::empty();
42 | }
43 |
44 | if (!GqlHelper::canSchema('navigationNavs.all')) {
45 | $query->andWhere(['in', 'navId', array_values(Db::idsByUids('{{%navigation_navs}}', $pairs['navigationNavs']))]);
46 | }
47 |
48 | return $query;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/gql/types/CustomAttributeType.php:
--------------------------------------------------------------------------------
1 | Type::string(),
18 | 'value' => Type::string(),
19 | ];
20 |
21 | return Craft::$app->getGql()->prepareFieldDefinitions($contentFields, $typeName);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/gql/types/NodeType.php:
--------------------------------------------------------------------------------
1 | fieldName;
31 |
32 | return match ($fieldName) {
33 | 'navHandle' => $source->getNav()->handle,
34 | default => parent::resolve($source, $arguments, $context, $resolveInfo),
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/gql/types/generators/CustomAttributeGenerator.php:
--------------------------------------------------------------------------------
1 | $typeName,
32 | 'fields' => function() use ($contentFields) {
33 | return $contentFields;
34 | },
35 | ]));
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/gql/types/generators/NodeGenerator.php:
--------------------------------------------------------------------------------
1 | getNavs()->getAllNavs();
24 | $gqlTypes = [];
25 |
26 | foreach ($navs as $nav) {
27 | $requiredContexts = Node::gqlScopesByContext($nav);
28 |
29 | if (!GqlHelper::isSchemaAwareOf($requiredContexts)) {
30 | if (!GqlHelper::canSchema('navigationNavs.all')) {
31 | continue;
32 | }
33 | }
34 |
35 | $type = static::generateType($nav);
36 | $gqlTypes[$type->name] = $type;
37 | }
38 |
39 | return $gqlTypes;
40 | }
41 |
42 | public static function generateType(mixed $context): mixed
43 | {
44 | $typeName = Node::gqlTypeNameByContext($context);
45 |
46 | if ($createdType = GqlEntityRegistry::getEntity($typeName)) {
47 | return $createdType;
48 | }
49 |
50 | $contentFieldGqlTypes = self::getContentFields($context);
51 | $navFields = Craft::$app->getGql()->prepareFieldDefinitions(array_merge(NodeInterface::getFieldDefinitions(), $contentFieldGqlTypes), $typeName);
52 |
53 | return GqlEntityRegistry::createEntity($typeName, new NodeType([
54 | 'name' => $typeName,
55 | 'fields' => function() use ($navFields) {
56 | return $navFields;
57 | },
58 | ]));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/helpers/Gql.php:
--------------------------------------------------------------------------------
1 | getElement()) {
26 | if ($element instanceof Entry) {
27 | return self::canQueryEntries();
28 | } else if ($element instanceof Category) {
29 | return self::canQueryCategories();
30 | } else if ($element instanceof Asset) {
31 | return self::canQueryAssets();
32 | }
33 | }
34 |
35 | return true;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/helpers/Plugin.php:
--------------------------------------------------------------------------------
1 | getNavs()->getAllNavs() as $nav) {
32 | $data[$nav->uid] = $nav->getConfig();
33 | }
34 |
35 | return $data;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/icon-mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
9 |
11 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
9 |
11 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/integrations/NodeFeedMeElement.php:
--------------------------------------------------------------------------------
1 | feed['elementType'] === Node::class) {
60 | $this->_processNestedNode($event);
61 | }
62 | });
63 | }
64 |
65 | public function getGroups(): array
66 | {
67 | return Navigation::$plugin->getNavs()->getAllNavs();
68 | }
69 |
70 | public function getQuery($settings, array $params = []): mixed
71 | {
72 | $query = Node::find()
73 | ->status(null)
74 | ->navId($settings['elementGroup'][Node::class])
75 | ->siteId(Hash::get($settings, 'siteId') ?: Craft::$app->getSites()->getPrimarySite()->id);
76 |
77 | Craft::configure($query, $params);
78 |
79 | return $query;
80 | }
81 |
82 | public function setModel($settings): \craft\base\Element
83 | {
84 | $this->element = new Node();
85 | $this->element->navId = $settings['elementGroup'][Node::class];
86 |
87 | $siteId = Hash::get($settings, 'siteId');
88 |
89 | if ($siteId) {
90 | $this->element->siteId = $siteId;
91 | }
92 |
93 | return $this->element;
94 | }
95 |
96 |
97 | // Protected Methods
98 | // =========================================================================
99 |
100 | protected function parseParentId($feedData, $fieldInfo): ?int
101 | {
102 | $value = $this->fetchSimpleValue($feedData, $fieldInfo);
103 |
104 | // In Craft 4, we need to explicitly call `setParentId()`, as it's no longer a property
105 | // only available as a setter method.
106 | $this->element->setParentId($value);
107 |
108 | return $value;
109 | }
110 |
111 | protected function parseElementId($feedData, $fieldInfo): ?int
112 | {
113 | $value = $this->fetchSimpleValue($feedData, $fieldInfo);
114 | $match = Hash::get($fieldInfo, 'options.match');
115 |
116 | // Element lookups must have a value to match against
117 | if ($value === null || $value === '') {
118 | return null;
119 | }
120 |
121 | $elementId = null;
122 |
123 | $query = (new Query())
124 | ->select(['elements.id', 'elements_sites.elementId', 'elements_sites.title', 'elements_sites.slug', 'elements_sites.uri', 'elements_sites.content'])
125 | ->from(['{{%elements}} elements'])
126 | ->innerJoin('{{%elements_sites}} elements_sites', '[[elements_sites.elementId]] = [[elements.id]]')
127 | ->andWhere(['dateDeleted' => null]);
128 |
129 | // Check if we're query a column or a custom field
130 | if (in_array($match, ['title', 'slug', 'uri'])) {
131 | $query->andWhere(['=', $match, $value]);
132 | } else {
133 | $contentQuery = Craft::$app->getDb()->getQueryBuilder()->jsonContains('content', [$match => $value]);
134 |
135 | $query->andWhere($contentQuery);
136 | }
137 |
138 | $result = $query->one();
139 |
140 | if ($result) {
141 | $elementId = $result['id'];
142 | }
143 |
144 | if ($elementId) {
145 | return $elementId;
146 | }
147 |
148 | return null;
149 | }
150 |
151 |
152 | // Private Methods
153 | // =========================================================================
154 |
155 | private function _processNestedNode($event): void
156 | {
157 | // Save the imported node as the parent, we'll need it in a sec
158 | $parentId = $event->element->id;
159 |
160 | // Check if we're mapping a node to start looking for children.
161 | $childrenNode = Hash::get($event->feed, 'fieldMapping.children.node');
162 |
163 | if (!$childrenNode) {
164 | return;
165 | }
166 |
167 | // Check if there's any children data for the node we've just imported
168 | $expandedData = Hash::expand($event->feedData, '/');
169 | $childrenData = Hash::get($expandedData, $childrenNode, []);
170 |
171 | foreach ($childrenData as $childData) {
172 | // Prep the data, cutting the nested content to the top of the array
173 | $newFeedData = Hash::flatten($childData, '/');
174 |
175 | $processedElementIds = [];
176 |
177 | // Directly modify the field mapping data, because we're programatically adding
178 | // the `parentId`, which cannot be mapped.
179 | $event->feed['fieldMapping']['parentId'] = [
180 | 'attribute' => true,
181 | 'default' => $parentId,
182 | ];
183 |
184 | // Trigger the import for each child
185 | Plugin::$plugin->getProcess()->processFeed(-1, $event->feed, $processedElementIds, $newFeedData);
186 | }
187 | }
188 |
189 | }
--------------------------------------------------------------------------------
/src/migrations/Install.php:
--------------------------------------------------------------------------------
1 | createTables();
18 | $this->createIndexes();
19 | $this->addForeignKeys();
20 |
21 | return true;
22 | }
23 |
24 | public function safeDown(): bool
25 | {
26 | $this->dropProjectConfig();
27 | $this->dropForeignKeys();
28 | $this->dropTables();
29 |
30 | return true;
31 | }
32 |
33 | public function createTables(): void
34 | {
35 | $this->archiveTableIfExists('{{%navigation_nodes}}');
36 | $this->createTable('{{%navigation_nodes}}', [
37 | 'id' => $this->integer()->notNull(),
38 | 'elementId' => $this->integer(),
39 | 'navId' => $this->integer()->notNull(),
40 | 'parentId' => $this->integer(),
41 | 'url' => $this->string(255),
42 | 'type' => $this->string(255),
43 | 'classes' => $this->string(255),
44 | 'urlSuffix' => $this->string(255),
45 | 'customAttributes' => $this->text(),
46 | 'data' => $this->text(),
47 | 'newWindow' => $this->boolean()->defaultValue(false),
48 | 'deletedWithNav' => $this->boolean()->null(),
49 | 'dateCreated' => $this->dateTime()->notNull(),
50 | 'dateUpdated' => $this->dateTime()->notNull(),
51 | 'uid' => $this->uid(),
52 | 'PRIMARY KEY(id)',
53 | ]);
54 |
55 | $this->archiveTableIfExists('{{%navigation_navs}}');
56 | $this->createTable('{{%navigation_navs}}', [
57 | 'id' => $this->primaryKey(),
58 | 'structureId' => $this->integer()->notNull(),
59 | 'name' => $this->string()->notNull(),
60 | 'handle' => $this->string()->notNull(),
61 | 'instructions' => $this->text(),
62 | 'sortOrder' => $this->smallInteger()->unsigned(),
63 | 'propagationMethod' => $this->string()->defaultValue(Nav::PROPAGATION_METHOD_ALL)->notNull(),
64 | 'maxNodes' => $this->integer(),
65 | 'maxNodesSettings' => $this->text(),
66 | 'permissions' => $this->text(),
67 | 'fieldLayoutId' => $this->integer(),
68 | 'defaultPlacement' => $this->enum('defaultPlacement', [Nav::DEFAULT_PLACEMENT_BEGINNING, Nav::DEFAULT_PLACEMENT_END])->defaultValue('end')->notNull(),
69 | 'dateCreated' => $this->dateTime()->notNull(),
70 | 'dateUpdated' => $this->dateTime()->notNull(),
71 | 'dateDeleted' => $this->dateTime()->null(),
72 | 'uid' => $this->uid(),
73 | ]);
74 |
75 | $this->archiveTableIfExists('{{%navigation_navs_sites}}');
76 | $this->createTable('{{%navigation_navs_sites}}', [
77 | 'id' => $this->primaryKey(),
78 | 'navId' => $this->integer()->notNull(),
79 | 'siteId' => $this->integer()->notNull(),
80 | 'enabled' => $this->boolean()->defaultValue(true)->notNull(),
81 | 'dateCreated' => $this->dateTime()->notNull(),
82 | 'dateUpdated' => $this->dateTime()->notNull(),
83 | 'uid' => $this->uid(),
84 | ]);
85 | }
86 |
87 | public function createIndexes(): void
88 | {
89 | $this->createIndex(null, '{{%navigation_nodes}}', ['navId'], false);
90 | $this->createIndex(null, '{{%navigation_navs}}', ['handle'], false);
91 | $this->createIndex(null, '{{%navigation_navs}}', ['structureId'], false);
92 | $this->createIndex(null, '{{%navigation_navs}}', ['fieldLayoutId'], false);
93 | $this->createIndex(null, '{{%navigation_navs}}', ['dateDeleted'], false);
94 | $this->createIndex(null, '{{%navigation_navs_sites}}', ['navId', 'siteId'], true);
95 | $this->createIndex(null, '{{%navigation_navs_sites}}', ['siteId'], false);
96 | }
97 |
98 | public function addForeignKeys(): void
99 | {
100 | $this->addForeignKey(null, '{{%navigation_nodes}}', ['navId'], '{{%navigation_navs}}', ['id'], 'CASCADE', null);
101 | $this->addForeignKey(null, '{{%navigation_nodes}}', ['elementId'], '{{%elements}}', ['id'], 'SET NULL', null);
102 | $this->addForeignKey(null, '{{%navigation_nodes}}', ['id'], '{{%elements}}', ['id'], 'CASCADE', null);
103 | $this->addForeignKey(null, '{{%navigation_navs}}', ['structureId'], '{{%structures}}', ['id'], 'CASCADE', null);
104 | $this->addForeignKey(null, '{{%navigation_navs}}', ['fieldLayoutId'], '{{%fieldlayouts}}', ['id'], 'SET NULL', null);
105 | $this->addForeignKey(null, '{{%navigation_navs_sites}}', ['siteId'], '{{%sites}}', ['id'], 'CASCADE', 'CASCADE');
106 | $this->addForeignKey(null, '{{%navigation_navs_sites}}', ['navId'], '{{%navigation_navs}}', ['id'], 'CASCADE', null);
107 | }
108 |
109 | public function dropTables(): void
110 | {
111 | $this->dropTableIfExists('{{%navigation_nodes}}');
112 | $this->dropTableIfExists('{{%navigation_navs}}');
113 | $this->dropTableIfExists('{{%navigation_navs_sites}}');
114 | }
115 |
116 | public function dropForeignKeys(): void
117 | {
118 | if ($this->db->tableExists('{{%navigation_nodes}}')) {
119 | MigrationHelper::dropAllForeignKeysOnTable('{{%navigation_nodes}}', $this);
120 | }
121 |
122 | if ($this->db->tableExists('{{%navigation_navs}}')) {
123 | MigrationHelper::dropAllForeignKeysOnTable('{{%navigation_navs}}', $this);
124 | }
125 |
126 | if ($this->db->tableExists('{{%navigation_navs_sites}}')) {
127 | MigrationHelper::dropAllForeignKeysOnTable('{{%navigation_navs_sites}}', $this);
128 | }
129 | }
130 |
131 | public function dropProjectConfig(): void
132 | {
133 | Craft::$app->getProjectConfig()->remove('navigation');
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/migrations/m231229_000000_content_refactor.php:
--------------------------------------------------------------------------------
1 | getNavs()->getAllNavs() as $type) {
18 | $this->updateElements(
19 | (new Query())->from('{{%navigation_nodes}}')->where(['navId' => $type->id]),
20 | $type->getFieldLayout(),
21 | );
22 | }
23 |
24 | return true;
25 | }
26 |
27 | public function safeDown(): bool
28 | {
29 | echo "m231229_000000_content_refactor cannot be reverted.\n";
30 |
31 | return false;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/models/Nav_SiteSettings.php:
--------------------------------------------------------------------------------
1 | _nav)) {
32 | return $this->_nav;
33 | }
34 |
35 | if (!$this->navId) {
36 | throw new InvalidConfigException('Node is missing its navigation ID');
37 | }
38 |
39 | if (($this->_nav = Navigation::$plugin->getNavs()->getNavById($this->navId)) === null) {
40 | throw new InvalidConfigException('Invalid navigation ID: ' . $this->navId);
41 | }
42 |
43 | return $this->_nav;
44 | }
45 |
46 | public function setNav(Nav $nav): void
47 | {
48 | $this->_nav = $nav;
49 | }
50 |
51 | public function getSite(): Site
52 | {
53 | if (!$this->siteId) {
54 | throw new InvalidConfigException('Navigation site settings model is missing its site ID');
55 | }
56 |
57 | if (($site = Craft::$app->getSites()->getSiteById($this->siteId)) === null) {
58 | throw new InvalidConfigException('Invalid site ID: ' . $this->siteId);
59 | }
60 |
61 | return $site;
62 | }
63 |
64 | protected function defineRules(): array
65 | {
66 | $rules = parent::defineRules();
67 | $rules[] = [['id', 'navId', 'siteId'], 'number', 'integerOnly' => true];
68 | $rules[] = [['siteId'], SiteIdValidator::class];
69 |
70 | return $rules;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/models/Settings.php:
--------------------------------------------------------------------------------
1 | getView()->renderTemplate('navigation/_types/custom/modal', [
46 | 'node' => $this->node,
47 | ]);
48 | }
49 |
50 | public function getUrl(): ?string
51 | {
52 | $url = $this->node->getRawUrl();
53 |
54 | // Parse aliases and env variables
55 | $url = App::parseEnv($url);
56 |
57 | // Allow twig support
58 | if ($url && strstr($url, '{')) {
59 | $object = $this->_getObject();
60 | $url = Craft::$app->getView()->renderObjectTemplate($url, $object);
61 | }
62 |
63 | return $url;
64 | }
65 |
66 |
67 | // Private Methods
68 | // =========================================================================
69 |
70 | private function _getObject(): array
71 | {
72 | return [
73 | 'currentUser' => Craft::$app->getUser()->getIdentity(),
74 | ];
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/nodetypes/PassiveType.php:
--------------------------------------------------------------------------------
1 | getView()->renderTemplate('navigation/_types/site/modal', [
47 | 'node' => $this->node,
48 | ]);
49 | }
50 |
51 | public function getSettingsHtml(): ?string
52 | {
53 | return Craft::$app->getView()->renderTemplate('navigation/_types/site/settings');
54 | }
55 |
56 | public function getDefaultTitle(): string
57 | {
58 | if ($site = $this->_getSite()) {
59 | if ($site->hasUrls) {
60 | return $site->name;
61 | }
62 | }
63 |
64 | return parent::getDefaultTitle();
65 | }
66 |
67 | public function getUrl(): ?string
68 | {
69 | if ($site = $this->_getSite()) {
70 | if ($site->hasUrls) {
71 | return rtrim($site->getBaseUrl(), '/');
72 | }
73 | }
74 |
75 | return null;
76 | }
77 |
78 |
79 | // Private Methods
80 | // =========================================================================
81 |
82 | private function _getSite(): ?Site
83 | {
84 | $data = $this->node->data ?? [];
85 |
86 | if ($data) {
87 | $siteId = $data['siteId'] ?? null;
88 |
89 | if ($siteId && $site = Craft::$app->getSites()->getSiteById($siteId)) {
90 | return $site;
91 | }
92 | }
93 |
94 | return null;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/records/Nav.php:
--------------------------------------------------------------------------------
1 | hasOne(Structure::class, ['id' => 'structureId']);
30 | }
31 |
32 | public function getNodes(): ActiveQueryInterface
33 | {
34 | return $this->hasMany(Node::class, ['navId' => 'id']);
35 | }
36 |
37 | public function getFieldLayout(): ActiveQueryInterface
38 | {
39 | return $this->hasOne(FieldLayout::class, ['id' => 'fieldLayoutId']);
40 | }
41 |
42 | public function getSiteSettings(): ActiveQueryInterface
43 | {
44 | return $this->hasMany(Nav_SiteSettings::class, ['navId' => 'id']);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/records/Nav_SiteSettings.php:
--------------------------------------------------------------------------------
1 | hasOne(Nav::class, ['id' => 'navId']);
22 | }
23 |
24 | public function getSite(): ActiveQueryInterface
25 | {
26 | return $this->hasOne(Site::class, ['id' => 'siteId']);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/records/Node.php:
--------------------------------------------------------------------------------
1 | hasOne(Element::class, ['id' => 'id']);
22 | }
23 |
24 | public function getNav(): ActiveQueryInterface
25 | {
26 | return $this->hasOne(Nav::class, ['id' => 'navId']);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-regular-400.eot
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/src/resources/dist/fonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/verbb/navigation/44683d87c0e0f0650e198d8a75b3aff2b4836195/src/resources/dist/fonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/src/resources/dist/js/navigation.js:
--------------------------------------------------------------------------------
1 | /*!
2 | SerializeJSON jQuery plugin.
3 | https://github.com/marioizquierdo/jquery.serializeJSON
4 | version 2.9.0 (Jan, 2018)
5 |
6 | Copyright (c) 2012-2018 Mario Izquierdo
7 | Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
8 | and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
9 | */
10 | !function(e){if("function"==typeof define&&define.amd)define(["jquery"],e);else if("object"==typeof exports){var t=require("jquery");module.exports=e(t)}else e(window.jQuery||window.Zepto||window.$)}((function(e){"use strict";e.fn.serializeJSON=function(t){var n,a,s,i,r,o,l,d,u,c,p,f,h;return n=e.serializeJSON,a=this,s=n.setupOpts(t),i=a.serializeArray(),n.readCheckboxUncheckedValues(i,s,a),r={},e.each(i,(function(e,t){o=t.name,l=t.value,u=n.extractTypeAndNameWithNoType(o),c=u.nameWithNoType,(p=u.type)||(p=n.attrFromInputWithName(a,o,"data-value-type")),n.validateType(o,p,s),"skip"!==p&&(f=n.splitInputNameIntoKeysArray(c),d=n.parseValue(l,o,p,s),(h=!d&&n.shouldSkipFalsy(a,o,c,p,s))||n.deepSet(r,f,d,s))})),r},e.serializeJSON={defaultOptions:{checkboxUncheckedValue:void 0,parseNumbers:!1,parseBooleans:!1,parseNulls:!1,parseAll:!1,parseWithFunction:null,skipFalsyValuesForTypes:[],skipFalsyValuesForFields:[],customTypes:{},defaultTypes:{string:function(e){return String(e)},number:function(e){return Number(e)},boolean:function(e){return-1===["false","null","undefined","","0"].indexOf(e)},null:function(e){return-1===["false","null","undefined","","0"].indexOf(e)?e:null},array:function(e){return JSON.parse(e)},object:function(e){return JSON.parse(e)},auto:function(t){return e.serializeJSON.parseValue(t,null,null,{parseNumbers:!0,parseBooleans:!0,parseNulls:!0})},skip:null},useIntKeysAsArrayIndex:!1},setupOpts:function(t){var n,a,s,i,r,o;for(n in o=e.serializeJSON,null==t&&(t={}),s=o.defaultOptions||{},a=["checkboxUncheckedValue","parseNumbers","parseBooleans","parseNulls","parseAll","parseWithFunction","skipFalsyValuesForTypes","skipFalsyValuesForFields","customTypes","defaultTypes","useIntKeysAsArrayIndex"],t)if(-1===a.indexOf(n))throw new Error("serializeJSON ERROR: invalid option '"+n+"'. Please use one of "+a.join(", "));return i=function(e){return!1!==t[e]&&""!==t[e]&&(t[e]||s[e])},r=i("parseAll"),{checkboxUncheckedValue:i("checkboxUncheckedValue"),parseNumbers:r||i("parseNumbers"),parseBooleans:r||i("parseBooleans"),parseNulls:r||i("parseNulls"),parseWithFunction:i("parseWithFunction"),skipFalsyValuesForTypes:i("skipFalsyValuesForTypes"),skipFalsyValuesForFields:i("skipFalsyValuesForFields"),typeFunctions:e.extend({},i("defaultTypes"),i("customTypes")),useIntKeysAsArrayIndex:i("useIntKeysAsArrayIndex")}},parseValue:function(t,n,a,s){var i,r;return i=e.serializeJSON,r=t,s.typeFunctions&&a&&s.typeFunctions[a]?r=s.typeFunctions[a](t):s.parseNumbers&&i.isNumeric(t)?r=Number(t):!s.parseBooleans||"true"!==t&&"false"!==t?s.parseNulls&&"null"==t?r=null:s.typeFunctions&&s.typeFunctions.string&&(r=s.typeFunctions.string(t)):r="true"===t,s.parseWithFunction&&!a&&(r=s.parseWithFunction(r,n)),r},isObject:function(e){return e===Object(e)},isUndefined:function(e){return void 0===e},isValidArrayIndex:function(e){return/^[0-9]+$/.test(String(e))},isNumeric:function(e){return e-parseFloat(e)>=0},optionKeys:function(e){if(Object.keys)return Object.keys(e);var t,n=[];for(t in e)n.push(t);return n},readCheckboxUncheckedValues:function(t,n,a){var s,i,r;null==n&&(n={}),e.serializeJSON,s="input[type=checkbox][name]:not(:checked):not([disabled])",a.find(s).add(a.filter(s)).each((function(a,s){if(i=e(s),null==(r=i.attr("data-unchecked-value"))&&(r=n.checkboxUncheckedValue),null!=r){if(s.name&&-1!==s.name.indexOf("[]["))throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+s.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");t.push({name:s.name,value:r})}}))},extractTypeAndNameWithNoType:function(e){var t;return(t=e.match(/(.*):([^:]+)$/))?{nameWithNoType:t[1],type:t[2]}:{nameWithNoType:e,type:null}},shouldSkipFalsy:function(t,n,a,s,i){var r=e.serializeJSON.attrFromInputWithName(t,n,"data-skip-falsy");if(null!=r)return"false"!==r;var o=i.skipFalsyValuesForFields;if(o&&(-1!==o.indexOf(a)||-1!==o.indexOf(n)))return!0;var l=i.skipFalsyValuesForTypes;return null==s&&(s="string"),!(!l||-1===l.indexOf(s))},attrFromInputWithName:function(e,t,n){var a,s;return s='[name="'+(a=t.replace(/(:|\.|\[|\]|\s)/g,"\\$1"))+'"]',e.find(s).add(e.filter(s)).attr(n)},validateType:function(t,n,a){var s,i;if(s=(i=e.serializeJSON).optionKeys(a?a.typeFunctions:i.defaultOptions.defaultTypes),n&&-1===s.indexOf(n))throw new Error("serializeJSON ERROR: Invalid type "+n+" found in input name '"+t+"', please use one of "+s.join(", "));return!0},splitInputNameIntoKeysArray:function(t){var n;return e.serializeJSON,n=t.split("["),""===(n=e.map(n,(function(e){return e.replace(/\]/g,"")})))[0]&&n.shift(),n},deepSet:function(t,n,a,s){var i,r,o,l,d,u;if(null==s&&(s={}),(u=e.serializeJSON).isUndefined(t))throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined");if(!n||0===n.length)throw new Error("ArgumentError: param 'keys' expected to be an array with least one element");i=n[0],1===n.length?""===i?t.push(a):t[i]=a:(r=n[1],""===i&&(d=t[l=t.length-1],i=u.isObject(d)&&(u.isUndefined(d[r])||n.length>2)?l:l+1),""===r||s.useIntKeysAsArrayIndex&&u.isValidArrayIndex(r)?!u.isUndefined(t[i])&&e.isArray(t[i])||(t[i]=[]):!u.isUndefined(t[i])&&u.isObject(t[i])||(t[i]={}),o=n.slice(1),u.deepSet(t[i],o,a,s))}}})),$(".btn-migrate").on("click",(function(e){e.preventDefault(),$('input[name="action"]').val($(this).data("action")),$("#main-form").submit()})),void 0===Craft.Navigation&&(Craft.Navigation={}),function($){Craft.setQueryParam("sort",null),Craft.Navigation.NodeIndex=Craft.BaseElementIndex.extend({elementModals:[],init:function(e,t,n){this.navId=n.navId,this.$navSidebar=$(".navigation-nodes-sidebar"),this.base(e,t,n),this.siteMenu&&this.siteMenu.$options.each((function(e,t){n.enabledSiteIds.includes($(t).data("site-id"))||$(t).remove()}));var a=$("#js-navigation-nodes-instructions");a.length&&(a=a.find(".navigation-nodes-instructions").remove()).insertBefore($("#main-content"))},afterInit:function(){Object.keys(this.sourceStates).forEach((e=>{this.sourceStates[e].order="structure"})),this.base(),this.$elements.on("click","tbody tr a.node-edit-btn",this.editNode.bind(this)),this.$navSidebar.find("form").each($.proxy((function(e,t){const n=$(t);n.hasClass("form-type-element")?n.on("submit",this.showElementModal.bind(this)):n.on("submit",this.onNodeFormSubmit.bind(this))}),this))},editNode:function(e){e.preventDefault();var t=$(e.target).parents("tr").find(".element");Craft.createElementEditor(t.data("type"),t)},showElementModal:function(e){e.preventDefault(),this.$form=$(e.target);var t=this.$form.find("button[type=submit]");this.nodeElementType=t.data("element-type"),this.nodeElementSources=t.data("sources");var n=this.nodeElementType+"__"+this.siteId,a=this.elementModals[n];a?(a.show(),a.elementIndex.view.deselectAllElements()):this.elementModals[n]=this.createModal()},createModal:function(){return Craft.createElementSelectorModal(this.nodeElementType,{defaultSiteId:this.siteId,sources:this.nodeElementSources,multiSelect:!0,onSelect:$.proxy(this,"onElementModalSelect")})},onElementModalSelect:function(e){for(var t=[],n=0;n{Craft.cp.displayNotice(e.data.message);var t=this.$form.find('[name="parentId"').val();this.updateElements(),this.$form[0].reset(),this.$form.find('[name="parentId"').val(t)})).catch((e=>{const t=e.response;if(t&&t.data&&t.data.errors)for(var s in n=$('').insertBefore(a.parent()),t.data.errors)if(t.data.errors.hasOwnProperty(s))for(var i=0;i"+e+"").appendTo(n)}t&&t.data&&t.data.message?Craft.cp.displayError(t.data.message):(console.error(e),Craft.cp.displayError())})).finally((()=>{t.addClass("hidden")}))},onUpdateElements:function(){this.updateParentSelect()},onSelectSite:function(){$(".navigation-nodes-sidebar a.tab").each((function(e,t){var n=$(t);n.attr("href",n.data("href"))}))},updateParentSelect:function(){var e={navId:this.navId,siteId:this.siteId};Craft.sendActionRequest("POST","navigation/nodes/get-parent-options",{data:e}).then((e=>{var t="";$.each(e.data.options,(function(e,n){var a=n.disabled?"disabled":"";t+='"+n.label+" "})),$(".js-parent-node select").each((function(e,n){var a=$(n).val();$(n).html(t),$(n).val(a)}))}))}}),Craft.registerElementIndexClass("verbb\\navigation\\elements\\Node",Craft.Navigation.NodeIndex),Craft.Navigation.ElementSelect=Garnish.Base.extend({init(e,t){const n=$(e),a=$(t);n.length&&setTimeout((function(){const e=n.data("elementSelect");e&&e.on("selectElements",(e=>{e.elements&&e.elements.length&&a.val(e.elements[0].siteId)}))}),100)}})}(jQuery);
11 | //# sourceMappingURL=navigation.js.map
--------------------------------------------------------------------------------
/src/resources/src/js/_jquery.serializejson.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | SerializeJSON jQuery plugin.
3 | https://github.com/marioizquierdo/jquery.serializeJSON
4 | version 2.9.0 (Jan, 2018)
5 |
6 | Copyright (c) 2012-2018 Mario Izquierdo
7 | Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
8 | and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
9 | */
10 | !function(e){if("function"==typeof define&&define.amd)define(["jquery"],e);else if("object"==typeof exports){var n=require("jquery");module.exports=e(n)}else e(window.jQuery||window.Zepto||window.$)}(function(e){"use strict";e.fn.serializeJSON=function(n){var r,s,t,i,a,u,l,o,p,c,d,f,y;return r=e.serializeJSON,s=this,t=r.setupOpts(n),i=s.serializeArray(),r.readCheckboxUncheckedValues(i,t,s),a={},e.each(i,function(e,n){u=n.name,l=n.value,p=r.extractTypeAndNameWithNoType(u),c=p.nameWithNoType,(d=p.type)||(d=r.attrFromInputWithName(s,u,"data-value-type")),r.validateType(u,d,t),"skip"!==d&&(f=r.splitInputNameIntoKeysArray(c),o=r.parseValue(l,u,d,t),(y=!o&&r.shouldSkipFalsy(s,u,c,d,t))||r.deepSet(a,f,o,t))}),a},e.serializeJSON={defaultOptions:{checkboxUncheckedValue:void 0,parseNumbers:!1,parseBooleans:!1,parseNulls:!1,parseAll:!1,parseWithFunction:null,skipFalsyValuesForTypes:[],skipFalsyValuesForFields:[],customTypes:{},defaultTypes:{string:function(e){return String(e)},number:function(e){return Number(e)},boolean:function(e){return-1===["false","null","undefined","","0"].indexOf(e)},null:function(e){return-1===["false","null","undefined","","0"].indexOf(e)?e:null},array:function(e){return JSON.parse(e)},object:function(e){return JSON.parse(e)},auto:function(n){return e.serializeJSON.parseValue(n,null,null,{parseNumbers:!0,parseBooleans:!0,parseNulls:!0})},skip:null},useIntKeysAsArrayIndex:!1},setupOpts:function(n){var r,s,t,i,a,u;u=e.serializeJSON,null==n&&(n={}),t=u.defaultOptions||{},s=["checkboxUncheckedValue","parseNumbers","parseBooleans","parseNulls","parseAll","parseWithFunction","skipFalsyValuesForTypes","skipFalsyValuesForFields","customTypes","defaultTypes","useIntKeysAsArrayIndex"];for(r in n)if(-1===s.indexOf(r))throw new Error("serializeJSON ERROR: invalid option '"+r+"'. Please use one of "+s.join(", "));return i=function(e){return!1!==n[e]&&""!==n[e]&&(n[e]||t[e])},a=i("parseAll"),{checkboxUncheckedValue:i("checkboxUncheckedValue"),parseNumbers:a||i("parseNumbers"),parseBooleans:a||i("parseBooleans"),parseNulls:a||i("parseNulls"),parseWithFunction:i("parseWithFunction"),skipFalsyValuesForTypes:i("skipFalsyValuesForTypes"),skipFalsyValuesForFields:i("skipFalsyValuesForFields"),typeFunctions:e.extend({},i("defaultTypes"),i("customTypes")),useIntKeysAsArrayIndex:i("useIntKeysAsArrayIndex")}},parseValue:function(n,r,s,t){var i,a;return i=e.serializeJSON,a=n,t.typeFunctions&&s&&t.typeFunctions[s]?a=t.typeFunctions[s](n):t.parseNumbers&&i.isNumeric(n)?a=Number(n):!t.parseBooleans||"true"!==n&&"false"!==n?t.parseNulls&&"null"==n?a=null:t.typeFunctions&&t.typeFunctions.string&&(a=t.typeFunctions.string(n)):a="true"===n,t.parseWithFunction&&!s&&(a=t.parseWithFunction(a,r)),a},isObject:function(e){return e===Object(e)},isUndefined:function(e){return void 0===e},isValidArrayIndex:function(e){return/^[0-9]+$/.test(String(e))},isNumeric:function(e){return e-parseFloat(e)>=0},optionKeys:function(e){if(Object.keys)return Object.keys(e);var n,r=[];for(n in e)r.push(n);return r},readCheckboxUncheckedValues:function(n,r,s){var t,i,a;null==r&&(r={}),e.serializeJSON,t="input[type=checkbox][name]:not(:checked):not([disabled])",s.find(t).add(s.filter(t)).each(function(s,t){if(i=e(t),null==(a=i.attr("data-unchecked-value"))&&(a=r.checkboxUncheckedValue),null!=a){if(t.name&&-1!==t.name.indexOf("[]["))throw new Error("serializeJSON ERROR: checkbox unchecked values are not supported on nested arrays of objects like '"+t.name+"'. See https://github.com/marioizquierdo/jquery.serializeJSON/issues/67");n.push({name:t.name,value:a})}})},extractTypeAndNameWithNoType:function(e){var n;return(n=e.match(/(.*):([^:]+)$/))?{nameWithNoType:n[1],type:n[2]}:{nameWithNoType:e,type:null}},shouldSkipFalsy:function(n,r,s,t,i){var a=e.serializeJSON.attrFromInputWithName(n,r,"data-skip-falsy");if(null!=a)return"false"!==a;var u=i.skipFalsyValuesForFields;if(u&&(-1!==u.indexOf(s)||-1!==u.indexOf(r)))return!0;var l=i.skipFalsyValuesForTypes;return null==t&&(t="string"),!(!l||-1===l.indexOf(t))},attrFromInputWithName:function(e,n,r){var s,t;return s=n.replace(/(:|\.|\[|\]|\s)/g,"\\$1"),t='[name="'+s+'"]',e.find(t).add(e.filter(t)).attr(r)},validateType:function(n,r,s){var t,i;if(i=e.serializeJSON,t=i.optionKeys(s?s.typeFunctions:i.defaultOptions.defaultTypes),r&&-1===t.indexOf(r))throw new Error("serializeJSON ERROR: Invalid type "+r+" found in input name '"+n+"', please use one of "+t.join(", "));return!0},splitInputNameIntoKeysArray:function(n){var r;return e.serializeJSON,r=n.split("["),""===(r=e.map(r,function(e){return e.replace(/\]/g,"")}))[0]&&r.shift(),r},deepSet:function(n,r,s,t){var i,a,u,l,o,p;if(null==t&&(t={}),(p=e.serializeJSON).isUndefined(n))throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined");if(!r||0===r.length)throw new Error("ArgumentError: param 'keys' expected to be an array with least one element");i=r[0],1===r.length?""===i?n.push(s):n[i]=s:(a=r[1],""===i&&(o=n[l=n.length-1],i=p.isObject(o)&&(p.isUndefined(o[a])||r.length>2)?l:l+1),""===a?!p.isUndefined(n[i])&&e.isArray(n[i])||(n[i]=[]):t.useIntKeysAsArrayIndex&&p.isValidArrayIndex(a)?!p.isUndefined(n[i])&&e.isArray(n[i])||(n[i]=[]):!p.isUndefined(n[i])&&p.isObject(n[i])||(n[i]={}),u=r.slice(1),p.deepSet(n[i],u,s,t))}}});
--------------------------------------------------------------------------------
/src/resources/src/scss/navigation.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 |
3 | // Navigation for Craft CMS
4 | // Author: Verbb - https://verbb.io/
5 |
6 | // ==========================================================================
7 |
8 | // ==========================================================================
9 | // Third Party
10 | // ==========================================================================
11 |
12 | @import "font-awesome";
13 |
14 |
15 | //
16 | // Accordion Tabs
17 | //
18 |
19 | .navigation-nodes-sidebar {
20 | margin: 0 -24px;
21 |
22 | #accordion {
23 | position: relative;
24 | z-index: 1;
25 | box-shadow: inset 0 -1px 0 #e3e5e8;
26 | min-height: 40px;
27 | overflow: hidden;
28 | box-shadow: 0 0 0 1px rgba(205, 216, 228, 0.25), 0 2px 12px rgba(205, 216, 228, 0.5);
29 | border-radius: 5px;
30 | }
31 |
32 | #accordion .tab-list {
33 | display: block;
34 | max-width: inherit;
35 | }
36 |
37 | #accordion .tab-list-item {
38 | .tab {
39 | color: #576575;
40 | border-bottom: 1px solid rgba(0, 0, 20, 0.1);
41 | max-width: 100% !important;
42 |
43 | position: relative;
44 | display: block;
45 | padding: 10px 20px;
46 | white-space: nowrap;
47 | overflow: hidden;
48 | background: #f3f7fc;
49 |
50 | &:after {
51 | font-family: 'Craft';
52 | speak: none;
53 | font-feature-settings: "liga", "dlig";
54 | text-rendering: optimizeLegibility;
55 | font-weight: normal;
56 | font-variant: normal;
57 | text-transform: none;
58 | line-height: 1;
59 | direction: ltr;
60 | display: inline-block;
61 | text-align: center;
62 | font-style: normal;
63 | vertical-align: middle;
64 | word-wrap: normal !important;
65 | user-select: none;
66 | content: 'downangle';
67 | float: right;
68 | margin-top: 2px;
69 | font-size: 16px;
70 | }
71 |
72 | &:hover {
73 | text-decoration: none;
74 | background: darken(#f3f7fc, 2%);
75 | }
76 |
77 | &.sel {
78 | color: #29323d;
79 | padding-bottom: 10px;
80 |
81 | &:after {
82 | transform: rotate(180deg);
83 | }
84 | }
85 | }
86 | }
87 |
88 | .tab-list-pane {
89 | background: #cdd8e4;
90 | padding: 20px;
91 | border-bottom: 1px solid rgba(0, 0, 20, 0.1);
92 |
93 | .field {
94 | margin-left: 0;
95 | margin-right: 0;
96 | }
97 | }
98 |
99 | input.text {
100 | background: #fff !important;
101 | border-color: #9eb4c5;
102 | }
103 | }
104 |
105 |
106 | //
107 | // Structure
108 | //
109 |
110 | .navigation-nodes-instructions {
111 | margin: -1rem 1.5rem 1rem 1.5rem;
112 | }
113 |
114 | // Hide toolbar items. Required to exist for element index
115 | .body-navigation-nodes-index #toolbar {
116 | .search-container,
117 | .search-container + section,
118 | .search,
119 | .search + div {
120 | display: none;
121 | }
122 | }
123 |
124 | .body-navigation-nodes-index #navigation-nodes-index {
125 | .tablepane {
126 | margin: -24px -24px -10px -24px;
127 | }
128 |
129 | table.data thead th {
130 | padding-top: 10px;
131 | padding-bottom: 10px;
132 | }
133 | }
134 |
135 | // Style the TR items, to also work when in dragee, but not affect other element indexes (modals)
136 | .body-navigation-nodes-index #navigation-nodes-index,
137 | .body-navigation-nodes-index .elements.datatablesorthelper {
138 | table.data th:first-child,
139 | table.data td:first-child {
140 | padding-left: 15px !important;
141 | }
142 |
143 | table.data th:last-child,
144 | table.data td:last-child {
145 | padding-right: 15px !important;
146 | }
147 |
148 | table.data tbody th,
149 | table.data tbody td {
150 | padding-top: 0;
151 | padding-bottom: 0;
152 | }
153 |
154 | table.data .element.small,
155 | table.data .element.large:not(.hasthumb) {
156 | padding-top: 7px;
157 | padding-bottom: 7px;
158 | }
159 |
160 | table.data th[data-attribute="typeLabel"],
161 | table.data td[data-attr="typeLabel"] {
162 | // width: 1px;
163 | // white-space: nowrap;
164 | text-align: right;
165 | }
166 |
167 | .node-edit-btn {
168 | position: static;
169 | height: 20px;
170 | border: 1px rgba(96, 125, 159, 0.25) solid;
171 | font-size: 11px;
172 | font-weight: 400;
173 | background: transparent;
174 | opacity: 0;
175 | transition: opacity 0.2s ease;
176 | margin-block: 0;
177 | margin-inline: 7px 0;
178 | }
179 |
180 | tr.sel .node-edit-btn {
181 | border: 1px rgba(255, 255, 255, 0.25) solid;
182 | }
183 |
184 | tr:hover .node-edit-btn {
185 | opacity: 1;
186 | }
187 |
188 | // Info Icons
189 | table.data .node-info-icons {
190 | display: inline-flex;
191 | align-items: baseline;
192 | }
193 |
194 | table.data .node-info-icons .fa {
195 | font-size: 11px;
196 | }
197 |
198 | table.data .node-info-icons .fa,
199 | table.data .node-info-icons .icon {
200 | position: static;
201 | margin: 0 0 0 7px;
202 | display: inline-block;
203 |
204 | &:hover:before,
205 | &:before {
206 | color: rgba(#7b8793, 0.5);
207 | }
208 | }
209 |
210 | table.data .node-info-icons .node-classes {
211 | color: #8f98a3;
212 | margin: 0 0 0 7px;
213 | display: inline-block;
214 | line-height: 1.4;
215 | font-weight: 400;
216 | font-size: 0.75em !important;
217 | }
218 |
219 | // Node Types
220 | .node-type {
221 | --node-type-color: 136, 136, 136;
222 |
223 | font-size: 10px;
224 | font-weight: 500;
225 | text-transform: uppercase;
226 | cursor: default;
227 | user-select: none;
228 | text-align: right;
229 | }
230 |
231 | .node-type span {
232 | padding: 4px 6px;
233 | border-radius: 4px;
234 | border: 1px solid transparent;
235 | color: rgba(var(--node-type-color), 1);
236 | background: rgba(var(--node-type-color), 0.05);
237 | border-color: rgba(var(--node-type-color), 0.25);
238 | }
239 | }
240 |
241 | // Hide some of the element info for the toast notifications
242 | #notifications .notification-body {
243 | .node-info-icons,
244 | .node-edit-btn {
245 | display: none;
246 | }
247 | }
248 |
249 | // ==========================================================================
250 | // Settings
251 | // ==========================================================================
252 |
253 | .navigation-settings-tabs li {
254 | width: 100%;
255 | max-width: 100% !important;
256 | }
257 |
258 |
--------------------------------------------------------------------------------
/src/services/Breadcrumbs.php:
--------------------------------------------------------------------------------
1 | getElements()->getElementByUri('__home__')) {
24 | $breadcrumbs[] = $this->_getBreadcrumbItem($element, '');
25 | }
26 |
27 | $path = '';
28 |
29 | foreach (Craft::$app->getRequest()->getSegments() as $segment) {
30 | $path .= '/' . $segment;
31 |
32 | // Try and fetch an element based on the path
33 | $element = Craft::$app->getElements()->getElementByUri(ltrim($path, '/'));
34 |
35 | if ($element) {
36 | $breadcrumbs[] = $this->_getBreadcrumbItem($element, $segment, $path);
37 | } else {
38 | $breadcrumbs[] = $this->_getBreadcrumbItem($segment, $segment, $path);
39 | }
40 | }
41 |
42 | if ($limit) {
43 | return array_slice($breadcrumbs, 0, $limit);
44 | }
45 |
46 | return $breadcrumbs;
47 | }
48 |
49 | private function _getBreadcrumbItem($item, $segment, $path = ''): array
50 | {
51 | // Generate the title from the segment or element
52 | $title = StringHelper::titleize((string)$item);
53 | $isElement = false;
54 | $element = null;
55 | $elementId = null;
56 | $elementType = null;
57 |
58 | if ($item instanceof ElementInterface) {
59 | $isElement = true;
60 | $element = $item;
61 | $elementId = $item->id;
62 | $elementType = get_class($item);
63 |
64 | // Check if the element has titles setup
65 | if ($item->hasTitles()) {
66 | $title = $item->title;
67 | }
68 | }
69 |
70 | $url = UrlHelper::siteUrl($path);
71 |
72 | return [
73 | 'title' => $title,
74 | 'url' => $url,
75 | 'segment' => $segment,
76 | 'isElement' => $isElement,
77 | 'element' => $element,
78 | 'elementId' => $elementId,
79 | 'elementType' => $elementType,
80 | 'link' => Html::tag('a', $title, ['href' => $url]),
81 | ];
82 | }
83 | }
--------------------------------------------------------------------------------
/src/services/Elements.php:
--------------------------------------------------------------------------------
1 | Craft::t('site', Entry::pluralDisplayName()),
34 | 'button' => Craft::t('navigation', 'Add an Entry'),
35 | 'type' => Entry::class,
36 | 'sources' => [],
37 | 'default' => true,
38 | 'color' => '#5e5378',
39 | ],
40 | [
41 | 'label' => Craft::t('site', Category::pluralDisplayName()),
42 | 'button' => Craft::t('navigation', 'Add a Category'),
43 | 'type' => Category::class,
44 | 'sources' => [],
45 | 'default' => true,
46 | 'color' => '#1BB311',
47 | ],
48 | [
49 | 'label' => Craft::t('site', Asset::pluralDisplayName()),
50 | 'button' => Craft::t('navigation', 'Add an Asset'),
51 | 'type' => Asset::class,
52 | 'sources' => [],
53 | 'default' => true,
54 | 'color' => '#e12d39',
55 | ],
56 | ];
57 |
58 | if (Craft::$app->getPlugins()->isPluginEnabled('commerce') && class_exists(Product::class)) {
59 | $elements[] = [
60 | 'label' => Craft::t('site', Product::pluralDisplayName()),
61 | 'button' => Craft::t('navigation', 'Add a Product'),
62 | 'type' => Product::class,
63 | 'sources' => [],
64 | 'default' => true,
65 | ];
66 | }
67 |
68 | // Add all other elements that support URIs
69 | $addedElementTypes = ArrayHelper::getColumn($elements, 'type');
70 |
71 | foreach (Craft::$app->getElements()->getAllElementTypes() as $elementType) {
72 | if ($elementType::hasUris() && !in_array($elementType, $addedElementTypes)) {
73 | $elements[] = [
74 | 'label' => Craft::t('site', $elementType::pluralDisplayName()),
75 | 'button' => Craft::t('navigation', 'Add a {name}', ['name' => $elementType::displayName()]),
76 | 'type' => $elementType,
77 | 'sources' => [],
78 | ];
79 | }
80 | }
81 |
82 | $event = new RegisterElementEvent([
83 | 'elements' => $elements,
84 | ]);
85 |
86 | $this->trigger(self::EVENT_REGISTER_NAVIGATION_ELEMENT, $event);
87 |
88 | $elementIndexes = Craft::$app->getElementSources();
89 |
90 | // For performance, only include element sources if we require them. They also do unexpected things
91 | // as they're element indexes (like for assets, creating user upload directories)
92 | if ($includeSources) {
93 | foreach ($event->elements as $key => $element) {
94 | $event->elements[$key]['sources'] = $elementIndexes->getSources($element['type'], 'modal');
95 | }
96 | }
97 |
98 | return $event->elements;
99 | }
100 |
101 | }
--------------------------------------------------------------------------------
/src/services/NodeTypes.php:
--------------------------------------------------------------------------------
1 | getRegisteredNodeTypes();
30 | }
31 |
32 | public function getRegisteredNodeTypes(): array
33 | {
34 | $nodeTypes = [
35 | PassiveType::class,
36 | ];
37 |
38 | if (Craft::$app->getIsMultiSite()) {
39 | $nodeTypes[] = SiteType::class;
40 | }
41 |
42 | $event = new RegisterNodeTypeEvent([
43 | 'types' => $nodeTypes,
44 | ]);
45 |
46 | $this->trigger(self::EVENT_REGISTER_NODE_TYPES, $event);
47 |
48 | $nodeTypes = $event->types;
49 |
50 | // Always add custom node at the end
51 | $nodeTypes[] = CustomType::class;
52 |
53 | $types = [];
54 |
55 | foreach ($nodeTypes as $type) {
56 | $types[] = ComponentHelper::createComponent([
57 | 'type' => $type,
58 | ], NodeTypeInterface::class);
59 | }
60 |
61 | return $types;
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/src/services/Nodes.php:
--------------------------------------------------------------------------------
1 | getElements()->getElementById($id, NodeElement::class, $siteId, $criteria);
30 | }
31 |
32 | public function getNodesForNav($navId, $siteId = null, $includeTemp = false): array
33 | {
34 | $nodes = NodeElement::find()
35 | ->navId($navId)
36 | ->status(null)
37 | ->siteId($siteId)
38 | ->status(null)
39 | ->all();
40 |
41 | if ($includeTemp) {
42 | $nodes = array_merge($nodes, $this->_tempNodes);
43 | }
44 |
45 | return $nodes;
46 | }
47 |
48 | public function onSaveElement(ElementEvent $event): void
49 | {
50 | // Skip this when updating Craft is currently in progress
51 | if (Craft::$app->getIsInMaintenanceMode()) {
52 | return;
53 | }
54 |
55 | $element = $event->element;
56 | $isNew = $event->isNew;
57 |
58 | // We only care about already-existing elements and if they have a URL
59 | if ($isNew || !$element->getUrl()) {
60 | return;
61 | }
62 |
63 | // This triggers for every element - including a Node!
64 | if (get_class($element) === NodeElement::class) {
65 | return;
66 | }
67 |
68 | // Ignore any drafts
69 | if ($element->getIsDraft()) {
70 | return;
71 | }
72 |
73 | $nodes = NodeElement::find()
74 | ->elementId($element->id)
75 | ->siteId($element->siteId)
76 | ->slug((string)$element->siteId)
77 | ->status(null)
78 | ->type(get_class($element))
79 | ->all();
80 |
81 | foreach ($nodes as $node) {
82 | // If no nav for the node, skip. Just to protect against nodes in some cases
83 | $nav = Navigation::$plugin->getNavs()->getNavById($node->navId);
84 |
85 | if (!$nav) {
86 | return;
87 | }
88 |
89 | // Check if the element is propagating, and in the allowed sites
90 | if ($element->propagating) {
91 | $supportedSites = ElementHelper::supportedSitesForElement($node);
92 | $supportedSiteIds = ArrayHelper::getColumn($supportedSites, 'siteId');
93 |
94 | if (!in_array($node->siteId, $supportedSiteIds, false)) {
95 | return;
96 | }
97 | }
98 |
99 | $currentElement = Craft::$app->getElements()->getElementById($element->id, get_class($element), $element->siteId);
100 |
101 | if ($element->uri) {
102 | $node->url = $element->uri;
103 | }
104 |
105 | // Only update the node name if they were the same before the element was saved
106 | if ($currentElement && $currentElement->title === $node->title) {
107 | $node->title = $element->title;
108 | }
109 |
110 | if ($currentElement) {
111 | $isMultiSite = Craft::$app->getIsMultiSite() && count($node->getSupportedSites()) > 1;
112 |
113 | // Sync the enabled status - if it's changed. Note that there's an inconsistency with reporting of a node is enabled for multi-site
114 | $nodeEnabled = $isMultiSite ? $node->getEnabledForSite() : $node->enabled;
115 | $elementEnabled = $isMultiSite ? $element->getEnabledForSite() : $element->enabled;
116 | $currentElementEnabled = $isMultiSite ? $currentElement->getEnabledForSite() : $currentElement->enabled;
117 |
118 | // Is the status different between the element and the node?
119 | if ($elementEnabled !== $currentElementEnabled && $elementEnabled !== $nodeEnabled) {
120 | if ($isMultiSite) {
121 | $node->enabled = true;
122 | $node->setEnabledForSite($elementEnabled);
123 | } else {
124 | $node->enabled = $elementEnabled;
125 | $node->setEnabledForSite(true);
126 | }
127 | }
128 | }
129 |
130 | $node->elementSiteId = $element->siteId;
131 |
132 | Craft::$app->getElements()->saveElement($node, true, false);
133 | }
134 | }
135 |
136 | public function onDeleteElement(ElementEvent $event): void
137 | {
138 | $element = $event->element;
139 |
140 | $nodes = NodeElement::find()
141 | ->elementId($element->id)
142 | ->type(get_class($element))
143 | ->siteId($element->siteId)
144 | ->ids();
145 |
146 | foreach ($nodes as $nodeId) {
147 | Craft::$app->getElements()->deleteElementById($nodeId);
148 | }
149 | }
150 |
151 | public function onMoveElement(MoveElementEvent $event): void
152 | {
153 | if (!($event->element instanceof NodeElement)) {
154 | return;
155 | }
156 |
157 | $nav = $event->element->getNav();
158 |
159 | // The element we've moving won't have its destination level set yet,
160 | // so use the target element (where we're moving to) to deduce that.
161 | $event->element->level = $event->getTargetElement()->level ?? $event->element->level;
162 |
163 | if ($nav->maxNodesSettings) {
164 | Navigation::$plugin->getNodes()->setTempNodes([$event->element]);
165 |
166 | if ($nav->isOverMaxLevel($event->element)) {
167 | throw new UserException('Unable to move node due to the maximum nodes per level.');
168 | }
169 | }
170 | }
171 |
172 | public function getParentOptions($nodes, $nav): array
173 | {
174 | $maxLevels = $nav->maxLevels ?: false;
175 |
176 | $parentOptions[] = [
177 | 'label' => '',
178 | 'value' => 0,
179 | ];
180 |
181 | foreach ($nodes as $node) {
182 | $label = '';
183 |
184 | for ($i = 1; $i < $node->level; $i++) {
185 | $label .= ' ';
186 | }
187 |
188 | $label .= $node->title;
189 |
190 | $parentOptions[] = [
191 | 'label' => $label,
192 | 'value' => $node->id,
193 | 'disabled' => $maxLevels !== false && $node->level >= $maxLevels,
194 | ];
195 | }
196 |
197 | return $parentOptions;
198 | }
199 |
200 | public function setTempNodes(array $nodes): void
201 | {
202 | $this->_tempNodes = $nodes;
203 | }
204 |
205 | public function getTempNodes(): array
206 | {
207 | return $this->_tempNodes;
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/templates/_field/input.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {{ forms.selectField({
4 | name: name,
5 | value: value,
6 | options: options,
7 | }) }}
--------------------------------------------------------------------------------
/src/templates/_field/settings.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
--------------------------------------------------------------------------------
/src/templates/_integrations/feed-me/column.html:
--------------------------------------------------------------------------------
1 | {% set nav = craft.navigation.getNavById(feed.elementGroup[elementType]) %}
2 |
3 | {{ nav.name ?? '' }}
--------------------------------------------------------------------------------
/src/templates/_integrations/feed-me/fields/nested-node.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {% set nameLabel = nameLabel ?? name ?? null %}
4 | {% set handle = handle ?? null %}
5 | {% set instructions = instructions ?? null %}
6 | {% set instructionsHandle = instructionsHandle ?? handle ?? null %}
7 |
8 | {% set default = default ?? null %}
9 | {% set required = required ?? null %}
10 | {% set attribute = attribute ?? null %}
11 | {% set field = field ?? null %}
12 | {% set fieldClass = fieldClass ?? null %}
13 |
14 | {% set path = path ?? [handle] %}
15 | {% set namespacePath = path|join('][') %}
16 | {% set nodePath = path|join('.') ~ '.node' %}
17 |
18 | {% set namespace = 'fieldMapping[' ~ namespacePath ~ ']' %}
19 | {% set value = hash_get(feed.fieldMapping, nodePath) %}
20 |
21 |
22 |
23 |
24 |
25 |
{{ nameLabel|t('site') }}
26 |
27 | {% if attribute and instructions %}
28 |
29 |
{{ instructions|raw }}
30 |
31 | {% endif %}
32 |
33 | {% if fieldClass and instructionsHandle %}
34 |
35 | {{ instructionsHandle }}
36 |
37 | {% endif %}
38 |
39 |
40 |
41 |
42 |
43 | {% set nodeOptions = [{ label: 'Don’t import' | t('feed-me'), value: 'noimport' }] %}
44 |
45 | {% for nodeKey, nodeValue in feed.getFeedNodes().data %}
46 | {% if nodeKey %}
47 | {% set nodeOptions = nodeOptions | merge([{ label: nodeValue, value: nodeKey }]) %}
48 | {% endif %}
49 | {% endfor %}
50 |
51 | {% namespace namespace %}
52 | {{ forms.selectField({
53 | name: 'node',
54 | value: value,
55 | options: nodeOptions,
56 | class: 'selectize fullwidth',
57 | }) }}
58 | {% endnamespace %}
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/templates/_integrations/feed-me/groups.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {% set navs = element.getGroups() %}
4 |
5 | {{ forms.selectField({
6 | label: "Navigation" | t('feed-me'),
7 | instructions: 'Choose the navigation you want to save your feed data into.' | t('feed-me'),
8 | id: 'elementGroup-' ~ elementType,
9 | name: 'elementGroup[' ~ elementType ~ ']',
10 | options: craft.feedme.getSelectOptions(navs),
11 | value: feed.elementGroup[elementType] ?? '',
12 | required: true,
13 | }) }}
--------------------------------------------------------------------------------
/src/templates/_integrations/feed-me/map.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 | {% import 'feed-me/_macros' as feedMeMacro %}
3 |
4 | {% if feed.elementGroup %}
5 | {% set navId = feed.elementGroup[feed.elementType] %}
6 |
7 | {% set nav = craft.navigation.getNavById(navId) %}
8 | {% endif %}
9 |
10 | {% set types = [{ label: 'Don‘t import', value: '' }] %}
11 |
12 | {% for tab in craft.navigation.getRegisteredElements() %}
13 | {% set types = types | merge([{ label: tab.label, value: tab.type }]) %}
14 | {% endfor %}
15 |
16 | {% for nodeType in craft.navigation.getRegisteredNodeTypes() %}
17 | {% set types = types | merge([{ label: nodeType.displayName, value: className(nodeType) }]) %}
18 | {% endfor %}
19 |
20 | {% set fields = [{
21 | name: 'Title',
22 | handle: 'title',
23 | default: {
24 | type: 'text',
25 | },
26 | }, {
27 | name: 'Type',
28 | handle: 'type',
29 | default: {
30 | type: 'select',
31 | options: types,
32 | },
33 | }, {
34 | type: 'element',
35 | name: 'Element',
36 | handle: 'elementId',
37 | instructions: 'The element this node links to.' | t('feed-me'),
38 | default: {
39 | type: 'text',
40 | },
41 | }, {
42 | name: 'URL',
43 | handle: 'url',
44 | default: {
45 | type: 'text',
46 | },
47 | }, {
48 | name: 'URL Suffix',
49 | handle: 'urlSuffix',
50 | default: {
51 | type: 'text',
52 | },
53 | }, {
54 | name: 'Open in New Window',
55 | handle: 'newWindow',
56 | default: {
57 | type: 'lightswitch',
58 | },
59 | }, {
60 | name: 'Classes',
61 | handle: 'classes',
62 | default: {
63 | type: 'text',
64 | },
65 | }, {
66 | type: 'nested-node',
67 | name: 'Children',
68 | handle: 'children',
69 | instructions: 'The starting node for nested child nodes.'|t('feed-me'),
70 | }, {
71 | name: 'Status',
72 | handle: 'enabled',
73 | instructions: 'Choose either a default status from the list or the imported field that will contain the status.' | t('feed-me'),
74 | default: {
75 | type: 'select',
76 | options: [
77 | { label: 'Don‘t import', value: '' },
78 | { label: 'Enabled', value: '1' },
79 | { label: 'Disabled', value: '0' },
80 | ],
81 | },
82 | }, {
83 | name: 'Node ID',
84 | handle: 'id',
85 | instructions: 'Warning: This should only be used for an existing Navigation Node ID.' | t('feed-me'),
86 | default: {
87 | type: 'text',
88 | },
89 | }] %}
90 |
91 | {{ 'Node Fields' | t('feed-me') }}
92 |
93 |
94 |
95 | {{ 'Field' | t('feed-me') }}
96 | {{ 'Feed Element' | t('feed-me') }}
97 | {{ 'Default Value' | t('feed-me') }}
98 |
99 |
100 | {% for field in fields %}
101 | {% set template = field.type ?? 'default' %}
102 | {% set variables = field | merge({ feed: feed, feedData: feedData, attribute: true }) %}
103 |
104 | {% include [
105 | 'navigation/_integrations/feed-me/fields/' ~ template,
106 | 'feed-me/_includes/fields/' ~ template
107 | ] ignore missing with variables only %}
108 | {% endfor %}
109 |
110 |
111 |
112 | {% set tabs = [] %}
113 |
114 | {% if nav.fieldLayoutId %}
115 | {% set tabs = craft.app.fields.getLayoutById(nav.fieldLayoutId).getTabs() %}
116 |
117 | {% for tab in tabs %}
118 |
119 |
120 | {{ tab.name }} Fields
121 |
122 |
123 |
124 | {{ 'Field' | t('feed-me') }}
125 | {{ 'Feed Element' | t('feed-me') }}
126 | {{ 'Default Value' | t('feed-me') }}
127 |
128 |
129 | {% for layoutField in tab.getElements() | filter(e => e is instance of('craft\\fieldlayoutelements\\CustomField')) %}
130 | {% set field = layoutField.getField() %}
131 | {% set fieldClass = craft.feedme.fields.getRegisteredField(className(field)) %}
132 | {% set template = fieldClass.getMappingTemplate() %}
133 |
134 | {% set variables = { name: field.name, handle: field.handle, feed: feed, feedData: feedData, field: field, fieldClass: fieldClass } %}
135 |
136 | {% include template ignore missing with variables only %}
137 | {% endfor %}
138 |
139 |
140 | {% endfor %}
141 | {% endif %}
142 |
143 |
144 |
145 | {{ "Set a unique identifier to match against existing elements" | t('feed-me') }}
146 |
147 | {{ "Select the fields you want to use to check for existing elements. When selected, Feed Me will look for existing elements that match the fields provided below and either update, or skip depending on your choice of Import Strategy." | t('feed-me') }}
148 |
149 | {% for tab in tabs %}
150 | {% for layoutField in tab.getElements() | filter(e => e is instance of('craft\\fieldlayoutelements\\CustomField')) %}
151 | {% set field = layoutField.getField() %}
152 | {% set fields = fields | merge([{ name: field.name, handle: field.handle, type: className(field) }]) %}
153 | {% endfor %}
154 | {% endfor %}
155 |
156 |
157 | {% for field in fields %}
158 | {% if field and craft.feedme.fieldCanBeUniqueId(field) %}
159 | {{ forms.checkboxField({
160 | name: 'fieldUnique[' ~ field.handle ~ ']',
161 | label: field.name,
162 | checked: feed.fieldUnique[field.handle] ?? '',
163 | }) }}
164 | {% endif %}
165 | {% endfor %}
166 |
--------------------------------------------------------------------------------
/src/templates/_layouts/index.html:
--------------------------------------------------------------------------------
1 | {% extends '_layouts/cp' %}
2 |
3 | {% do view.registerAssetBundle("verbb\\navigation\\assetbundles\\NavigationAsset") %}
4 |
5 | {% if title is not defined %}
6 | {% set title = craft.navigation.getPluginName() %}
7 | {% endif %}
8 |
9 | {% block content %}
10 | {% block blockContent %}
11 |
12 | {% endblock %}
13 |
14 |
15 |
16 |
37 | {% endblock %}
38 |
--------------------------------------------------------------------------------
/src/templates/_special/render.html:
--------------------------------------------------------------------------------
1 | {% apply spaceless %}
2 |
3 | {% set ulAttributes = options.ulAttributes ?? {} %}
4 | {% set ulClass = options.ulClass is defined ? [options.ulClass] : [] %}
5 | {% set ulAttributes = ulAttributes | merge({ class: ulClass }) %}
6 |
7 | {% set liAttributes = options.liAttributes ?? {} %}
8 | {% set liClass = options.liClass is defined ? [options.liClass] : [] %}
9 | {% set liAttributes = liAttributes | merge({ class: liClass }) %}
10 |
11 | {% set aAttributes = options.aAttributes ?? {} %}
12 | {% set aClass = options.aClass is defined ? [options.aClass] : [] %}
13 | {% set aAttributes = aAttributes | merge({ class: aClass }) %}
14 |
15 | {% set currentClass = options.currentClass ?? 'current' %}
16 | {% set activeClass = options.activeClass ?? 'active' %}
17 | {% set hasChildrenClass = options.hasChildrenClass ?? 'has-children' %}
18 |
19 |
20 | {% nav node in nodes %}
21 | {% set liClasses = liAttributes.class | merge([node.classes]) | filter %}
22 |
23 | {% if node.children | length %}
24 | {% set liClasses = liClasses | merge([hasChildrenClass]) %}
25 | {% endif %}
26 |
27 | {% set liNodeAttributes = liAttributes | merge({ class: liClasses }) %}
28 |
29 | {% set aNodeAttributes = aAttributes | merge({
30 | href: node.url ?? false,
31 | target: node.newWindow ? '_blank' : false,
32 | rel: node.newWindow ? 'noopener' : false,
33 | }) | merge(node.getCustomAttributesObject()) %}
34 |
35 | {% if node.active %}
36 | {% set aClasses = aNodeAttributes.class | merge([activeClass]) | filter %}
37 | {% set aNodeAttributes = aNodeAttributes | merge({ class: aClasses }) %}
38 | {% endif %}
39 |
40 | {% if node.getCurrent() %}
41 | {% set aClasses = aNodeAttributes.class | merge([currentClass]) | filter %}
42 | {% set aNodeAttributes = aNodeAttributes | merge({ class: aClasses, 'aria-current': 'page' }) %}
43 | {% endif %}
44 |
45 |
46 |
47 | {{- node.title -}}
48 |
49 |
50 | {% ifchildren %}
51 |
54 | {% endifchildren %}
55 |
56 | {% endnav %}
57 |
58 |
59 | {% endapply %}
60 |
--------------------------------------------------------------------------------
/src/templates/_types/custom/modal.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {{ forms.textField({
4 | label: 'URL' | t('navigation'),
5 | instructions: 'The URL for this navigation item.' | t('navigation'),
6 | id: 'url',
7 | name: 'url',
8 | value: node.getRawUrl(),
9 | }) }}
--------------------------------------------------------------------------------
/src/templates/_types/site/modal.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {% namespace 'data' %}
4 |
5 | {% set sites = [{
6 | label: 'Select a site' | t('navigation'),
7 | value: '',
8 | }] %}
9 |
10 | {% for site in craft.app.sites.getEditableSites() %}
11 | {% if site.hasUrls %}
12 | {% set sites = sites | merge([{ label: site.name, value: site.id }]) %}
13 | {% endif %}
14 | {% endfor %}
15 |
16 | {{ forms.selectField({
17 | label: 'Site' | t('app'),
18 | instructions: 'Select a site to use its Base URL.' | t('navigation'),
19 | id: 'siteId',
20 | name: 'siteId',
21 | value: node.data.siteId ?? '',
22 | options: sites,
23 | }) }}
24 |
25 | {% endnamespace %}
--------------------------------------------------------------------------------
/src/templates/_types/site/settings.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 |
3 | {% set sites = [{
4 | label: 'Select a site' | t('navigation'),
5 | value: '',
6 | }] %}
7 |
8 | {% for site in craft.app.sites.getEditableSites() %}
9 | {% if site.hasUrls %}
10 | {% set sites = sites | merge([{ label: site.name, value: site.id }]) %}
11 | {% endif %}
12 | {% endfor %}
13 |
14 | {{ forms.selectField({
15 | label: 'Site' | t('app'),
16 | instructions: 'Select a site to use its Base URL.' | t('navigation'),
17 | id: 'siteId',
18 | name: 'siteId',
19 | options: sites,
20 | }) }}
--------------------------------------------------------------------------------
/src/templates/navs/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'navigation/_layouts' %}
2 | {% import '_includes/forms' as forms %}
3 |
4 | {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%}
5 |
6 | {% set crumbs = [
7 | { label: craft.navigation.getPluginName(), url: url('navigation') },
8 | { label: 'Navigations' | t('navigation'), url: url('navigation/navs') },
9 | ] %}
10 |
11 | {% if craft.app.getIsMultiSite() and requestedSite %}
12 | {% set crumbs = crumbs | unshift({
13 | id: 'site-crumb',
14 | icon: 'world',
15 | label: requestedSite.name | t('site'),
16 | menu: {
17 | items: siteMenuItems(null, requestedSite),
18 | label: 'Select site' | t('site')
19 | },
20 | }) %}
21 | {% endif %}
22 |
23 | {% block actionButton %}
24 | {% if editable and currentUser.can('navigation-createNavs') %}
25 |
32 | {% endif %}
33 | {% endblock %}
34 |
35 | {% block blockContent %}
36 |
37 | {% endblock %}
38 |
39 | {% set tableData = [] %}
40 | {% set editableNavs = 0 %}
41 |
42 | {% set canReorder = currentUser.can('navigation-createNavs') ? true : false %}
43 |
44 | {% for navigation in navigations %}
45 | {% set canDelete = currentUser.can('navigation-deleteNav:' ~ navigation.uid) ? true : false %}
46 |
47 | {% set tableData = tableData | merge([{
48 | id: navigation.id,
49 | title: navigation.name | t('site'),
50 | url: url('navigation/navs/build/' ~ navigation.id),
51 | name: navigation.name | t('site') | e,
52 | handle: navigation.handle,
53 | _showDelete: canDelete,
54 | settings: {
55 | label: 'Edit Settings' | t('navigation') | e,
56 | url: currentUser.can('navigation-editNav:' ~ navigation.uid) ? url('navigation/navs/edit/' ~ navigation.id),
57 | },
58 | }]) %}
59 |
60 | {% if currentUser.can('navigation-editNav:' ~ navigation.uid) %}
61 | {% set editableNavs = editableNavs + 1 %}
62 | {% endif %}
63 | {% endfor %}
64 |
65 | {% js %}
66 | var columns = [
67 | { name: '__slot:title', title: Craft.t('app', 'Name') },
68 | { name: '__slot:handle', title: Craft.t('app', 'Handle') },
69 |
70 | {% if editable and editableNavs %}
71 | { name: 'settings', title: Craft.t('app', 'Settings'),
72 | callback: function(value) {
73 | if (value.url) {
74 | return '' + value.label + ' ';
75 | }
76 |
77 | return '';
78 | }
79 | },
80 | {% endif %}
81 | ];
82 |
83 | new Craft.VueAdminTable({
84 | columns: columns,
85 | container: '#navigations-vue-admin-table',
86 | deleteAction: '{{ editable ? 'navigation/navs/delete-nav' : '' }}',
87 | emptyMessage: Craft.t('navigation', 'No navigations exist yet.'),
88 | reorderAction: '{{ navigations | length > 1 and canReorder ? 'navigation/navs/reorder-nav' : '' }}',
89 | tableData: {{ tableData | json_encode | raw }},
90 | });
91 |
92 | // When changing the site select, navigate to the navigation index for that site.
93 | var $siteMenuBtn = $('#header .sitemenubtn:first');
94 |
95 | if (this.$siteMenuBtn.length) {
96 | var siteMenu = $siteMenuBtn.menubtn().data('menubtn').menu;
97 |
98 | siteMenu.on('optionselect', function(ev) {
99 | siteMenu.$options.removeClass('sel');
100 | var $option = $(ev.selectedOption).addClass('sel');
101 | $siteMenuBtn.html($option.html());
102 | Craft.cp.setSiteId($option.data('site-id'));
103 |
104 | location.reload();
105 | });
106 | }
107 |
108 | {% endjs %}
109 |
--------------------------------------------------------------------------------
/src/templates/settings/_panes/general.html:
--------------------------------------------------------------------------------
1 | {% import '_includes/forms' as forms %}
2 | {% import 'verbb-base/_macros' as macros %}
3 |
4 | {{ forms.textField({
5 | id: 'pluginName',
6 | name: 'pluginName',
7 | label: 'Plugin Name' | t('app'),
8 | value: settings.pluginName,
9 | first: true,
10 | autofocus: true,
11 | instructions: 'Plugin name for the end user.' | t('navigation'),
12 | warning: macros.configWarning('pluginName', 'navigation'),
13 | }) }}
14 |
--------------------------------------------------------------------------------
/src/templates/settings/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'navigation/_layouts' %}
2 |
3 | {% import '_includes/forms' as forms %}
4 | {% import 'verbb-base/_macros' as macros %}
5 |
6 | {% requireAdmin %}
7 |
8 | {% set crumbs = [
9 | { label: craft.navigation.getPluginName(), url: url('navigation/settings') },
10 | { label: 'Settings' | t('app'), url: url('navigation/settings') }
11 | ] %}
12 |
13 | {% set navItems = {
14 | 'general': { title: 'General Settings' | t('navigation') },
15 | } %}
16 |
17 | {% set selectedItem = 'general' %}
18 | {% set fullPageForm = true %}
19 |
20 | {% block sidebar %}
21 |
22 |
23 | {% for id, item in navItems %}
24 | {% if item.heading is defined %}
25 | {{ item.heading }}
26 | {% else %}
27 |
28 |
29 | {{ item.title }}
30 |
31 |
32 | {% endif %}
33 | {% endfor %}
34 |
35 |
36 | {% endblock %}
37 |
38 | {% block blockContent %}
39 |
40 |
41 |
42 |
43 | {% for id, item in navItems %}
44 | {% if item.title is defined %}
45 |
46 |
{{ item.title }}
47 |
48 | {% namespace 'settings' %}
49 | {% include 'navigation/settings/_panes/' ~ id ignore missing %}
50 | {% endnamespace %}
51 |
52 | {% endif %}
53 | {% endfor %}
54 |
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/src/translations/en/navigation.php:
--------------------------------------------------------------------------------
1 | 'Add a Category',
5 | 'Add an Asset' => 'Add an Asset',
6 | 'Add an Entry' => 'Add an Entry',
7 | 'Add a Product' => 'Add a Product',
8 | 'Add a {name}' => 'Add a {name}',
9 | 'Additional attributes for this node.' => 'Additional attributes for this node.',
10 | 'Additional content appended to the element‘s URL.' => 'Additional content appended to the element‘s URL.',
11 | 'Additional CSS classes for this navigation item.' => 'Additional CSS classes for this navigation item.',
12 | 'Add {name}' => 'Add {name}',
13 | 'After other {type}' => 'After other {type}',
14 | 'Attribute' => 'Attribute',
15 | 'Before other {type}' => 'Before other {type}',
16 | 'Changing this may result in data loss.' => 'Changing this may result in data loss.',
17 | 'Choose which sites this navigation should be available in.' => 'Choose which sites this navigation should be available in.',
18 | 'Classes' => 'Classes',
19 | 'Couldn’t add node.' => 'Couldn’t add node.',
20 | 'Create a new navigation' => 'Create a new navigation',
21 | 'Create any level-specific limits for the maximum number of nodes.' => 'Create any level-specific limits for the maximum number of nodes.',
22 | 'Create navigations' => 'Create navigations',
23 | 'Custom Attributes' => 'Custom Attributes',
24 | 'Custom URL' => 'Custom URL',
25 | 'Default {type} Placement' => 'Default {type} Placement',
26 | 'Delete navigation' => 'Delete navigation',
27 | 'Duplicate' => 'Duplicate',
28 | 'Edit' => 'Edit',
29 | 'Edit navigation settings' => 'Edit navigation settings',
30 | 'Edit Nodes' => 'Edit Nodes',
31 | 'Edit Settings' => 'Edit Settings',
32 | 'Element ID is required.' => 'Element ID is required.',
33 | 'Exceeded maximum allowed nodes ({number}) for this nav.' => 'Exceeded maximum allowed nodes ({number}) for this nav.',
34 | 'Exceeded maximum allowed nodes for this level.' => 'Exceeded maximum allowed nodes for this level.',
35 | 'General Settings' => 'General Settings',
36 | 'Helper text to guide the author. Shown at the top of the page when editing a navigation.' => 'Helper text to guide the author. Shown at the top of the page when editing a navigation.',
37 | 'How you’ll refer to this navigation in the templates.' => 'How you’ll refer to this navigation in the templates.',
38 | 'Level {level}' => 'Level {level}',
39 | 'Linked Element ID is required.' => 'Linked Element ID is required.',
40 | 'Linked to {element}' => 'Linked to {element}',
41 | 'Manage “{type}”' => 'Manage “{type}”',
42 | 'Max Nodes' => 'Max Nodes',
43 | 'Max Nodes per Level' => 'Max Nodes per Level',
44 | 'Name of this navigation in the control panel.' => 'Name of this navigation in the control panel.',
45 | 'Name of this node in the navigation.' => 'Name of this node in the navigation.',
46 | 'Navigation' => 'Navigation',
47 | 'Navigation Options' => 'Navigation Options',
48 | 'Navigations' => 'Navigations',
49 | 'Navigation saved.' => 'Navigation saved.',
50 | 'New navigation' => 'New navigation',
51 | 'New Window' => 'New Window',
52 | 'Node' => 'Node',
53 | 'Node Fields' => 'Node Fields',
54 | 'Nodes' => 'Nodes',
55 | 'nodes' => 'nodes',
56 | 'Node Type' => 'Node Type',
57 | 'Node Type Fields' => 'Node Type Fields',
58 | 'Node{plural} added.' => 'Node{plural} added.',
59 | 'Of the enabled sites above, which sites should nodes in this navigation be saved to?' => 'Of the enabled sites above, which sites should nodes in this navigation be saved to?',
60 | 'Only save nodes to the site they were created in' => 'Only save nodes to the site they were created in',
61 | 'Open in new window' => 'Open in new window',
62 | 'Parent' => 'Parent',
63 | 'Passive' => 'Passive',
64 | 'Permissions' => 'Permissions',
65 | 'Plugin name for the end user.' => 'Plugin name for the end user.',
66 | 'Propagation Method' => 'Propagation Method',
67 | 'Save nodes to all sites enabled for this navigation' => 'Save nodes to all sites enabled for this navigation',
68 | 'Save nodes to other sites in the same site group' => 'Save nodes to other sites in the same site group',
69 | 'Save nodes to other sites with the same language' => 'Save nodes to other sites with the same language',
70 | 'Select a navigation' => 'Select a navigation',
71 | 'Select a navigation item as the parent.' => 'Select a navigation item as the parent.',
72 | 'Select a site' => 'Select a site',
73 | 'Select a site to use its Base URL.' => 'Select a site to use its Base URL.',
74 | 'Select which element groups should be allowed to pick elements from.' => 'Select which element groups should be allowed to pick elements from.',
75 | 'Select which node types can be added when building your navigation.' => 'Select which node types can be added when building your navigation.',
76 | 'Settings' => 'Settings',
77 | 'Show {name}' => 'Show {name}',
78 | 'Site' => 'Site',
79 | 'The element this node is linked to.' => 'The element this node is linked to.',
80 | 'The maximum number of levels this navigation can have. Leave blank for no limit.' => 'The maximum number of levels this navigation can have. Leave blank for no limit.',
81 | 'The maximum number of total nodes this navigation can have. Leave blank for no limit.' => 'The maximum number of total nodes this navigation can have. Leave blank for no limit.',
82 | 'The URL for this navigation item.' => 'The URL for this navigation item.',
83 | 'The URL of this node.' => 'The URL of this node.',
84 | 'Title' => 'Title',
85 | 'Type' => 'Type',
86 | 'Unable to duplicate navigation.' => 'Unable to duplicate navigation.',
87 | 'Unable to save navigation.' => 'Unable to save navigation.',
88 | 'URL' => 'URL',
89 | 'URL Suffix' => 'URL Suffix',
90 | 'Value' => 'Value',
91 | 'View all navigations' => 'View all navigations',
92 | 'View navigation - {nav}' => 'View navigation - {nav}',
93 | 'Where new {type} should be placed by default in the structure.' => 'Where new {type} should be placed by default in the structure.',
94 | 'Whether to allow {name} nodes to be added.' => 'Whether to allow {name} nodes to be added.',
95 | 'Whether to allow {name} to be added.' => 'Whether to allow {name} to be added.',
96 | 'Whether to open this navigation item in a new window.' => 'Whether to open this navigation item in a new window.',
97 | ];
--------------------------------------------------------------------------------
/src/translations/fr/navigation.php:
--------------------------------------------------------------------------------
1 | 'Ajouter une catégorie',
5 | 'Add an Asset' => 'Ajouter une ressource',
6 | 'Add an Entry' => 'Ajouter une entrée',
7 | 'Add a Product' => 'Add a Product',
8 | 'Add a {name}' => 'Add a {name}',
9 | 'Additional attributes for this node.' => 'Attributs supplémentaires pour ce nœud.',
10 | 'Additional content appended to the element‘s URL.' => 'Contenu supplémentaire ajouté à l\'URL de l\'élément.',
11 | 'Additional CSS classes for this navigation item.' => 'Classes CSS supplémentaires pour cet élément de navigation.',
12 | 'Add {name}' => 'Ajouter {name}',
13 | 'After other {type}' => 'After other {type}',
14 | 'Attribute' => 'Attribut',
15 | 'Before other {type}' => 'Before other {type}',
16 | 'Changing this may result in data loss.' => 'Changing this may result in data loss.',
17 | 'Choose which sites this navigation should be available in.' => 'Choose which sites this navigation should be available in.',
18 | 'Classes' => 'Classes',
19 | 'Couldn’t add node.' => 'Couldn’t add node.',
20 | 'Create a new navigation' => 'Create a new navigation',
21 | 'Create any level-specific limits for the maximum number of nodes.' => 'Create any level-specific limits for the maximum number of nodes.',
22 | 'Create navigations' => 'Create navigations',
23 | 'Custom Attributes' => 'Attributs personnalisés',
24 | 'Custom URL' => 'URL personnalisée',
25 | 'Default {type} Placement' => 'Default {type} Placement',
26 | 'Delete navigation' => 'Supprimer la navigation',
27 | 'Duplicate' => 'Duplicate',
28 | 'Edit' => 'Éditer',
29 | 'Edit navigation settings' => 'Éditer les paramètres de navigation',
30 | 'Edit Nodes' => 'Edit Nodes',
31 | 'Element ID is required.' => 'Element ID is required.',
32 | 'Exceeded maximum allowed nodes ({number}) for this nav.' => 'Exceeded maximum allowed nodes ({number}) for this nav.',
33 | 'Exceeded maximum allowed nodes for this level.' => 'Exceeded maximum allowed nodes for this level.',
34 | 'General Settings' => 'Paramètres généraux',
35 | 'How you’ll refer to this navigation in the templates.' => 'How you’ll refer to this navigation in the templates.',
36 | 'Level {level}' => 'Level {level}',
37 | 'Linked Element ID is required.' => 'Linked Element ID is required.',
38 | 'Linked to {element}' => 'Lié à {element}',
39 | 'Manage “{type}”' => 'Manage “{type}”',
40 | 'Max Nodes' => 'Max Nodes',
41 | 'Max Nodes per Level' => 'Max Nodes per Level',
42 | 'Name of this navigation in the control panel.' => 'Name of this navigation in the control panel.',
43 | 'Name of this node in the navigation.' => 'Nom de ce nœud dans la navigation.',
44 | 'Navigation' => 'Navigation',
45 | 'Navigation Options' => 'Navigation Options',
46 | 'Navigations' => 'Navigations',
47 | 'Navigation saved.' => 'Navigation enregistrée.',
48 | 'New navigation' => 'Nouvelle navigation',
49 | 'New Window' => 'Nouvelle fenêtre',
50 | 'Node' => 'Nœud',
51 | 'Node Fields' => 'Champs du nœud',
52 | 'Nodes' => 'Nœuds',
53 | 'nodes' => 'nodes',
54 | 'Node Type' => 'Node Type',
55 | 'Node Type Fields' => 'Champs de type de nœud',
56 | 'Node{plural} added.' => 'Node{plural} added.',
57 | 'Of the enabled sites above, which sites should nodes in this navigation be saved to?' => 'Of the enabled sites above, which sites should nodes in this navigation be saved to?',
58 | 'Only save nodes to the site they were created in' => 'Only save nodes to the site they were created in',
59 | 'Open in new window' => 'Ouvrir dans une nouvelle fenêtre',
60 | 'Parent' => 'Parent',
61 | 'Passive' => 'Passif',
62 | 'Permissions' => 'Permissions',
63 | 'Plugin name for the end user.' => 'Nom du plugin pour l\'utilisateur final.',
64 | 'Propagation Method' => 'Propagation Method',
65 | 'Save nodes to all sites enabled for this navigation' => 'Save nodes to all sites enabled for this navigation',
66 | 'Save nodes to other sites in the same site group' => 'Save nodes to other sites in the same site group',
67 | 'Save nodes to other sites with the same language' => 'Save nodes to other sites with the same language',
68 | 'Select a navigation' => 'Select a navigation',
69 | 'Select a navigation item as the parent.' => 'Sélectionnez un élément de navigation comme parent.',
70 | 'Select a site' => 'Sélectionnez un site',
71 | 'Select a site to use its Base URL.' => 'Sélectionnez un site pour utiliser son URL de base.',
72 | 'Select which element groups should be allowed to pick elements from.' => 'Select which element groups should be allowed to pick elements from.',
73 | 'Select which node types can be added when building your navigation.' => 'Select which node types can be added when building your navigation.',
74 | 'Settings' => 'Paramètres',
75 | 'Show {name}' => 'Show {name}',
76 | 'Site' => 'Site',
77 | 'The element this node is linked to.' => 'L\'élément auquel ce nœud est lié.',
78 | 'The maximum number of levels this navigation can have. Leave blank for no limit.' => 'The maximum number of levels this navigation can have. Leave blank for no limit.',
79 | 'The maximum number of total nodes this navigation can have. Leave blank for no limit.' => 'The maximum number of total nodes this navigation can have. Leave blank for no limit.',
80 | 'The URL for this navigation item.' => 'L\'URL de cet élément de navigation.',
81 | 'The URL of this node.' => 'L\'URL de ce nœud.',
82 | 'Title' => 'Titre',
83 | 'Type' => 'Type',
84 | 'Unable to duplicate navigation.' => 'Unable to duplicate navigation.',
85 | 'Unable to save navigation.' => 'Unable to save navigation.',
86 | 'URL' => 'URL',
87 | 'URL Suffix' => 'Suffixe URL',
88 | 'Value' => 'Valeur',
89 | 'View all navigations' => 'View all navigations',
90 | 'View navigation - {nav}' => 'View navigation - {nav}',
91 | 'Where new {type} should be placed by default in the structure.' => 'Where new {type} should be placed by default in the structure.',
92 | 'Whether to allow {name} nodes to be added.' => 'Whether to allow {name} nodes to be added.',
93 | 'Whether to allow {name} to be added.' => 'Whether to allow {name} to be added.',
94 | 'Whether to open this navigation item in a new window.' => 'Ouvrir cet élément de navigation dans une nouvelle fenêtre ou non.',
95 | 'Select a navigation item as the parent.' => 'Sélectionnez un élément de navigation comme parent.',
96 | 'Open in new window' => 'Ouvrir dans une nouvelle fenêtre',
97 | 'Title' => 'Titre',
98 | 'Name of this node in the navigation.' => 'Nom de ce nœud dans la navigation.',
99 | 'The URL of this node.' => 'L\'URL de ce nœud.',
100 | 'Nothing yet.' => 'Rien pour le moment.',
101 | 'Max Nodes' => 'Nombre maximum de nœuds',
102 | 'Name of this navigation in the control panel.' => 'Nom de cette navigation dans le panneau de configuration.',
103 | 'How you’ll refer to this navigation in the templates.' => 'Comment vous ferez référence à cette navigation dans les modèles.',
104 | 'Helper text to guide the author. Shown at the top of the page when editing a navigation.' => 'Texte d\'aide pour guider l\'auteur. Affiché en haut de la page lors de la modification d\'une navigation.',
105 | 'The maximum number of levels this navigation can have. Leave blank for no limit.' => 'Le nombre maximum de niveaux que cette navigation peut avoir. Laissez vide pour aucune limite.',
106 | 'The maximum number of total nodes this navigation can have. Leave blank for no limit.' => 'Le nombre maximum de nœuds au total que cette navigation peut avoir. Laissez vide pour aucune limite.',
107 | 'Max Nodes per Level' => 'Nombre maximum de nœuds par niveau',
108 | 'Create any level-specific limits for the maximum number of nodes.' => 'Créez des limites spécifiques au niveau pour le nombre maximum de nœuds.',
109 | 'Node Fields' => 'Champs de nœud',
110 | 'Edit Nodes' => 'Modifier les nœuds'
111 | ];
112 |
--------------------------------------------------------------------------------
/src/translations/hu/navigation.php:
--------------------------------------------------------------------------------
1 | 'Add a Category',
5 | 'Add an Asset' => 'Add an Asset',
6 | 'Add an Entry' => 'Add an Entry',
7 | 'Add a Product' => 'Add a Product',
8 | 'Add a {name}' => 'Add a {name}',
9 | 'Additional attributes for this node.' => 'Additional attributes for this node.',
10 | 'Additional content appended to the element‘s URL.' => 'Additional content appended to the element‘s URL.',
11 | 'Additional CSS classes for this navigation item.' => 'További CSS osztályok ehhez a menüponthoz.',
12 | 'Add {name}' => 'Add {name}',
13 | 'After other {type}' => 'After other {type}',
14 | 'Attribute' => 'Attribute',
15 | 'Before other {type}' => 'Before other {type}',
16 | 'Changing this may result in data loss.' => 'Changing this may result in data loss.',
17 | 'Choose which sites this navigation should be available in.' => 'Choose which sites this navigation should be available in.',
18 | 'Classes' => 'Osztályok',
19 | 'Couldn’t add node.' => 'Couldn’t add node.',
20 | 'Create a new navigation' => 'Új menü létrehozása',
21 | 'Create any level-specific limits for the maximum number of nodes.' => 'Create any level-specific limits for the maximum number of nodes.',
22 | 'Create navigations' => 'Create navigations',
23 | 'Custom Attributes' => 'Custom Attributes',
24 | 'Custom URL' => 'Egyedi URL',
25 | 'Default {type} Placement' => 'Default {type} Placement',
26 | 'Delete navigation' => 'Delete navigation',
27 | 'Duplicate' => 'Duplicate',
28 | 'Edit' => 'Edit',
29 | 'Edit navigation settings' => 'Edit navigation settings',
30 | 'Edit Nodes' => 'Edit Nodes',
31 | 'Element ID is required.' => 'Element ID is required.',
32 | 'Exceeded maximum allowed nodes ({number}) for this nav.' => 'Exceeded maximum allowed nodes ({number}) for this nav.',
33 | 'Exceeded maximum allowed nodes for this level.' => 'Exceeded maximum allowed nodes for this level.',
34 | 'General Settings' => 'General Settings',
35 | 'How you’ll refer to this navigation in the templates.' => 'How you’ll refer to this navigation in the templates.',
36 | 'Level {level}' => 'Level {level}',
37 | 'Linked Element ID is required.' => 'Linked Element ID is required.',
38 | 'Linked to {element}' => 'Linked to {element}',
39 | 'Manage “{type}”' => 'Manage “{type}”',
40 | 'Max Nodes' => 'Max Nodes',
41 | 'Max Nodes per Level' => 'Max Nodes per Level',
42 | 'Name of this navigation in the control panel.' => 'Name of this navigation in the control panel.',
43 | 'Name of this node in the navigation.' => 'A menüpont neve.',
44 | 'Navigation' => 'Navigation',
45 | 'Navigation Options' => 'Navigation Options',
46 | 'Navigations' => 'Menük',
47 | 'Navigation saved.' => 'Navigáció elmentve.',
48 | 'New navigation' => 'Új menü',
49 | 'New Window' => 'New Window',
50 | 'Node' => 'Node',
51 | 'Node Fields' => 'Node Fields',
52 | 'Nodes' => 'Nodes',
53 | 'nodes' => 'nodes',
54 | 'Node Type' => 'Node Type',
55 | 'Node Type Fields' => 'Node Type Fields',
56 | 'Node{plural} added.' => 'Node{plural} added.',
57 | 'Of the enabled sites above, which sites should nodes in this navigation be saved to?' => 'Of the enabled sites above, which sites should nodes in this navigation be saved to?',
58 | 'Only save nodes to the site they were created in' => 'Only save nodes to the site they were created in',
59 | 'Open in new window' => 'Megnyitás új ablakban',
60 | 'Parent' => 'Szülő',
61 | 'Passive' => 'Passive',
62 | 'Permissions' => 'Permissions',
63 | 'Plugin name for the end user.' => 'A felhasználónak megjelenő beépülő modul név.',
64 | 'Propagation Method' => 'Propagation Method',
65 | 'Save nodes to all sites enabled for this navigation' => 'Save nodes to all sites enabled for this navigation',
66 | 'Save nodes to other sites in the same site group' => 'Save nodes to other sites in the same site group',
67 | 'Save nodes to other sites with the same language' => 'Save nodes to other sites with the same language',
68 | 'Select a navigation' => 'Select a navigation',
69 | 'Select a navigation item as the parent.' => 'Válaszd ki, melyik menüpont legyen az elem szülője.',
70 | 'Select a site' => 'Select a site',
71 | 'Select a site to use its Base URL.' => 'Select a site to use its Base URL.',
72 | 'Select which element groups should be allowed to pick elements from.' => 'Select which element groups should be allowed to pick elements from.',
73 | 'Select which node types can be added when building your navigation.' => 'Select which node types can be added when building your navigation.',
74 | 'Settings' => 'Settings',
75 | 'Show {name}' => 'Show {name}',
76 | 'Site' => 'Site',
77 | 'The element this node is linked to.' => 'The element this node is linked to.',
78 | 'The maximum number of levels this navigation can have. Leave blank for no limit.' => 'Szintek maximális száma a menüben. Hagyd üresen, ha mindegy.',
79 | 'The maximum number of total nodes this navigation can have. Leave blank for no limit.' => 'The maximum number of total nodes this navigation can have. Leave blank for no limit.',
80 | 'The URL for this navigation item.' => 'A menüpont URL-je',
81 | 'The URL of this node.' => 'A menüpont URL-je.',
82 | 'Title' => 'Cím',
83 | 'Type' => 'Type',
84 | 'Unable to duplicate navigation.' => 'Unable to duplicate navigation.',
85 | 'Unable to save navigation.' => 'Unable to save navigation.',
86 | 'URL' => 'URL',
87 | 'URL Suffix' => 'URL Suffix',
88 | 'Value' => 'Value',
89 | 'View all navigations' => 'View all navigations',
90 | 'View navigation - {nav}' => 'View navigation - {nav}',
91 | 'Where new {type} should be placed by default in the structure.' => 'Where new {type} should be placed by default in the structure.',
92 | 'Whether to allow {name} nodes to be added.' => 'Whether to allow {name} nodes to be added.',
93 | 'Whether to allow {name} to be added.' => 'Whether to allow {name} to be added.',
94 | 'Whether to open this navigation item in a new window.' => 'A menüpont új ablakban nyíljon-e meg.',
95 | ];
--------------------------------------------------------------------------------
/src/translations/nl/navigation.php:
--------------------------------------------------------------------------------
1 | 'Een categorie toevoegen',
5 | 'Add an Asset' => 'Een bestand toevoegen',
6 | 'Add an Entry' => 'Een item toevoegen',
7 | 'Add a Product' => 'Een product toevoegen',
8 | 'Add a {name}' => 'Een {name} toevoegen',
9 | 'Additional attributes for this node.' => 'Extra attributen voor dit menu-item.',
10 | 'Additional content appended to the element‘s URL.' => 'Extra inhoud toegevoegd aan de URL van het element.',
11 | 'Additional CSS classes for this navigation item.' => 'Extra CSS-klassen voor dit menu-item.',
12 | 'Add {name}' => '{name} toevoegen',
13 | 'After other {type}' => 'Na andere {type}',
14 | 'Attribute' => 'Attribuut',
15 | 'Before other {type}' => 'Voor andere {type}',
16 | 'Changing this may result in data loss.' => 'Het wijzigen hiervan kan leiden tot gegevensverlies.',
17 | 'Choose which sites this navigation should be available in.' => 'Kies op welke sites dit menu beschikbaar moet zijn.',
18 | 'Classes' => 'Klassen',
19 | 'Couldn’t add node.' => 'Kon node niet toevoegen.',
20 | 'Create a new navigation' => 'Een nieuw menu maken',
21 | 'Create any level-specific limits for the maximum number of nodes.' => 'Maak eventuele niveau-specifieke limieten voor het maximale aantal menu-items.',
22 | 'Create navigations' => 'Menu\'s aanmaken',
23 | 'Custom Attributes' => 'Aangepaste attributen',
24 | 'Custom URL' => 'Aangepaste URL',
25 | 'Default {type} Placement' => 'Standaard {type} plaatsing',
26 | 'Delete navigation' => 'Menu verwijderen',
27 | 'Duplicate' => 'Dupliceren',
28 | 'Edit' => 'Bewerken',
29 | 'Edit navigation settings' => 'Menu-instellingen bewerken',
30 | 'Edit Settings' => 'Instellingen bewerken',
31 | 'Edit Nodes' => 'Nodes bewerken',
32 | 'Element ID is required.' => 'Element-ID is vereist.',
33 | 'Exceeded maximum allowed nodes ({number}) for this nav.' => 'Maximaal toegestane menu-items ({number}) voor deze nav overschreden.',
34 | 'Exceeded maximum allowed nodes for this level.' => 'Maximaal toegestane menu-items voor dit niveau overschreden.',
35 | 'General Settings' => 'Algemene instellingen',
36 | 'Helper text to guide the author. Shown at the top of the page when editing a navigation.' => 'Helptekst om de auteur te begeleiden. Wordt bovenaan de pagina weergegeven bij het bewerken van een menu.',
37 | 'How you’ll refer to this navigation in the templates.' => 'Hoe je naar dit menu zult verwijzen in de templates.',
38 | 'Level {level}' => 'Niveau {level}',
39 | 'Linked Element ID is required.' => 'Gekoppelde element-ID is vereist.',
40 | 'Linked to {element}' => 'Gekoppeld aan {element}',
41 | 'Manage \'{type}\'' => '\'{type}\' beheren',
42 | 'Max Nodes' => 'Max. menu-items',
43 | 'Max Nodes per Level' => 'Max. menu-items per niveau',
44 | 'Name of this navigation in the control panel.' => 'Naam van dit menu in het controlepaneel.',
45 | 'Name of this node in the navigation.' => 'Naam van dit menu-item in het menu.',
46 | 'Navigation' => 'Menu',
47 | 'Navigation Options' => 'Menu-opties',
48 | 'Navigations' => 'Menu\'s',
49 | 'Navigation saved.' => 'Menu opgeslagen.',
50 | 'New navigation' => 'Nieuw menu',
51 | 'New Window' => 'Nieuw venster',
52 | 'Node' => 'Menu-item',
53 | 'Node Fields' => 'Menu-item velden',
54 | 'Nodes' => 'Menu-items',
55 | 'nodes' => 'menu-items',
56 | 'Node Type' => 'Menu-item type',
57 | 'Node Type Fields' => 'Menu-item type velden',
58 | 'Node{plural} added.' => 'Menu-item{plural} toegevoegd.',
59 | 'Of the enabled sites above, which sites should nodes in this navigation be saved to?' => 'Van de bovenstaande ingeschakelde sites, op welke sites moeten menu-items in dit menu worden opgeslagen?',
60 | 'Only save nodes to the site they were created in' => 'Sla menu-items alleen op in de site waarin ze zijn gemaakt',
61 | 'Open in new window' => 'Openen in nieuw venster',
62 | 'Parent' => 'Ouder',
63 | 'Passive' => 'Passief menu-item',
64 | 'Permissions' => 'Rechten',
65 | 'Plugin name for the end user.' => 'Plugin-naam voor de eindgebruiker.',
66 | 'Propagation Method' => 'Verspreidingsmethode',
67 | 'Save nodes to all sites enabled for this navigation' => 'Sla menu-items op voor alle sites die voor dit menu zijn ingeschakeld',
68 | 'Save nodes to other sites in the same site group' => 'Sla menu-items op voor andere sites in dezelfde sitegroep',
69 | 'Save nodes to other sites with the same language' => 'Sla menu-items op voor andere sites met dezelfde taal',
70 | 'Select a navigation' => 'Selecteer een menu',
71 | 'Select a navigation item as the parent.' => 'Selecteer een menu-item als ouder.',
72 | 'Select a site' => 'Selecteer een site',
73 | 'Select a site to use its Base URL.' => 'Selecteer een site om de basis-URL ervan te gebruiken.',
74 | 'Select which element groups should be allowed to pick elements from.' => 'Selecteer uit welke elementgroepen elementen mogen worden gekozen.',
75 | 'Select which node types can be added when building your navigation.' => 'Selecteer welke menu-item types kunnen worden toegevoegd bij het bouwen van je menu.',
76 | 'Settings' => 'Instellingen',
77 | 'Show {name}' => 'Toon {name}',
78 | 'Site' => 'Site',
79 | 'The element this node is linked to.' => 'Het element waaraan dit menu-item is gekoppeld.',
80 | 'The maximum number of levels this navigation can have. Leave blank for no limit.' => 'Het maximale aantal niveaus dat dit menu kan hebben. Laat leeg voor geen limiet.',
81 | 'The maximum number of total nodes this navigation can have. Leave blank for no limit.' => 'Het maximale aantal menu-items dat dit menu kan hebben. Laat leeg voor geen limiet.',
82 | 'The URL for this navigation item.' => 'De URL voor dit menu-item.',
83 | 'The URL of this node.' => 'De URL van dit menu-items.',
84 | 'Title' => 'Titel',
85 | 'Type' => 'Type',
86 | 'Unable to duplicate navigation.' => 'Kan menu niet dupliceren.',
87 | 'Unable to save navigation.' => 'Kan menu niet opslaan.',
88 | 'URL' => 'URL',
89 | 'URL Suffix' => 'URL-achtervoegsel',
90 | 'Value' => 'Waarde',
91 | 'View all navigations' => 'Alle menu\'s bekijken',
92 | 'View navigation - {nav}' => 'Menu bekijken - {nav}',
93 | 'Where new {type} should be placed by default in the structure.' => 'Waar nieuwe {type} standaard in de structuur moet worden geplaatst.',
94 | 'Whether to allow {name} nodes to be added.' => 'Of {name} menu-items mogen worden toegevoegd.',
95 | 'Whether to allow {name} to be added.' => 'Of {name} mag worden toegevoegd.',
96 | 'Whether to open this navigation item in a new window.' => 'Of dit menu-item in een nieuw venster moet worden geopend.',
97 | ];
--------------------------------------------------------------------------------
/src/variables/NavigationVariable.php:
--------------------------------------------------------------------------------
1 | getPluginName();
23 | }
24 |
25 | public function getRegisteredElements(): array
26 | {
27 | return Navigation::$plugin->getElements()->getRegisteredElements();
28 | }
29 |
30 | public function getRegisteredNodeTypes(): array
31 | {
32 | return Navigation::$plugin->getNodeTypes()->getRegisteredNodeTypes();
33 | }
34 |
35 | public function getActiveNode($criteria = null, $includeChildren = false): ?NodeElement
36 | {
37 | $nodes = $this->nodes($criteria)->all();
38 |
39 | foreach ($nodes as $node) {
40 | if ($node->getActive($includeChildren)) {
41 | return $node;
42 | }
43 | }
44 |
45 | return null;
46 | }
47 |
48 | public function nodes($criteria = null): NodeQuery
49 | {
50 | if ($criteria instanceof NodeQuery) {
51 | $query = $criteria;
52 | } else {
53 | $query = NodeElement::find();
54 | }
55 |
56 | if ($criteria) {
57 | if (is_string($criteria)) {
58 | $criteria = ['handle' => $criteria];
59 | }
60 |
61 | Craft::configure($query, $criteria);
62 | }
63 |
64 | return $query;
65 | }
66 |
67 | public function render($criteria = null, array $options = []): Markup
68 | {
69 | $query = $this->nodes($criteria);
70 |
71 | // Add eager-loading in by default. Generate a map for `children.children.children.etc`
72 | $eagerLoadingMap = [];
73 |
74 | for ($i = 1; $i < 8; $i++) {
75 | $eagerLoadingMap[] = rtrim(str_repeat('children.', $i), '.');
76 | }
77 |
78 | $query->with($eagerLoadingMap);
79 |
80 | $nodes = $query->all();
81 |
82 | $template = Craft::$app->getView()->renderTemplate('navigation/_special/render', [
83 | 'nodes' => $nodes,
84 | 'options' => $options,
85 | ], View::TEMPLATE_MODE_CP);
86 |
87 | return Template::raw($template);
88 | }
89 |
90 | public function breadcrumbs(array $options = []): array
91 | {
92 | return Navigation::$plugin->getBreadcrumbs()->getBreadcrumbs($options);
93 | }
94 |
95 | public function tree($criteria = null): array
96 | {
97 | $nodes = $this->nodes($criteria)->level(1)->all();
98 |
99 | $nodeTree = [];
100 |
101 | Navigation::$plugin->getNavs()->buildNavTree($nodes, $nodeTree);
102 |
103 | return $nodeTree;
104 | }
105 |
106 | public function getNavById($id): ?Nav
107 | {
108 | return Navigation::$plugin->getNavs()->getNavById($id);
109 | }
110 |
111 | public function getNavByHandle($handle): ?Nav
112 | {
113 | return Navigation::$plugin->getNavs()->getNavByHandle($handle);
114 | }
115 |
116 | public function getAllNavs(): array
117 | {
118 | return Navigation::$plugin->getNavs()->getAllNavs();
119 | }
120 |
121 | public function getBuilderTabs($nav): array
122 | {
123 | return Navigation::$plugin->getNavs()->getBuilderTabs($nav);
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------